Skip to content

Basics - Context

Context is the shared data bag passed through a task's execution. It holds inputs, intermediate values, and anything the task writes back for downstream consumers.

Assigning Data

The hash (or Context / Task / Result) handed to execute is normalized into a Context. String keys are symbolized; nested values are not.

CalculateShipping.execute(weight: 2.5, destination: "CA")

Note

Context.build passes an existing un-frozen Context through unchanged, unwraps anything that responds to #context (Task, Result), and wraps hash-likes in a fresh Context.

Accessing and Modifying

Read with method, hash, or safe accessors; mutate freely inside work (the root context is frozen only after Runtime teardown). ctx is a shorthand alias for 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 return nil for unknown keys. Context includes Enumerable and exposes the usual keys/values/key?/each/each_key/each_value. #merge / #deep_merge mutate in place and return self. #store (aliased []=) symbolizes the key. See YARD for the full surface.

Serialization

Context serializes cleanly for logs, telemetry payloads, and Rails render json: callers:

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

Context flows through nested executions. A sub-task invoked with execute(context) (or execute(task) / execute(result)) reuses the same underlying Context, so writes compound.

# 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

Passing a Context, Task, or Result shares context by reference — callee writes are visible to the caller. context.to_h exposes the backing hash; Context.build(context.to_h) rebuilds the top-level table but nested mutables still alias. Use context.deep_dup for full isolation.

Strict Mode

By default, reading an unknown key via the dynamic method reader returns nil. Enable strict mode to raise NoMethodError for unknown reads instead — useful for catching typos in larger tasks.

Strict mode can be set globally 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 NoMethodError: unknown context key :typoed_key (strict mode)
  end
end

Strict mode only affects the dynamic method reader. Every other access channel keeps its lenient semantics so defaults and explicit presence checks still work:

Access Behavior in strict mode
context.missing raises NoMethodError
context[:missing] returns nil
context.fetch(:missing, :default) returns :default
context.dig(:a, :b) returns nil
context.missing? returns false
context.missing = 1 writes succeed

Note

strict_context is re-applied on every Task#initialize — nested tasks flip the shared flag to their own settings.strict_context for their execution. For consistent behavior across a pipeline, set it on a base class (e.g. ApplicationTask).

Pattern Matching

Context supports both array and hash deconstruction (Ruby 3.0+).

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 backing table; a key list slices it (unknown keys are omitted). deconstruct yields [[key, value], ...] pairs for find-pattern matches (in [*, [:weight, Float], *]).