name: cmdx description: Build, debug, and optimize CMDx tasks and workflows in Ruby. Use when creating service/command objects with CMDx, composing business logic into workflows, handling task failures and interruptions, or working with CMDx attributes, callbacks, middleware, and configuration.
CMDx Agent Skill
CMDx is a Ruby framework for composable command/service objects with built-in attribute validation, type coercion, error handling, and observability.
For full documentation, see the docs/ directory or the LLM reference. Key doc pages are linked throughout this skill via progressive disclosure.
Task Lifecycle (CERO)
Every task follows: Compose → Execute → React → Observe.
middlewares.call!(task) do
before_validation → define & validate attributes → fail! if errors
before_execution → result.executing! → task.work
verify returns
rescue Fault → result.throw!(fault.result)
rescue StandardError → retry or result.fail!
ensure
result.executed!
on_complete / on_interrupted # by state
on_executed # if execution ran
on_success / on_skipped / on_failed # by status
on_good / on_bad # by status group
log → rollback? → freeze → clear chain
end
Minimal Task
class Greet < CMDx::Task
required :name, type: :string, presence: true
def work
context.greeting = "Hello, #{name}!"
end
end
result = Greet.execute(name: "World")
result.success? #=> true
result.context.greeting #=> "Hello, World!"
Full-Featured Task
class ProcessPayment < CMDx::Task
settings(
retries: 3,
retry_on: [Gateway::TimeoutError],
retry_jitter: :exponential_backoff,
rollback_on: ["failed"],
tags: ["billing"]
)
register :middleware, CMDx::Middlewares::Timeout, seconds: 10
register :middleware, CMDx::Middlewares::Runtime
before_execution :find_order
on_success :send_receipt
on_failed :alert_support, if: -> { context.amount > 1000 }
required :order_id, type: :integer
required :amount, type: :big_decimal, numeric: { greater_than: 0 }
optional :currency, type: :string, default: "USD", inclusion: { in: %w[USD EUR GBP] }
optional :idempotency_key, type: :string, default: -> { SecureRandom.uuid }
returns :charge_id, :receipt_url
def work
charge = Gateway.charge!(amount: amount, currency: currency, key: idempotency_key)
context.charge_id = charge.id
context.receipt_url = charge.receipt_url
end
def rollback
Gateway.refund!(context.charge_id) if context.charge_id
end
private
def find_order
@order = Order.find(order_id)
fail!("Order already paid") if @order.paid?
end
def send_receipt
ReceiptMailer.send(@order, context.receipt_url).deliver_later
end
def alert_support
SupportNotifier.high_value_failure(@order, result.reason)
end
end
Workflow Example
Workflows compose tasks into sequential or parallel execution groups.
class OnboardCustomer < CMDx::Task
include CMDx::Workflow
task ValidateIdentity
task CreateAccount, breakpoints: %w[failed]
task SetupBilling, if: :billing_required?
tasks SendWelcomeEmail, SendWelcomeSms, strategy: :parallel
private
def billing_required?
context.plan != "free"
end
end
result = OnboardCustomer.execute(email: "user@example.com", plan: "pro")
Workflows share a single context across all tasks. A failing task halts execution when its status matches the group's breakpoints (default: ["failed"]).
For advanced patterns, see references/workflows.md and docs/workflows.md.
Attributes
Declared with required, optional, or attribute:
class Example < CMDx::Task
required :email, type: :string, format: { with: URI::MailTo::EMAIL_REGEXP }
optional :role, type: :string, default: "member", inclusion: { in: %w[admin member guest] }
attribute :notes, required: false
def work
# Attributes accessible as methods: email, role, notes
# These return coerced/validated values (see pitfall #2)
end
end
Pipeline
Each attribute flows through: Source → Coerce → Transform → Validate.
Type coercion
Built-in types: :array, :big_decimal, :boolean, :complex, :date, :datetime, :float, :hash, :integer, :rational, :string, :symbol, :time.
required :count, type: :integer # single type
required :value, types: [Integer, Float] # multiple types
Validations
Built-in: presence, absence, format, length, numeric, inclusion, exclusion.
required :age, type: :integer, numeric: { greater_than: 0, less_than: 150 }
required :code, type: :string, length: { is: 6 }, format: { with: /\A[A-Z0-9]+\z/ }
optional :tags, type: :array, length: { maximum: 10 }
Naming
attribute :template, prefix: true # method: context_template
attribute :format, prefix: "report_" # method: report_format
attribute :branch, suffix: true # method: branch_context
attribute :scheduled_at, as: :when # method: when
Transforms
attribute :email, transform: :strip
attribute :tags, transform: :compact_blank
attribute :score, type: :integer, transform: proc { |v| v.clamp(0, 100) }
For the complete attribute reference, see references/attributes.md. Deep dives: docs/attributes/definitions.md, docs/attributes/coercions.md, docs/attributes/validations.md.
Interruptions
skip! and fail!
def work
skip!("Already processed") # halts
skip!("Duplicate", halt: false) # continues
fail!("Not found", code: 404) # halts
fail!("Validation failed", halt: false, errors: validation_errors) # continues
end
throw!
Propagates another task's result upward:
def work
inner_result = InnerTask.execute(context)
throw!(inner_result) if inner_result.failed?
end
Faults
execute never raises — returns a Result. execute! raises CMDx::FailFault or CMDx::SkipFault when the status matches breakpoints.
result = MyTask.execute(data: input)
result.success?
begin
result = MyTask.execute!(data: input)
rescue CMDx::FailFault => e
e.result # the failed Result
e.context # the context (delegated from result)
e.message # failure reason string (set from result.reason)
e.result.reason # same reason via result
end
Fault matching
for? and matches? are class methods that return matcher classes for use in rescue:
rescue CMDx::FailFault.for?(PaymentTask, BillingTask) => e
# only catches FailFaults from PaymentTask or BillingTask
rescue CMDx::FailFault.matches? { |f| f.result.metadata[:critical] } => e
# only catches FailFaults where the block returns true
end
Result
result = MyTask.execute(input: data)
# State: initialized, executing, complete, interrupted
result.state #=> "complete"
result.complete? #=> true
result.interrupted? #=> false
# Status: success, skipped, failed
result.status #=> "success"
result.success? #=> true
result.good? #=> true (success OR skipped)
result.bad? #=> false (skipped OR failed)
# Data
result.context # shared Context object
result.reason # skip/fail reason string
result.cause # the Fault that caused interruption
result.metadata # hash of extra data (errors, runtime, etc.)
result.chain # Chain of results in execution
# Handlers
result.on(:success) { |r| redirect_to(dashboard_path) }
.on(:failed) { |r| render_error(r.reason) }
.on(:skipped) { |r| log_skip(r.reason) }
# Pattern matching
case result
in ["complete", "success"] then handle_success
in ["interrupted", "failed"] then handle_failure
end
Callbacks
Registered as class methods. Accept method names, procs, or blocks. Support if:/unless: conditions.
class Example < CMDx::Task
before_validation :normalize_input
before_execution :load_dependencies
on_success :notify_user, if: -> { context.notify? }
on_failed :log_failure
on_complete :cleanup
end
Execution order
before_validation— before attribute validationbefore_execution— after validation, beforeworkon_complete/on_interrupted— by stateon_executed— if execution ranon_success/on_skipped/on_failed— by statuson_good/on_bad— by status group
Middleware
Wraps the entire execution. Must yield.
# Built-in
register :middleware, CMDx::Middlewares::Timeout, seconds: 5
register :middleware, CMDx::Middlewares::Runtime # result.metadata[:runtime]
register :middleware, CMDx::Middlewares::Correlate, id: proc { SecureRandom.uuid }
# Custom
class AuditMiddleware
def self.call(task, **options)
AuditLog.start(task.class.name)
yield(task)
ensure
AuditLog.finish(task.class.name, task.result.status)
end
end
register :middleware, AuditMiddleware
Configuration
Global
CMDx.configure do |config|
config.task_breakpoints = "failed"
config.workflow_breakpoints = ["skipped", "failed"]
config.rollback_on = ["failed"]
config.freeze_results = true
config.backtrace = false
config.logger = Logger.new($stdout)
config.exception_handler = proc { |task, e| ErrorTracker.report(e) }
end
Per-task
class MyTask < CMDx::Task
settings(
retries: 3,
retry_on: [Net::TimeoutError],
retry_jitter: :exponential_backoff,
rollback_on: ["failed"],
task_breakpoints: ["failed"],
tags: ["critical"],
log_level: :info,
log_formatter: CMDx::LogFormatters::Json.new,
deprecate: :log
)
end
For all options, see references/configuration.md and docs/configuration.md.
Returns (Output Contract)
class CreateUser < CMDx::Task
returns :user, :token
def work
context.user = User.create!(params)
context.token = generate_token(context.user)
end
end
Missing returns cause the task to fail with validation errors.
Context
A shared, hash-like object passed through tasks:
context[:key] # read
context.key # read (method_missing)
context.key = value # write
context.store(:key, value) # write
context.merge!(hash) # bulk write
context.fetch(:key, default) # read with default
context.fetch_or_store(:key, v) # read or write
context.key?(:key) # existence check
context.dig(:a, :b, :c) # nested read
context.delete!(:key) # remove
Retries
settings retries: 3, retry_on: [Net::TimeoutError]
settings retries: 5, retry_jitter: :exponential_backoff
settings retries: 10, retry_jitter: ->(count) { [count * 0.5, 5.0].min }
Only retries when the rescued exception matches retry_on. Clears errors between attempts. See docs/retries.md.
Dry Run
result = MyTask.execute(data: input, dry_run: true)
result.dry_run? #=> true
The work method still runs — implement dry-run guards inside work using dry_run? (delegated from task to chain).
Common Pitfalls
1. Forgetting def work
Every task must define work. Without it, execution raises CMDx::UndefinedMethodError.
2. Using context vs attribute methods
# Wrong: bypasses validation/coercion
def work
context.email
end
# Right: declare attributes, use generated methods
required :email, type: :string
def work
email
end
3. Not handling throw! in nested tasks
# Wrong: inner failure is silently swallowed
def work
InnerTask.execute(context)
end
# Right: propagate or check the result
def work
inner = InnerTask.execute(context)
throw!(inner) unless inner.success?
end
4. Mutating context after freeze
Results are frozen by default (freeze_results: true). Attempting to modify context after execution raises an error. Set freeze_results: false if post-execution mutation is needed.
5. Middleware that doesn't yield
A middleware that omits yield silently swallows execution. The result will be marked as failed with metadata[:source] == :swallowed_middleware.
6. Breakpoints confusion
task_breakpoints: controls whenexecute!raises (default:["failed"])workflow_breakpoints: controls when a workflow halts (default:["failed"])- Group breakpoints:
tasks TaskA, TaskB, breakpoints: %w[skipped failed] - Empty breakpoints
[]means never halt
7. Missing returns
Declared returns are verified after work. If the context doesn't contain the declared keys, the task fails with validation errors.
8. Attribute ordering
Attributes are order-dependent. If one attribute references another as a source or condition, declare the referenced attribute first:
# Correct
required :credentials, source: :database_config
attribute :connection_string, source: :credentials
# Wrong: connection_string references credentials before it exists
attribute :connection_string, source: :credentials
required :credentials, source: :database_config
9. Exception vs Fault
StandardError exceptions are caught by execute and converted to failed results. execute! re-raises them. CMDx::TimeoutError inherits from Interrupt, not StandardError — it's always raised.
References
- Attribute details — coercions, validations, naming, transforms, nesting
- Workflow patterns — composition, breakpoints, parallel, conditions
- Interruptions & faults — skip!/fail!/throw!, propagation strategies, fault matching, errors
- Result API — states, statuses, handlers, pattern matching, chain analysis
- Configuration options — global and per-task settings
- Testing guide — RSpec matchers, setup, patterns
Deep-dive docs
- Basics: setup, execution, context, chain
- Interruptions: halt, faults, exceptions
- Outcomes: result, states, statuses
- Features: callbacks, middlewares, workflows, retries, logging
- Testing | Tips & tricks | Comparison