Inputs - Definitions¶
Inputs declare the task's interface. Each declaration generates an accessor and wires up coercion, validation, defaults, transforms, and :if/:unless gates.
Declarations¶
Important
Inputs are order-dependent. If one input references another as a source or condition, the referenced input must be defined first.
# Correct: credentials defined before connection_string
required :credentials, source: :database_config
input :connection_string, source: :credentials
# Wrong: connection_string references credentials before it exists
input :connection_string, source: :credentials
required :credentials, source: :database_config
Important
Input names that conflict with existing Ruby or CMDx methods raise CMDx::DefinitionError at class-load time. Use :as, :prefix, or :suffix to resolve naming conflicts. See Naming.
Tip
Prefer the required and optional shorthands over inputs(..., required: …) — they read better and make intent obvious at a glance.
Optional¶
Optional inputs return nil when not provided.
class ScheduleEvent < CMDx::Task
input :title
inputs :duration, :location
# Shorthand for inputs ..., required: false (preferred)
optional :description
optional :visibility, :attendees
def work
title #=> "Team Standup"
duration #=> 30
location #=> nil
description #=> nil
visibility #=> nil
attendees #=> ["alice@company.com", "bob@company.com"]
end
end
# Inputs passed as keyword arguments
ScheduleEvent.execute(
title: "Team Standup",
duration: 30,
attendees: ["alice@company.com", "bob@company.com"]
)
Required¶
Required inputs must be provided in call arguments or task execution will fail.
class PublishArticle < CMDx::Task
input :title, required: true
inputs :content, :author_id, required: true
# Shorthand for inputs ..., required: true (preferred)
required :category
required :status, :tags
# Conditionally required
required :publisher, if: :magazine?
input :approver, required: true, unless: proc { status == :published }
def work
title #=> "Getting Started with Ruby"
content #=> "This is a comprehensive guide..."
author_id #=> 42
category #=> "programming"
status #=> :published
tags #=> ["ruby", "beginner"]
publisher #=> "Eastbay"
approver #=> #<Editor ...>
end
private
def magazine?
context.title.end_with?("[M]")
end
end
Note
A required input with a falsy :if/:unless gate behaves as optional. Coercions, validations, defaults, and transformations still apply.
Removals¶
Remove inherited or previously defined inputs and their accessor methods via deregister. The lookup key is always the original input name — :as, :prefix, and :suffix only affect the generated accessor, not the registry key:
class ApplicationTask < CMDx::Task
required :tenant_id
optional :debug_mode
required :user_id, as: :customer_id # accessor: customer_id
end
class PublicTask < ApplicationTask
deregister :input, :tenant_id
deregister :input, :debug_mode
deregister :input, :user_id # deregister by original name, NOT :customer_id
def work
# tenant_id, debug_mode, and user_id (customer_id) are no longer defined
end
end
Important
deregister :input, *names removes inputs (and any nested children). Unknown names raise NoMethodError.
Introspection¶
Inspect the full input schema for tooling, documentation generation, or debugging:
class CreateUser < CMDx::Task
required :email, coerce: :string, format: /\A.+@.+\z/
optional :role, default: "member", inclusion: { in: %w[member admin] }
end
CreateUser.inputs_schema
#=> {
# email: { name: :email, description: nil, required: true,
# options: { required: true, coerce: :string, format: /\A.+@.+\z/ },
# children: [] },
# role: { name: :role, ... }
# }
Each entry exposes :name (the accessor name, post-:as/:prefix/:suffix), :description, :required, the raw declaration :options, and any nested :children recursively.
Note
:required in the schema is the static flag — :if / :unless gates aren't evaluated at schema time. Inspect options[:if] / options[:unless] directly when generating docs.
Note
Failed coercion/validation leaves the backing ivar at nil, records the message on task.errors under the accessor name, skips nested children, and throws a failed signal before work runs.
Sources¶
Inputs read from any accessible object — not just context. The default source is :context; override with source: to pull data from a method, proc, callable class, or another already-defined input:
Context¶
class BackupDatabase < CMDx::Task
# Default source is :context
required :database_name
optional :compression_level
# Explicitly specify context source
input :backup_path, source: :context
def work
database_name #=> context.database_name
backup_path #=> context.backup_path
compression_level #=> context.compression_level
end
end
Symbol References¶
Reference instance methods by symbol for dynamic source values:
class BackupDatabase < CMDx::Task
inputs :host, :credentials, source: :database_config
# Access from declared inputs
input :connection_string, source: :credentials
def work
# Your logic here...
end
private
def database_config
@database_config ||= DatabaseConfig.find(context.database_name)
end
end
Proc or Lambda¶
Use anonymous functions for dynamic source values:
class BackupDatabase < CMDx::Task
# Proc
input :timestamp, source: proc { Time.current }
# Lambda
input :server, source: -> { Current.server }
end
Class or Module¶
For complex source logic, use classes or modules:
class DatabaseResolver
def self.call(task)
Database.find(task.context.database_name)
end
end
class BackupDatabase < CMDx::Task
# Class or Module
input :schema, source: DatabaseResolver
# Instance
input :metadata, source: DatabaseResolver.new
end
Description¶
Add metadata to inputs for documentation or introspection purposes.
class CreateUser < CMDx::Task
required :email, description: "The user's primary email address"
# Alias :desc
optional :phone, desc: "Primary contact number"
# Bulk definition - description applies to all
inputs :first_name, :last_name, desc: "Part of user's legal name"
end
Nesting¶
Build complex structures with nested inputs. Children resolve from the parent's value (via respond_to?, #[], or #key?) and support all input options except :source — nested children always read from the parent and ignore any :source on their own declaration.
Note
Nested inputs support all features: naming, coercions, validations, defaults, and more.
class ConfigureServer < CMDx::Task
# Required parent with required children
required :network_config do
required :hostname, :port, :protocol, :subnet
optional :load_balancer
input :firewall_rules
end
# Optional parent with conditional children
optional :ssl_config do
required :certificate_path, :private_key # Only required if ssl_config provided
optional :enable_http2, prefix: true
end
# Multi-level nesting
input :monitoring do
required :provider
optional :alerting do
required :threshold_percentage
optional :notification_channel
end
end
def work
network_config #=> { hostname: "api.company.com" ... }
hostname #=> "api.company.com"
load_balancer #=> nil
end
end
ConfigureServer.execute(
server_id: "srv-001",
network_config: {
hostname: "api.company.com",
port: 443,
protocol: "https",
subnet: "10.0.1.0/24",
firewall_rules: "allow_web_traffic"
},
monitoring: {
provider: "datadog",
alerting: {
threshold_percentage: 85.0,
notification_channel: "slack"
}
}
)
Important
Child requirements only apply when the parent is provided, which is what you want for optional structures.
Error Handling¶
Resolution failures (missing required inputs, coercion failures, validator failures) accumulate on task.errors. When resolution finishes and errors exist, Runtime throws a failed signal: the joined sentence becomes result.reason; the structured map is exposed on result.errors.
Note
Nested inputs are only resolved when their parent is present and non-nil.
class ConfigureServer < CMDx::Task
required :server_id, :environment
required :network_config do
required :hostname, :port
end
def work
# Your logic here...
end
end
# Missing required top-level inputs
result = ConfigureServer.execute(server_id: "srv-001")
result.state #=> "interrupted"
result.status #=> "failed"
result.reason #=> "environment is required. network_config is required"
result.metadata #=> {}
result.errors.to_h #=> {
# environment: ["is required"],
# network_config: ["is required"]
# }
result.errors.full_messages
#=> {
# environment: ["environment is required"],
# network_config: ["network_config is required"]
# }
# Missing required nested inputs
result = ConfigureServer.execute(
server_id: "srv-001",
environment: "production",
network_config: { hostname: "api.company.com" } # Missing port
)
result.state #=> "interrupted"
result.status #=> "failed"
result.reason #=> "port is required"
result.errors.to_h #=> { port: ["is required"] }