cmdx 0.5.0 → 1.0.1

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