Basics - Context¶
Think of Context as the shared notepad for a task run. It carries what came in (inputs), what you figured out along the way, and anything the next step might need. One bag, many readers and writers — all in one execution.
Assigning Data¶
Whatever you pass to execute — a Hash, another Context, a Task, or a Result — gets normalized into a Context. String keys become symbols; nested values stay as-is (no deep magic).
Note
Context.build is the traffic cop: an unfrozen Context passes through unchanged; things with #context (like Task or Result) unwrap; hash-likes become a new Context.
Accessing and Modifying¶
You can read like an object, like a hash, or with safe helpers. While work is running, mutate away — the root context only freezes after teardown. Short alias: ctx means context.
class CalculateShipping < CMDx::Task
def work
# Reads
weight = context.weight # method style (nil for unknown keys)
service_type = context[:service_type] # hash style
rush_delivery = context.fetch(:rush, false) # safe default
carrier = context.dig(:options, :carrier)
attempt = context.retrieve(:attempt_count, 0) # fetch-or-set
context.weight? # truthy predicate
context.empty? # false when any key stored
context.size # number of top-level keys
# Writes
context.calculated_at = Time.now
context[:status] = "calculating" # alias: context.store(:status, "...")
context.insurance_included ||= false
context.merge(shipping_cost: calculate_cost) # top-level last-write-wins (mutates in place)
context.deep_merge(options: { carrier: "ups" }) # recurses into Hash values
context.delete(:credit_card_token)
context.clear # wipes every entry
end
end
Note
Method-style reads give nil for keys that don’t exist — no exception. Context is Enumerable and has the usual keys / values / key? / each friends. #merge and #deep_merge change the context in place and return self. #store (same as []=) symbolizes the key. For every method, peek at the YARD docs.
Serialization¶
Need to log it or return JSON? Context plays nice:
context.to_h #=> { weight: 2.5, destination: "CA" } (the backing table, not a copy)
context.as_json #=> same as to_h (aliased for Rails/ActiveSupport callers)
context.to_json #=> '{"weight":2.5,"destination":"CA"}' (Symbol keys are emitted as strings)
context.to_s #=> 'weight=2.5 destination="CA"' (space-separated key=value.inspect)
Sharing Between Tasks¶
When a task calls another task with the same context object (or something that unwraps to it), you’re appending to the same notepad. Writes stack up — handy for pipelines.
# During execution
class CalculateShipping < CMDx::Task
def work
# Validate shipping data
validation_result = ValidateAddress.execute(context)
# Via context
CalculateInsurance.execute(context)
# Via result
NotifyShippingCalculated.execute(validation_result)
# Context now contains accumulated data from all tasks
context.address_validated #=> true (from validation)
context.insurance_calculated #=> true (from insurance)
context.notification_sent #=> true (from notification)
end
end
# After execution
result = CalculateShipping.execute(destination: "New York, NY")
CreateShippingLabel.execute(result)
Important
Sharing a Context, Task, or Result means by reference — the callee’s writes show up for the caller. context.to_h is still the live backing hash; Context.build(context.to_h) copies the top level but nested objects might still be the same object in memory. Want a full snapshot? context.deep_dup.
Strict Mode¶
By default, context.unknown_method is just nil — forgiving, sometimes too forgiving. Turn on strict mode and typos explode as CMDx::UnknownAccessorError so you catch them early.
Global or per-task:
CMDx.configure do |config|
config.strict_context = true
end
class CalculateShipping < CMDx::Task
settings(strict_context: true)
def work
context.weight #=> reads fine when set
context.typoed_key #=> raises CMDx::UnknownAccessorError: unknown context key :typoed_key (strict mode)
end
end
Strict mode only changes the dynamic method reader. Brackets, fetch, dig, predicates, and writers behave like before:
| How you access | In strict mode |
|---|---|
context.missing |
Raises CMDx::UnknownAccessorError |
context[:missing] |
Still nil |
context.fetch(:missing, :default) |
Still :default |
context.dig(:a, :b) |
Still nil |
context.missing? |
Still false |
context.missing = 1 |
Still works — assignment always allowed |
Note
Strictness is reapplied on each Task#initialize. Nested tasks can flip the shared flag to their settings.strict_context while they run. For a whole app that should feel the same, set it once on a base class like ApplicationTask.
Pattern Matching¶
Ruby 3+ can destructure Context like a hash or an array of pairs — nice for case expressions.
result = CalculateShipping.execute(weight: 2.5, destination: "CA", options: { carrier: "ups" })
case result.context
in { destination: "CA", weight: Float => kg } if kg > 1.0
bulk_ship(kg)
in { options: { carrier: String => code } }
track_with(code)
end
deconstruct_keys(nil) returns the full table; pass a key list and you get a slice (unknown keys omitted). deconstruct yields [[key, value], ...] for find-style patterns like in [*, [:weight, Float], *].