The runtime,
rewritten.
Signals in. Immutable results out.
A business-logic framework powered by an explicit signal-based runtime.
The code you keep rewriting.
Every Rails app has a 300-line controller like this. CMDx gives it a home.
class PaymentsController < ApplicationController
before_action :authenticate_user!
def create
order = current_user.orders.find(params[:order_id])
return redirect_to(order) if order.paid?
if params[:token].blank?
flash[:error] = "Missing token"
return render :new
end
ActiveRecord::Base.transaction do
charge = Stripe::Charge.create(
amount: order.total_cents,
currency: "usd",
source: params[:token],
idempotency_key: "order-#{order.id}"
)
order.update!(paid: true, charge_id: charge.id, paid_at: Time.current)
AuditLog.create!(user: current_user, action: "payment", subject: order)
OrderMailer.receipt(order).deliver_later
Analytics.track(current_user, "order_paid", order_id: order.id)
end
redirect_to order, notice: "Payment received"
rescue Stripe::CardError => e
flash[:error] = e.message
render :new
rescue => e
Rails.logger.error(e)
Sentry.capture_exception(e)
render :new
end
end
- mixed concerns
- rescue-driven flow
- not reusable
- untyped inputs
class ProcessPayment < CMDx::Task
required :order_id, coerce: :integer
required :token, coerce: :string
output :order
on_success :send_receipt!
def work
ActiveRecord::Base.transaction do
order.update!(charged: true, charge_id: charge.id)
end
context.order = order
end
private
def order = @order ||= Order.find(order_id)
def charge = Stripe::Charge.create(amount: order.total_cents, currency: "usd", source: token)
def send_receipt! = OrderMailer.receipt(order).deliver_later
end
- typed inputs
- one responsibility
- reusable anywhere
- predictable result
def create
result = ProcessPayment.execute(order_id: params[:order_id], token: params[:token])
return redirect_to(result.context.order) if result.success?
render :new, alert: result.reason
end
CERO — the four-step flow.
A single, repeatable shape behind every task.
Compose
Define typed inputs and the business rules they enforce.
required :id, coerce: :integer
Execute
Run tasks anywhere — controllers, jobs, scripts, tests.
Task.execute(id: 42)
React
Branch on success, skip, or fail — no rescues for control flow.
result.success?
result.skipped?
result.failed?
Observe
Every run ships structured logs and telemetry.
configuration.telemetry.subscribe(...)
From one task to full workflows.
From a single command to an orchestrated pipeline. Click through the files.
class ApproveLoan < CMDx::Task
required :application_id, coerce: :integer
optional :override_checks, default: false
on_success :notify_applicant!
output :approved_at
def work
if application.nil?
fail!("Application not found", code: 404)
elsif application.approved?
skip!("Already approved")
else
application.approve!
context.approved_at = Time.current
end
end
private
def application
@application ||= LoanApplication.find_by(id: application_id)
end
def notify_applicant!
ApprovalMailer.approved(application).deliver_later
end
end
class OnboardCustomer < CMDx::Task
include CMDx::Workflow
# Sequential pipeline
task CreateAccount
task SetupBilling
task AssignPlan
# Parallel notifications
tasks SendWelcomeEmail,
SendWelcomeSms,
CreateDashboard,
strategy: :parallel
# Conditional execution
task ScheduleOnboardingCall,
if: :premium_plan?
private
def premium_plan?
context.plan.tier == :premium
end
end
# Execute with typed, validated arguments
result = ApproveLoan.execute(application_id: 42)
# React to the outcome
if result.success?
puts "Approved at #{result.context.approved_at}"
elsif result.skipped?
puts "Skipped: #{result.reason}"
elsif result.failed?
puts "Failed: #{result.reason}"
puts "Code: #{result.metadata[:code]}"
end
# Bang variant raises on failure
result = OnboardCustomer.execute!(email: "jane@co.com")
# => raises CMDx::Fault on failure
# Plug into any observability stack
CMDx.configure do |c|
c.telemetry.subscribe(:task_executed) do |event|
Datadog.increment("cmdx.executed", tags: [
"task:#{event.task_class}",
"status:#{event.payload[:result].status}"
])
end
end
# Every event ships with a cid, task_id,
# runtime, origin, and correlation metadata.
# Structured logs are emitted automatically.
OnboardCustomer.execute(email: "jane@co.com")
Everything your tasks need.
Batteries included. No external dependencies required.
Typed inputs
Declare inputs with coercion, defaults, and validation. Invalid data fails before work runs, never mid-method.
Automatic retries
Configurable retries with exponential backoff, scoped to the exception classes you choose. Declared per task with retry_on.
Workflows
Chain tasks into sequential or parallel pipelines. Conditional steps, shared context, halt-aware error propagation.
Structured logging
Each execution emits a chain id, task, state, status, runtime, and metadata as a structured key-value line.
Middleware & callbacks
Wrap tasks with auth, caching, timing, and other cross-cutting concerns. Hook lifecycle events with on_success, on_skipped, and on_failed.
I18n messages
Translatable error messages built on the standard I18n backend. English ships with the gem; bring your own locale files for everything else.
Built for real workloads.
From core business logic to external integrations.
Payment processing
Charges, refunds, ledgers, and reconciliation with audit-friendly result objects and automatic retries on gateway errors.
User onboarding
Orchestrate signup → billing → welcome emails with parallel fan-out and conditional steps for paid tiers.
Webhook handlers
Validate, dispatch, and retry Stripe, GitHub, or Twilio events with one task per event type and typed payloads.
Background jobs
Give Sidekiq or async-job workers structured inputs, predictable results, and retry-aware error handling.
ETL & data sync
Compose import → transform → load pipelines with per-step checkpoints and parallel stages for throughput.
AI agent flows
Wrap LLM tool calls, embeddings, and vector writes with timeouts, retries, and telemetry you can observe.