Getting Started¶
Note
These docs follow main. If you are on an older gem version, open the docs/ folder from that release’s tag so examples match what you run.
Welcome. CMDx is a small Ruby framework that helps you write business logic as clear, reusable tasks—think “one job per class,” with inputs checked for you, errors handled in a predictable way, and logs that tell a story when things go wrong.
If you have ever opened a “service object” file and wondered what actually runs, in what order, and what happens on error, CMDx is meant to calm that chaos.
Sound familiar?
- Every team invents its own “service” pattern, so nothing feels the same from file to file.
- When something breaks, it is hard to follow the path the code took.
- Error handling is inconsistent, so you stop trusting the happy path.
What CMDx gives you instead:
- One clear contract: how you declare inputs, run work, and read the outcome.
- Built-in flow control (success, skip, fail) so branches are explicit, not hidden.
- Workflows that chain tasks without spaghetti.
- Structured logging so you can see what ran, how long it took, and why it stopped.
- Input validation and type coercion so bad data fails fast with useful messages.
Requirements¶
- Ruby: MRI 3.3+ (or a recent JRuby / TruffleRuby that matches)
- Gems:
bigdecimalandlogger(stdlib gems on most setups)
You do not need Rails. If you use Rails, there is an optional hook (CMDx::Railtie) so integration is a choice, not a requirement.
Installation¶
Pick one:
Configuration¶
Rails: generate an initializer so you have one place for global settings:
That drops config/initializers/cmdx.rb into your app.
Not on Rails: copy the same template by hand from the repo: install.rb template.
Quick Start¶
Below is a tiny task you can paste into irb or a scratch Ruby file—no framework required. It says hello when the name is present, and complains politely when it is not.
require "cmdx"
class Greet < CMDx::Task
required :name, coerce: :string, presence: true
def work
context.greeting = "Hello, #{name}!"
end
end
result = Greet.execute(name: "World")
result.success? #=> true
result.context.greeting #=> "Hello, World!"
result = Greet.execute(name: "")
result.failed? #=> true
result.reason #=> "name cannot be empty"
result.errors.to_h #=> { name: ["cannot be empty"] }
For how tasks fit together in your head—CERO and the full execution pipeline—read Fundamentals.
When you are ready to go deeper, this map points you at the right doc:
| You want… | Read… | Quick idea |
|---|---|---|
| Mental model + lifecycle | Fundamentals | CERO; task lifecycle diagram |
| Global defaults | Configuration | CMDx.configure { ... } |
| One class, one job | Setup | class Foo < CMDx::Task |
required / optional inputs |
Definitions | required :id, coerce: :integer |
| Safer input types | Coercions | coerce: :integer |
| Rules on values | Validations | numeric: { min: 1 } |
| Values when omitted | Defaults | optional :tier, default: "free" |
Data in and out of work |
Context | context.greeting = ... |
.execute vs .execute! |
Execution | Result object vs raise |
| What happened? | Result | success?, reason, errors |
| Stop early on purpose | Signals | skip!, fail! |
| Failures as exceptions | Faults | Task.execute! |
| Declared outputs | Outputs | output :user, :token |
| Several tasks in a row | Workflows | include CMDx::Workflow |
| Who called whom | Chain | cid / tid in logs |
| Cross-cutting stuff (timing, auth, etc.) | Middlewares | register :middleware |
| Hooks around run | Callbacks | on_success, before_execution |
| Automatic retries | Retries | retry_on Net::OpenTimeout, limit: 3 |
| Cap how often a task runs | Rate limit | rate_limit :email, max: 5, per: 60 |
| At-most-once side effects | Idempotency | idempotent_by :payment_id |
| One worker at a time | Locks | lock_with :account_id |
| Protect flaky dependencies | Circuit breaker | circuit_breaker threshold: 3 |
| Redis / memory backing | Stores | config.store = Stores::Redis.new(...) |
| What got logged | Logging | Built-in structured lines |
| RSpec patterns | Testing | be_success, be_failed matchers |
| Translated error messages | Internationalization | Locale YAML under cmdx |
Task generator¶
Rails ships generators so you are not copy-pasting boilerplate:
# Task
rails generate cmdx:task ModerateBlogPost
# Workflow
rails generate cmdx:workflow ProcessNotifications
# Namespaced
rails generate cmdx:task Admin::AuditUser
# => Creates app/tasks/admin/audit_user.rb
You get a starter file like:
# app/tasks/moderate_blog_post.rb
class ModerateBlogPost < CMDx::Task
def work
# Your logic here...
end
end
If you define ApplicationTask, new files inherit from it; otherwise they inherit CMDx::Task. Handy for shared retries, logging, or request context:
# app/tasks/application_task.rb
class ApplicationTask < CMDx::Task
retry_on Net::OpenTimeout, Net::ReadTimeout, limit: 3, jitter: :exponential
before_execution :set_request_context
private
def set_request_context
context.request_id ||= SecureRandom.uuid
end
end
Tip
Name tasks like verb + thing: ModerateBlogPost, ScheduleAppointment, ValidateDocument. Your future self will thank you in grep.
Documentation and editor help¶
Public APIs in the gem are documented with YARD. Together with the DSL (required, optional, output, coerce:, validate:, on_success, …) you get:
- Readable task definitions — the top of a class lists its contract at a glance.
- Editor hints — Solargraph, RubyMine, and friends can show docs inline.
- A generated book — run
bundle exec yard doclocally or browse the published API docs.
If something in this guide felt fast, read Fundamentals next, then pick a link from the table above—you are already on the right track.