v2.0 · Signal

The runtime,
rewritten.

Signals in. Immutable results out.
A business-logic framework powered by an explicit signal-based runtime.

$ gem install cmdx
· 01 · the problem

The code you keep rewriting.

Every Rails app has a 300-line controller like this. CMDx gives it a home.

today, in every rails app
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
with cmdx
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
your controller, now
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
· 02 · the pipeline

CERO — the four-step flow.

A single, repeatable shape behind every task.

01 · C

Compose

Define typed inputs and the business rules they enforce.

required :id, coerce: :integer
02 · E

Execute

Run tasks anywhere — controllers, jobs, scripts, tests.

Task.execute(id: 42)
03 · R

React

Branch on success, skip, or fail — no rescues for control flow.

result.success?
result.skipped?
result.failed?
04 · O

Observe

Every run ships structured logs and telemetry.

configuration.telemetry.subscribe(...)
· 03 · see it in action

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")
· 04 · features

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.

· 05 · built for

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.

stripebraintreeledgers

User onboarding

Orchestrate signup → billing → welcome emails with parallel fan-out and conditional steps for paid tiers.

signupbillingwelcome

Webhook handlers

Validate, dispatch, and retry Stripe, GitHub, or Twilio events with one task per event type and typed payloads.

stripegithubtwilio

Background jobs

Give Sidekiq or async-job workers structured inputs, predictable results, and retry-aware error handling.

sidekiqasync-jobsolid_queue

ETL & data sync

Compose import → transform → load pipelines with per-step checkpoints and parallel stages for throughput.

extracttransformload

AI agent flows

Wrap LLM tool calls, embeddings, and vector writes with timeouts, retries, and telemetry you can observe.

openaitoolsvectors