cmdx 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +42 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +399 -20
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +409 -34
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/basics/call.md CHANGED
@@ -1,31 +1,242 @@
1
1
  # Basics - Call
2
2
 
3
- Calling a task executes the logic within it. Tasks can only be executed via
4
- the `call` and `call!` class methods.
3
+ Calling a task executes the business logic within it. Tasks provide two execution methods that handle success and failure scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow.
5
4
 
6
- ## Non-bang
5
+ ## Table of Contents
7
6
 
8
- The `call` method will always return a `CMDx::Result` object after execution.
7
+ - [Execution Methods Overview](#execution-methods-overview)
8
+ - [Non-bang Call (`call`)](#non-bang-call-call)
9
+ - [Bang Call (`call!`)](#bang-call-call)
10
+ - [Direct Instantiation](#direct-instantiation)
11
+ - [Parameter Passing](#parameter-passing)
12
+ - [Result Propagation (`throw!`)](#result-propagation-throw)
13
+ - [Result Callbacks](#result-callbacks)
14
+ - [Task State Lifecycle](#task-state-lifecycle)
15
+ - [Return Value Details](#return-value-details)
16
+
17
+ ## Execution Methods Overview
18
+
19
+ | Method | Returns | Exceptions | Use Case |
20
+ |--------|---------|------------|----------|
21
+ | `call` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
22
+ | `call!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` on failure/skip | Exception-based control flow |
23
+
24
+ ## Non-bang Call (`call`)
25
+
26
+ The `call` method always returns a `CMDx::Result` object regardless of execution outcome. This is the preferred method for most use cases.
27
+
28
+ ```ruby
29
+ result = ProcessOrderTask.call(order_id: 12345)
30
+
31
+ # Check execution state
32
+ result.success? #=> true/false
33
+ result.failed? #=> true/false
34
+ result.skipped? #=> true/false
35
+
36
+ # Access result data
37
+ result.context.order_id #=> 12345
38
+ result.runtime #=> 0.05 (seconds)
39
+ result.state #=> "complete"
40
+ result.status #=> "success"
41
+ ```
42
+
43
+ ### Handling Different Outcomes
44
+
45
+ ```ruby
46
+ result = ProcessOrderTask.call(order_id: 12345)
47
+
48
+ case result.status
49
+ when "success"
50
+ puts "Order processed: #{result.context.order_id}"
51
+ when "skipped"
52
+ puts "Order skipped: #{result.metadata[:reason]}"
53
+ when "failed"
54
+ puts "Order failed: #{result.metadata[:reason]}"
55
+ end
56
+ ```
57
+
58
+ ## Bang Call (`call!`)
59
+
60
+ The bang `call!` method raises a `CMDx::Fault` exception when tasks fail or are skipped, based on the `task_halt` configuration. It returns a `CMDx::Result` object only on success. This method is useful in scenarios where you want exception-based control flow.
61
+
62
+ ```ruby
63
+ begin
64
+ result = ProcessOrderTask.call!(order_id: 12345)
65
+ puts "Order processed: #{result.context.order_id}"
66
+ rescue CMDx::Failed => e
67
+ # Handle failure
68
+ RetryOrderJob.perform_later(e.result.context.order_id)
69
+ rescue CMDx::Skipped => e
70
+ # Handle skip
71
+ Rails.logger.info("Order skipped: #{e.result.metadata[:reason]}")
72
+ end
73
+ ```
74
+
75
+ ### Exception Types
76
+
77
+ | Exception | Raised When | Purpose |
78
+ |-----------|-------------|---------|
79
+ | `CMDx::Failed` | Task execution fails | Handle failure scenarios |
80
+ | `CMDx::Skipped` | Task execution is skipped | Handle skip scenarios |
81
+
82
+ ## Direct Instantiation
83
+
84
+ Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
85
+
86
+ ```ruby
87
+ # Direct instantiation
88
+ task = ProcessOrderTask.new(order_id: 12345, notify_customer: true)
89
+
90
+ # Access properties before execution
91
+ task.id #=> "abc123..." (unique task ID)
92
+ task.context.order_id #=> 12345
93
+ task.context.notify_customer #=> true
94
+ task.result.state #=> "initialized"
95
+
96
+ # Manual execution
97
+ task.perform
98
+ task.result.success? #=> true/false
99
+ ```
100
+
101
+ ### Execution Approaches
102
+
103
+ | Approach | Use Case | Benefits |
104
+ |----------|----------|----------|
105
+ | `TaskClass.call(...)` | Standard execution | Simple, handles full lifecycle |
106
+ | `TaskClass.call!(...)` | Exception-based flow | Automatic fault raising |
107
+ | `TaskClass.new(...).perform` | Advanced scenarios | Full control, testing flexibility |
108
+
109
+ > [!NOTE]
110
+ > Direct instantiation gives you access to the task instance before and after execution, but you must call the execution method manually.
111
+
112
+ ## Parameter Passing
113
+
114
+ All methods accept parameters that become available in the task context:
9
115
 
10
116
  ```ruby
11
- ProcessOrderTask.call #=> <CMDx::Result ...>
117
+ # Direct parameters
118
+ result = ProcessOrderTask.call(
119
+ order_id: 12345,
120
+ notify_customer: true,
121
+ priority: "high"
122
+ )
123
+
124
+ # From another task result
125
+ validation_result = ValidateOrderTask.call(order_id: 12345)
126
+
127
+ # -- Passing Result object
128
+ result = ProcessOrderTask.call!(validation_result)
129
+
130
+ # -- Passing context directly
131
+ result = ProcessOrderTask.new(validation_result.context)
12
132
  ```
13
133
 
14
- ## Bang
134
+ ## Result Propagation (`throw!`)
15
135
 
16
- The bang `call!` method raises a `CMDx::Fault` based exception depending on the defined
17
- `task_halt` status options, otherwise it will return a `CMDx::Result` object. This
18
- form of call is useful in background jobs where retries are done via the exception mechanism.
136
+ The `throw!` method enables result propagation, allowing tasks to bubble up failures from subtasks while preserving the original fault information:
19
137
 
20
138
  ```ruby
21
- ProcessOrderTask.call! #=> raises CMDx::Failed
139
+ class ProcessOrderTask < CMDx::Task
140
+ def call
141
+ validation_result = ValidateOrderTask.call(context)
142
+ throw!(validation_result) if validation_result.failed?
143
+
144
+ payment_result = ProcessPaymentTask.call(context)
145
+ throw!(payment_result) if payment_result.skipped?
146
+
147
+ delivery_result = ScheduleDeliveryTask.call(context)
148
+ throw!(delivery_result) # failed or skipped
149
+
150
+ # Continue with main logic
151
+ context.order = Order.find(context.order_id)
152
+ finalize_order_processing
153
+ end
154
+ end
155
+ ```
156
+
157
+ ## Result Callbacks
158
+
159
+ Results support fluent callback patterns for conditional logic:
160
+
161
+ ```ruby
162
+ ProcessOrderTask
163
+ .call(order_id: 12345)
164
+ .on_success { |result|
165
+ SendOrderConfirmationTask.call(result.context)
166
+ }
167
+ .on_failed { |result|
168
+ Honeybadger.notify(result.metadata[:error])
169
+ }
170
+ .on_executed { |result|
171
+ StatsD.timing('order.processing_time', result.runtime)
172
+ }
173
+ ```
174
+
175
+ ### Available Callbacks
176
+
177
+ ```ruby
178
+ result = ProcessOrderTask.call(order_id: 12345)
179
+
180
+ # State-based callbacks
181
+ result
182
+ .on_complete { |r| cleanup_resources(r) }
183
+ .on_interrupted { |r| handle_interruption(r) }
184
+ .on_executed { |r| log_execution_time(r) }
185
+
186
+ # Status-based callbacks
187
+ result
188
+ .on_success { |r| handle_success(r) }
189
+ .on_skipped { |r| handle_skip(r) }
190
+ .on_failed { |r| handle_failure(r) }
191
+
192
+ # Outcome-based callbacks
193
+ result
194
+ .on_good { |r| log_positive_outcome(r) }
195
+ .on_bad { |r| log_negative_outcome(r) }
196
+ ```
197
+
198
+ ## Task State Lifecycle
199
+
200
+ Tasks progress through defined states during execution:
201
+
202
+ ```ruby
203
+ result = ProcessOrderTask.call(order_id: 12345)
204
+
205
+ # Execution states
206
+ result.state #=> "initialized" -> "executing" -> "complete"/"interrupted"
207
+
208
+ # Outcome statuses
209
+ result.status #=> "success"/"failed"/"skipped"
210
+ ```
211
+
212
+ ## Return Value Details
213
+
214
+ The `Result` object provides comprehensive execution information:
215
+
216
+ ```ruby
217
+ result = ProcessOrderTask.call(order_id: 12345)
218
+
219
+ # Execution metadata
220
+ result.id #=> "abc123..." (unique execution ID)
221
+ result.runtime #=> 0.05 (execution time in seconds)
222
+ result.task #=> ProcessOrderTask instance
223
+ result.chain #=> Chain object for tracking executions
224
+
225
+ # Context and metadata
226
+ result.context #=> Context with all task data
227
+ result.metadata #=> Hash with execution metadata
228
+
229
+ # State checking methods
230
+ result.good? #=> true for success/skipped
231
+ result.bad? #=> true for failed/skipped
232
+ result.complete? #=> true when execution finished
233
+ result.interrupted? #=> true for failed/skipped
22
234
  ```
23
235
 
24
236
  > [!IMPORTANT]
25
- > Tasks are single use objects, once they have been called they are frozen and cannot be called again
26
- > as result object will be returned. Build a new task call to execute a new instance of the same task.
237
+ > Tasks are single-use objects. Once executed, they are frozen and cannot be called again. Create a new task instance to execute the same task again.
27
238
 
28
239
  ---
29
240
 
30
- - **Prev:** [Basics - Setup](https://github.com/drexed/cmdx/blob/main/docs/basics/setup.md)
31
- - **Next:** [Basics - Context](https://github.com/drexed/cmdx/blob/main/docs/basics/context.md)
241
+ - **Prev:** [Basics - Setup](setup.md)
242
+ - **Next:** [Basics - Context](context.md)
@@ -0,0 +1,271 @@
1
+ # Basics - Chain
2
+
3
+ A chain represents a collection of related task executions that share a common execution context. Chains provide unified tracking, indexing, and reporting for task workflows using thread-local storage to automatically group related tasks without manual coordination.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Thread-Local Chain Management](#thread-local-chain-management)
8
+ - [Automatic Chain Creation](#automatic-chain-creation)
9
+ - [Chain Inheritance](#chain-inheritance)
10
+ - [Chain Structure and Metadata](#chain-structure-and-metadata)
11
+ - [Correlation ID Integration](#correlation-id-integration)
12
+ - [State Delegation](#state-delegation)
13
+ - [Serialization and Logging](#serialization-and-logging)
14
+
15
+ ## Thread-Local Chain Management
16
+
17
+ Chains use thread-local storage to automatically group related task executions within the same thread while maintaining isolation across different threads:
18
+
19
+ ```ruby
20
+ # Each thread gets its own chain context
21
+ Thread.new do
22
+ result = ProcessOrderTask.call(order_id: 123)
23
+ result.chain.id # => unique ID for this thread
24
+ end
25
+
26
+ Thread.new do
27
+ result = ProcessOrderTask.call(order_id: 456)
28
+ result.chain.id # => different unique ID
29
+ end
30
+
31
+ # Access the current thread's chain
32
+ CMDx::Chain.current # => current chain or nil
33
+ CMDx::Chain.clear # => clears current thread's chain
34
+ ```
35
+
36
+ ## Automatic Chain Creation
37
+
38
+ Every task execution automatically creates or joins a thread-local chain context:
39
+
40
+ ```ruby
41
+ # Single task creates its own chain
42
+ result = ProcessUserOrderTask.call(order_id: 123)
43
+ result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
44
+ result.chain.results.size #=> 1
45
+
46
+ # Subsequent tasks in the same thread join the existing chain
47
+ result2 = AnotherTask.call(data: "test")
48
+ result2.chain.id == result.chain.id #=> true
49
+ result2.chain.results.size #=> 2
50
+ ```
51
+
52
+ ## Chain Inheritance
53
+
54
+ When tasks call other tasks within the same thread, they automatically inherit the current chain, creating a cohesive execution trail:
55
+
56
+ ```ruby
57
+ class ProcessUserOrderTask < CMDx::Task
58
+ def call
59
+ context.order = Order.find(order_id)
60
+
61
+ # Subtasks automatically inherit the current thread's chain
62
+ SendOrderConfirmationTask.call(order_id: order_id)
63
+ NotifyWarehousePartnersTask.call(order_id: order_id)
64
+ end
65
+ end
66
+
67
+ result = ProcessUserOrderTask.call(order_id: 123)
68
+ chain = result.chain
69
+
70
+ # All related tasks share the same chain automatically
71
+ chain.results.size #=> 3
72
+ chain.results.map(&:task).map(&:class)
73
+ #=> [ProcessUserOrderTask, SendOrderConfirmationTask, NotifyWarehousePartnersTask]
74
+ ```
75
+
76
+ > [!NOTE]
77
+ > Tasks automatically inherit the current thread's chain, creating a unified execution trail for debugging and monitoring purposes without any manual chain management.
78
+
79
+ ## Chain Structure and Metadata
80
+
81
+ Chains provide comprehensive execution information:
82
+
83
+ ```ruby
84
+ result = ProcessUserOrderTask.call(order_id: 123)
85
+ chain = result.chain
86
+
87
+ # Chain identification
88
+ chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
89
+ chain.results #=> [<CMDx::Result ...>, <CMDx::Result ...>]
90
+
91
+ # Execution state (delegates to outer most result)
92
+ chain.state #=> "complete"
93
+ chain.status #=> "success"
94
+ chain.outcome #=> "success"
95
+ chain.runtime #=> 0.5
96
+ ```
97
+
98
+ ## Correlation ID Integration
99
+
100
+ Chains automatically integrate with the correlation tracking system through thread-local storage, providing seamless request tracing across task boundaries. The chain ID serves as the correlation identifier, enabling you to trace execution flows through distributed systems and complex business logic.
101
+
102
+ ### Custom Chain IDs
103
+
104
+ You can specify custom chain IDs for specific correlation contexts:
105
+
106
+ ```ruby
107
+ # Create a chain with custom ID
108
+ chain = CMDx::Chain.new(id: "user-session-123")
109
+ CMDx::Chain.current = chain
110
+
111
+ result = ProcessUserOrderTask.call(order_id: 123)
112
+ result.chain.id #=> "user-session-123"
113
+ ```
114
+
115
+ ### Automatic Correlation Inheritance
116
+
117
+ Chains inherit correlation IDs using a hierarchical precedence system:
118
+
119
+ ```ruby
120
+ # 1. Existing chain ID takes precedence
121
+ CMDx::Chain.current = CMDx::Chain.new(id: "custom-correlation-123")
122
+ result = ProcessUserOrderTask.call(order_id: 123)
123
+ result.chain.id #=> "custom-correlation-123"
124
+
125
+ # 2. Thread-local correlation ID is used if no chain exists
126
+ CMDx::Chain.clear
127
+ CMDx::Correlator.id = "thread-correlation-456"
128
+ result = ProcessUserOrderTask.call
129
+ result.chain.id #=> "thread-correlation-456"
130
+
131
+ # 3. Generated UUID when no correlation exists
132
+ CMDx::Correlator.clear
133
+ result = ProcessUserOrderTask.call
134
+ result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5" (generated)
135
+ ```
136
+
137
+ ### Cross-Task Correlation Propagation
138
+
139
+ When tasks call subtasks within the same thread, correlation IDs automatically propagate:
140
+
141
+ ```ruby
142
+ class ProcessUserOrderTask < CMDx::Task
143
+ def call
144
+ context.order = Order.find(order_id)
145
+
146
+ # Subtasks inherit the same correlation ID automatically
147
+ SendOrderConfirmationTask.call(order_id: order_id)
148
+ NotifyWarehousePartnersTask.call(order_id: order_id)
149
+ end
150
+ end
151
+
152
+ # Set correlation for this execution context
153
+ CMDx::Chain.current = CMDx::Chain.new(id: "user-order-correlation-123")
154
+
155
+ result = ProcessUserOrderTask.call(order_id: 456)
156
+ chain = result.chain
157
+
158
+ # All tasks share the same correlation ID
159
+ chain.id #=> "user-order-correlation-123"
160
+ chain.results.all? { |r| r.chain.id == "user-order-correlation-123" } #=> true
161
+ ```
162
+
163
+ ### Correlation Context Management
164
+
165
+ Use correlation blocks to manage correlation scope:
166
+
167
+ ```ruby
168
+ # Correlation applies only within the block
169
+ CMDx::Correlator.use("api-request-789") do
170
+ result = ProcessApiRequestTask.call(request_data: data)
171
+ result.chain.id #=> "api-request-789"
172
+
173
+ # Nested task calls inherit the same correlation
174
+ AuditLogTask.call(audit_data: data)
175
+ end
176
+
177
+ # Outside the block, correlation context is restored
178
+ result = AnotherTask.call
179
+ result.chain.id #=> different correlation ID
180
+ ```
181
+
182
+ ### Middleware Integration
183
+
184
+ The `CMDx::Middlewares::Correlate` middleware automatically manages correlation contexts during task execution:
185
+
186
+ ```ruby
187
+ class ProcessOrderTask < CMDx::Task
188
+ # Apply correlate middleware globally or per-task
189
+ use CMDx::Middlewares::Correlate
190
+
191
+ def call
192
+ # Correlation is automatically managed
193
+ # Chain ID reflects the established correlation context
194
+ end
195
+ end
196
+ ```
197
+
198
+ > [!TIP]
199
+ > Chain IDs serve as correlation identifiers, making it easy to trace related operations across your application. The thread-local storage ensures automatic correlation without manual chain management.
200
+
201
+ > [!NOTE]
202
+ > Correlation IDs are particularly useful for debugging distributed systems, API request tracing, and understanding complex business workflows. All logs and results automatically include the chain ID for correlation.
203
+
204
+ ## State Delegation
205
+
206
+ Chain state information delegates to the first (outer most) result, representing the overall execution outcome:
207
+
208
+ ```ruby
209
+ class ProcessOrderTask < CMDx::Task
210
+ def call
211
+ ValidateOrderDataTask.call!(order_id: order_id) # Success
212
+ ProcessOrderPaymentTask.call!(order_id: order_id) # Failed
213
+ end
214
+ end
215
+
216
+ result = ProcessOrderTask.call
217
+ chain = result.chain
218
+
219
+ # Chain status reflects the main task, not subtasks
220
+ chain.status #=> "failed" (ProcessOrderPaymentTask failed)
221
+ chain.state #=> "interrupted"
222
+
223
+ # Individual task results maintain their own state
224
+ chain.results[0].status #=> "failed" (ProcessOrderTask)
225
+ chain.results[1].status #=> "success" (ValidateOrderDataTask)
226
+ chain.results[2].status #=> "failed" (ProcessOrderPaymentTask)
227
+ ```
228
+
229
+ > [!IMPORTANT]
230
+ > Chain state always reflects the first (outer most) task outcome, not the subtasks. Individual subtask results maintain their own success/failure states.
231
+
232
+ ## Serialization and Logging
233
+
234
+ Chains provide comprehensive serialization capabilities for monitoring and debugging:
235
+
236
+ ```ruby
237
+ result = ProcessUserOrderTask.call(order_id: 123)
238
+ chain = result.chain
239
+
240
+ # Hash representation with all execution data
241
+ chain.to_h
242
+ #=> {
243
+ # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
244
+ # state: "complete",
245
+ # status: "success",
246
+ # outcome: "success",
247
+ # runtime: 0.5,
248
+ # results: [
249
+ # { class: "ProcessUserOrderTask", state: "complete", status: "success", ... },
250
+ # { class: "SendOrderConfirmationTask", state: "complete", status: "success", ... },
251
+ # { class: "NotifyWarehousePartnersTask", state: "complete", status: "success", ... }
252
+ # ]
253
+ # }
254
+
255
+ # Human-readable summary
256
+ puts chain.to_s
257
+ # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
258
+ # ================================================
259
+ #
260
+ # ProcessUserOrderTask: index=0 state=complete status=success ...
261
+ # SendOrderConfirmationTask: index=1 state=complete status=success ...
262
+ # NotifyWarehousePartnersTask: index=2 state=complete status=success ...
263
+ #
264
+ # ================================================
265
+ # state: complete | status: success | outcome: success | runtime: 0.5
266
+ ```
267
+
268
+ ---
269
+
270
+ - **Prev:** [Basics - Context](context.md)
271
+ - **Next:** [Interruptions - Halt](../interruptions/halt.md)