Skip to content

Basics - Chain

Chains automatically track related task executions within a thread. Think of them as execution traces that help you understand what happened and in what order.

Management

Each execution context maintains its own isolated chain. CMDx uses Fiber.storage when available (Ruby 3.2+), falling back to Thread.current on older Rubies.

Tip

Fiber-based storage means CMDx works correctly with async frameworks like Falcon and the async gem out of the box.

# Thread A
Thread.new do
  result = ImportDataset.execute(file_path: "/data/batch1.csv")
  result.chain.id    #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
end

# Thread B (completely separate chain)
Thread.new do
  result = ImportDataset.execute(file_path: "/data/batch2.csv")
  result.chain.id    #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
end

# Access current thread's chain
CMDx::Chain.current  #=> Returns current chain or nil
CMDx::Chain.clear    #=> Clears current thread's chain

Tasks automatically create or join the current thread's chain:

Important

Chain management is automatic—no manual lifecycle handling needed.

class ImportDataset < CMDx::Task
  def work
    # First task creates new chain
    result1 = ValidateHeaders.execute(file_path: context.file_path)
    result1.chain.id           #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
    result1.chain.results.size #=> 1

    # Second task joins existing chain
    result2 = SendNotification.execute(to: "admin@company.com")
    result2.chain.id == result1.chain.id  #=> true
    result2.chain.results.size            #=> 2

    # Both results reference the same chain
    result1.chain.results == result2.chain.results #=> true
  end
end

Inheritance

Subtasks automatically inherit the current thread's chain, building a unified execution trail:

class ImportDataset < CMDx::Task
  def work
    context.dataset = Dataset.find(context.dataset_id)

    # Subtasks automatically inherit current chain
    ValidateSchema.execute
    TransformData.execute!(context)
    SaveToDatabase.execute(dataset_id: context.dataset_id)
  end
end

result = ImportDataset.execute(dataset_id: 456)
chain = result.chain

# All tasks share the same chain
chain.results.size #=> 4 (main task + 3 subtasks)
chain.results.map { |r| r.task.class }
#=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]

Structure

Chains expose comprehensive execution information:

Important

Chain state reflects the first (outermost) task result. Subtasks maintain their own states.

result = ImportDataset.execute(dataset_id: 456)
chain = result.chain

# Chain identification (UUID v7 when available, UUID v4 otherwise)
chain.id        #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
chain.results   #=> Array of all results in execution order
chain.dry_run?  #=> true/false

# State delegation (from first/outer-most result)
chain.state     #=> "complete"
chain.status    #=> "success"
chain.outcome   #=> "success"

# Convenience methods (delegated from results array)
chain.size      #=> 4 (number of results)
chain.first     #=> First result (outermost task)
chain.last      #=> Last result (most recent subtask)

# Access individual results
chain.results.each_with_index do |result, index|
  puts "#{index}: #{result.task.class} - #{result.status}"
end

Freezing

After the outermost task completes, CMDx freezes the chain, context, and all results to enforce immutability. Subtask results are frozen as part of this top-level freeze—not individually during their own execution.