cmdx 1.1.2 → 1.5.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +4 -1
  4. data/.cursor/prompts/llms.md +20 -0
  5. data/.cursor/prompts/rspec.md +4 -1
  6. data/.cursor/prompts/yardoc.md +3 -2
  7. data/.cursor/rules/cursor-instructions.mdc +56 -1
  8. data/.irbrc +6 -0
  9. data/.rubocop.yml +29 -18
  10. data/CHANGELOG.md +5 -133
  11. data/LLM.md +3317 -0
  12. data/README.md +68 -44
  13. data/docs/attributes/coercions.md +162 -0
  14. data/docs/attributes/defaults.md +90 -0
  15. data/docs/attributes/definitions.md +281 -0
  16. data/docs/attributes/naming.md +78 -0
  17. data/docs/attributes/validations.md +309 -0
  18. data/docs/basics/chain.md +56 -249
  19. data/docs/basics/context.md +56 -289
  20. data/docs/basics/execution.md +114 -0
  21. data/docs/basics/setup.md +37 -334
  22. data/docs/callbacks.md +89 -467
  23. data/docs/deprecation.md +91 -174
  24. data/docs/getting_started.md +212 -202
  25. data/docs/internationalization.md +11 -647
  26. data/docs/interruptions/exceptions.md +23 -198
  27. data/docs/interruptions/faults.md +71 -151
  28. data/docs/interruptions/halt.md +109 -186
  29. data/docs/logging.md +44 -256
  30. data/docs/middlewares.md +113 -426
  31. data/docs/outcomes/result.md +81 -228
  32. data/docs/outcomes/states.md +33 -221
  33. data/docs/outcomes/statuses.md +21 -311
  34. data/docs/tips_and_tricks.md +120 -70
  35. data/docs/workflows.md +99 -283
  36. data/lib/cmdx/.DS_Store +0 -0
  37. data/lib/cmdx/attribute.rb +229 -0
  38. data/lib/cmdx/attribute_registry.rb +94 -0
  39. data/lib/cmdx/attribute_value.rb +193 -0
  40. data/lib/cmdx/callback_registry.rb +69 -77
  41. data/lib/cmdx/chain.rb +56 -73
  42. data/lib/cmdx/coercion_registry.rb +52 -68
  43. data/lib/cmdx/coercions/array.rb +19 -18
  44. data/lib/cmdx/coercions/big_decimal.rb +20 -24
  45. data/lib/cmdx/coercions/boolean.rb +26 -25
  46. data/lib/cmdx/coercions/complex.rb +21 -22
  47. data/lib/cmdx/coercions/date.rb +25 -23
  48. data/lib/cmdx/coercions/date_time.rb +24 -25
  49. data/lib/cmdx/coercions/float.rb +25 -22
  50. data/lib/cmdx/coercions/hash.rb +31 -32
  51. data/lib/cmdx/coercions/integer.rb +30 -24
  52. data/lib/cmdx/coercions/rational.rb +29 -24
  53. data/lib/cmdx/coercions/string.rb +19 -22
  54. data/lib/cmdx/coercions/symbol.rb +37 -0
  55. data/lib/cmdx/coercions/time.rb +26 -25
  56. data/lib/cmdx/configuration.rb +49 -108
  57. data/lib/cmdx/context.rb +222 -44
  58. data/lib/cmdx/deprecator.rb +61 -0
  59. data/lib/cmdx/errors.rb +42 -252
  60. data/lib/cmdx/exceptions.rb +39 -0
  61. data/lib/cmdx/faults.rb +78 -39
  62. data/lib/cmdx/freezer.rb +51 -0
  63. data/lib/cmdx/identifier.rb +30 -0
  64. data/lib/cmdx/locale.rb +52 -0
  65. data/lib/cmdx/log_formatters/json.rb +21 -22
  66. data/lib/cmdx/log_formatters/key_value.rb +20 -22
  67. data/lib/cmdx/log_formatters/line.rb +15 -22
  68. data/lib/cmdx/log_formatters/logstash.rb +22 -23
  69. data/lib/cmdx/log_formatters/raw.rb +16 -22
  70. data/lib/cmdx/middleware_registry.rb +70 -74
  71. data/lib/cmdx/middlewares/correlate.rb +90 -54
  72. data/lib/cmdx/middlewares/runtime.rb +58 -0
  73. data/lib/cmdx/middlewares/timeout.rb +48 -68
  74. data/lib/cmdx/railtie.rb +12 -45
  75. data/lib/cmdx/result.rb +229 -314
  76. data/lib/cmdx/task.rb +194 -366
  77. data/lib/cmdx/utils/call.rb +49 -0
  78. data/lib/cmdx/utils/condition.rb +71 -0
  79. data/lib/cmdx/utils/format.rb +61 -0
  80. data/lib/cmdx/validator_registry.rb +63 -72
  81. data/lib/cmdx/validators/exclusion.rb +38 -67
  82. data/lib/cmdx/validators/format.rb +48 -49
  83. data/lib/cmdx/validators/inclusion.rb +43 -74
  84. data/lib/cmdx/validators/length.rb +91 -154
  85. data/lib/cmdx/validators/numeric.rb +87 -162
  86. data/lib/cmdx/validators/presence.rb +37 -50
  87. data/lib/cmdx/version.rb +1 -1
  88. data/lib/cmdx/worker.rb +178 -0
  89. data/lib/cmdx/workflow.rb +85 -81
  90. data/lib/cmdx.rb +19 -13
  91. data/lib/generators/cmdx/install_generator.rb +14 -13
  92. data/lib/generators/cmdx/task_generator.rb +25 -50
  93. data/lib/generators/cmdx/templates/install.rb +11 -46
  94. data/lib/generators/cmdx/templates/task.rb.tt +3 -2
  95. data/lib/locales/en.yml +18 -4
  96. data/src/cmdx-logo.png +0 -0
  97. metadata +32 -116
  98. data/docs/ai_prompts.md +0 -393
  99. data/docs/basics/call.md +0 -317
  100. data/docs/configuration.md +0 -344
  101. data/docs/parameters/coercions.md +0 -396
  102. data/docs/parameters/defaults.md +0 -335
  103. data/docs/parameters/definitions.md +0 -446
  104. data/docs/parameters/namespacing.md +0 -378
  105. data/docs/parameters/validations.md +0 -405
  106. data/docs/testing.md +0 -553
  107. data/lib/cmdx/callback.rb +0 -53
  108. data/lib/cmdx/chain_inspector.rb +0 -56
  109. data/lib/cmdx/chain_serializer.rb +0 -63
  110. data/lib/cmdx/coercion.rb +0 -57
  111. data/lib/cmdx/coercions/virtual.rb +0 -29
  112. data/lib/cmdx/core_ext/hash.rb +0 -83
  113. data/lib/cmdx/core_ext/module.rb +0 -98
  114. data/lib/cmdx/core_ext/object.rb +0 -125
  115. data/lib/cmdx/correlator.rb +0 -122
  116. data/lib/cmdx/error.rb +0 -67
  117. data/lib/cmdx/fault.rb +0 -140
  118. data/lib/cmdx/immutator.rb +0 -52
  119. data/lib/cmdx/lazy_struct.rb +0 -246
  120. data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
  121. data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
  122. data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
  123. data/lib/cmdx/logger.rb +0 -49
  124. data/lib/cmdx/logger_ansi.rb +0 -68
  125. data/lib/cmdx/logger_serializer.rb +0 -116
  126. data/lib/cmdx/middleware.rb +0 -70
  127. data/lib/cmdx/parameter.rb +0 -312
  128. data/lib/cmdx/parameter_evaluator.rb +0 -231
  129. data/lib/cmdx/parameter_inspector.rb +0 -66
  130. data/lib/cmdx/parameter_registry.rb +0 -106
  131. data/lib/cmdx/parameter_serializer.rb +0 -59
  132. data/lib/cmdx/result_ansi.rb +0 -71
  133. data/lib/cmdx/result_inspector.rb +0 -71
  134. data/lib/cmdx/result_logger.rb +0 -59
  135. data/lib/cmdx/result_serializer.rb +0 -104
  136. data/lib/cmdx/rspec/matchers.rb +0 -28
  137. data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
  138. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
  139. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
  140. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
  141. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
  142. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
  143. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
  144. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
  145. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
  146. data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
  147. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
  148. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
  149. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
  150. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
  151. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
  152. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
  153. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
  154. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
  155. data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
  156. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
  157. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
  158. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
  159. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
  160. data/lib/cmdx/task_deprecator.rb +0 -58
  161. data/lib/cmdx/task_processor.rb +0 -246
  162. data/lib/cmdx/task_serializer.rb +0 -57
  163. data/lib/cmdx/utils/ansi_color.rb +0 -73
  164. data/lib/cmdx/utils/log_timestamp.rb +0 -36
  165. data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
  166. data/lib/cmdx/utils/name_affix.rb +0 -52
  167. data/lib/cmdx/validator.rb +0 -57
  168. data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
  169. data/lib/generators/cmdx/workflow_generator.rb +0 -84
  170. data/lib/locales/ar.yml +0 -35
  171. data/lib/locales/cs.yml +0 -35
  172. data/lib/locales/da.yml +0 -35
  173. data/lib/locales/de.yml +0 -35
  174. data/lib/locales/el.yml +0 -35
  175. data/lib/locales/es.yml +0 -35
  176. data/lib/locales/fi.yml +0 -35
  177. data/lib/locales/fr.yml +0 -35
  178. data/lib/locales/he.yml +0 -35
  179. data/lib/locales/hi.yml +0 -35
  180. data/lib/locales/it.yml +0 -35
  181. data/lib/locales/ja.yml +0 -35
  182. data/lib/locales/ko.yml +0 -35
  183. data/lib/locales/nl.yml +0 -35
  184. data/lib/locales/no.yml +0 -35
  185. data/lib/locales/pl.yml +0 -35
  186. data/lib/locales/pt.yml +0 -35
  187. data/lib/locales/ru.yml +0 -35
  188. data/lib/locales/sv.yml +0 -35
  189. data/lib/locales/th.yml +0 -35
  190. data/lib/locales/tr.yml +0 -35
  191. data/lib/locales/vi.yml +0 -35
  192. data/lib/locales/zh.yml +0 -35
data/LLM.md ADDED
@@ -0,0 +1,3317 @@
1
+ # CMDx Documentation
2
+
3
+ This file contains all the CMDx documentation consolidated from the docs directory.
4
+
5
+ ---
6
+
7
+ url: https://github.com/drexed/cmdx/blob/main/docs/getting_started.md
8
+ ---
9
+
10
+ # Getting Started
11
+
12
+ CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. Design robust workflows with automatic attribute validation, structured error handling, comprehensive logging, and intelligent execution flow control.
13
+
14
+ ## Installation
15
+
16
+ Add CMDx to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'cmdx'
20
+ ```
21
+
22
+ For Rails applications, generate the configuration:
23
+
24
+ ```bash
25
+ rails generate cmdx:install
26
+ ```
27
+
28
+ This creates `config/initializers/cmdx.rb` file.
29
+
30
+ ## Configuration Hierarchy
31
+
32
+ CMDx follows a two-tier configuration hierarchy:
33
+
34
+ 1. **Global Configuration**: Framework-wide defaults
35
+ 2. **Task Settings**: Class-level overrides via `settings`
36
+
37
+ > [!IMPORTANT]
38
+ > Task-level settings take precedence over global configuration. Settings are inherited from superclasses and can be overridden in subclasses.
39
+
40
+ ## Global Configuration
41
+
42
+ Global configuration settings apply to all tasks inherited from `CMDx::Task`.
43
+ Globally these settings are initialized with sensible defaults.
44
+
45
+ ### Breakpoints
46
+
47
+ Breakpoints control when `execute!` raises faults.
48
+
49
+ ```ruby
50
+ CMDx.configure do |config|
51
+ config.task_breakpoints = "skipped"
52
+ config.workflow_breakpoints = ["skipped", "failed"]
53
+ end
54
+ ```
55
+
56
+ ### Logging
57
+
58
+ ```ruby
59
+ CMDx.configure do |config|
60
+ config.logger = CustomLogger.new($stdout)
61
+ end
62
+ ```
63
+
64
+ ### Middlewares
65
+
66
+ ```ruby
67
+ CMDx.configure do |config|
68
+ # Via callable (must respond to `call(task, options)`)
69
+ config.middlewares.register CMDx::Middlewares::Timeout
70
+
71
+ # Via proc or lambda
72
+ config.middlewares.register proc { |task, options|
73
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
74
+ result = yield
75
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
76
+ Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" }
77
+ result
78
+ }
79
+
80
+ # With options
81
+ config.middlewares.register AuditTrailMiddleware, service_name: "document_processor"
82
+
83
+ # Remove middleware
84
+ config.middlewares.deregister CMDx::Middlewares::Timeout
85
+ end
86
+ ```
87
+
88
+ > [!NOTE]
89
+ > Middlewares are executed in registration order. Each middleware wraps the next, creating an execution chain around task logic.
90
+
91
+ ### Callbacks
92
+
93
+ ```ruby
94
+ CMDx.configure do |config|
95
+ # Via method
96
+ config.callbacks.register :before_execution, :initialize_user_session
97
+
98
+ # Via callable (must respond to `call(task)`)
99
+ config.callbacks.register :on_success, LogUserActivity
100
+
101
+ # Via proc or lambda
102
+ config.callbacks.register :on_complete, proc { |task|
103
+ execution_time = task.metadata[:runtime]
104
+ Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"])
105
+ }
106
+
107
+ # With options
108
+ config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task?
109
+
110
+ # Remove callback
111
+ config.callbacks.deregister :on_success, LogUserActivity
112
+ end
113
+ ```
114
+
115
+ ### Coercions
116
+
117
+ ```ruby
118
+ CMDx.configure do |config|
119
+ # Via callable (must respond to `call(value, options)`)
120
+ config.coercions.register :currency, CurrencyCoercion
121
+
122
+ # Via method (must match signature `def coordinates_coercion(value, options)`)
123
+ config.coercions.register :coordinates, :coordinates_coercion
124
+
125
+ # Via proc or lambda
126
+ config.coercions.register :tag_list, proc { |value, options|
127
+ delimiter = options[:delimiter] || ','
128
+ max_tags = options[:max_tags] || 50
129
+
130
+ tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?)
131
+ tags.first(max_tags)
132
+ }
133
+
134
+ # Remove coercion
135
+ config.coercions.deregister :currency
136
+ end
137
+ ```
138
+
139
+ ### Validators
140
+
141
+ ```ruby
142
+ CMDx.configure do |config|
143
+ # Via callable (must respond to `call(value, options)`)
144
+ config.validators.register :username, UsernameValidator
145
+
146
+ # Via method (must match signature `def url_validator(value, options)`)
147
+ config.validators.register :url, :url_validator
148
+
149
+ # Via proc or lambda
150
+ config.validators.register :access_token, proc { |value, options|
151
+ expected_prefix = options[:prefix] || "tok_"
152
+ minimum_length = options[:min_length] || 40
153
+
154
+ value.start_with?(expected_prefix) && value.length >= minimum_length
155
+ }
156
+
157
+ # Remove validator
158
+ config.validators.deregister :username
159
+ end
160
+ ```
161
+
162
+ ## Task Configuration
163
+
164
+ ### Settings
165
+
166
+ Override global configuration for specific tasks using `settings`:
167
+
168
+ ```ruby
169
+ class GenerateInvoice < CMDx::Task
170
+ settings(
171
+ # Global configuration overrides
172
+ task_breakpoints: ["failed"], # Breakpoint override
173
+ workflow_breakpoints: [], # Breakpoint override
174
+ logger: CustomLogger.new($stdout), # Custom logger
175
+
176
+ # Task configuration settings
177
+ breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints
178
+ log_level: :info, # Log level override
179
+ log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
180
+ tags: ["billing", "financial"], # Logging tags
181
+ deprecated: true # Task deprecations
182
+ )
183
+
184
+ def work
185
+ # Your logic here...
186
+ end
187
+ end
188
+ ```
189
+
190
+ > [!TIP]
191
+ > Use task-level settings for tasks that require special handling, such as financial reporting, external API integrations, or critical system operations.
192
+
193
+ ### Registrations
194
+
195
+ Register middlewares, callbacks, coercions, and validators on a specific task.
196
+ Deregister options that should not be available.
197
+
198
+ ```ruby
199
+ class SendCampaignEmail < CMDx::Task
200
+ # Middlewares
201
+ register :middleware, CMDx::Middlewares::Timeout
202
+ deregister :middleware, AuditTrailMiddleware
203
+
204
+ # Callbacks
205
+ register :callback, :on_complete, proc { |task|
206
+ runtime = task.metadata[:runtime]
207
+ Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"])
208
+ }
209
+ deregister :callback, :before_execution, :initialize_user_session
210
+
211
+ # Coercions
212
+ register :coercion, :currency, CurrencyCoercion
213
+ deregister :coercion, :coordinates
214
+
215
+ # Validators
216
+ register :validator, :username, :username_validator
217
+ deregister :validator, :url
218
+
219
+ def work
220
+ # Your logic here...
221
+ end
222
+ end
223
+ ```
224
+
225
+ ## Configuration Management
226
+
227
+ ### Access
228
+
229
+ ```ruby
230
+ # Global configuration access
231
+ CMDx.configuration.logger #=> <Logger instance>
232
+ CMDx.configuration.task_breakpoints #=> ["failed"]
233
+ CMDx.configuration.middlewares.registry #=> [<Middleware>, ...]
234
+
235
+ # Task configuration access
236
+ class ProcessUpload < CMDx::Task
237
+ settings(tags: ["files", "storage"])
238
+
239
+ def work
240
+ self.class.settings[:logger] #=> Global configuration value
241
+ self.class.settings[:tags] #=> Task configuration value => ["files", "storage"]
242
+ end
243
+ end
244
+ ```
245
+
246
+ ### Resetting
247
+
248
+ > [!WARNING]
249
+ > Resetting configuration affects the entire application. Use primarily in test environments or during application initialization.
250
+
251
+ ```ruby
252
+ # Reset to framework defaults
253
+ CMDx.reset_configuration!
254
+
255
+ # Verify reset
256
+ CMDx.configuration.task_breakpoints #=> ["failed"] (default)
257
+ CMDx.configuration.middlewares.registry #=> Empty registry
258
+
259
+ # Commonly used in test setup (RSpec example)
260
+ RSpec.configure do |config|
261
+ config.before(:each) do
262
+ CMDx.reset_configuration!
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## Task Generator
268
+
269
+ Generate new CMDx tasks quickly using the built-in generator:
270
+
271
+ ```bash
272
+ rails generate cmdx:task ModerateBlogPost
273
+ ```
274
+
275
+ This creates a new task file with the basic structure:
276
+
277
+ ```ruby
278
+ # app/tasks/moderate_blog_post.rb
279
+ class ModerateBlogPost < CMDx::Task
280
+ def work
281
+ # Your logic here...
282
+ end
283
+ end
284
+ ```
285
+
286
+ > [!TIP]
287
+ > Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
288
+
289
+ ---
290
+
291
+ url: https://github.com/drexed/cmdx/blob/main/docs/basics/setup.md
292
+ ---
293
+
294
+ # Basics - Setup
295
+
296
+ Tasks are the core building blocks of CMDx, encapsulating business logic within structured, reusable objects. Each task represents a unit of work with automatic attribute validation, error handling, and execution tracking.
297
+
298
+ ## Structure
299
+
300
+ Tasks inherit from `CMDx::Task` and require only a `work` method:
301
+
302
+ ```ruby
303
+ class ValidateDocument < CMDx::Task
304
+ def work
305
+ # Your logic here...
306
+ end
307
+ end
308
+ ```
309
+
310
+ An exception will be raised if a work method is not defined.
311
+
312
+ ```ruby
313
+ class IncompleteTask < CMDx::Task
314
+ # No `work` method defined
315
+ end
316
+
317
+ IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
318
+ ```
319
+
320
+ ## Inheritance
321
+
322
+ All configuration options are inheritable by any child classes.
323
+ Create a base class to share common configuration across tasks:
324
+
325
+ ```ruby
326
+ class ApplicationTask < CMDx::Task
327
+ register :middleware, SecurityMiddleware
328
+
329
+ before_execution :initialize_request_tracking
330
+
331
+ attribute :session_id
332
+
333
+ private
334
+
335
+ def initialize_request_tracking
336
+ context.tracking_id ||= SecureRandom.uuid
337
+ end
338
+ end
339
+
340
+ class SyncInventory < ApplicationTask
341
+ def work
342
+ # Your logic here...
343
+ end
344
+ end
345
+ ```
346
+
347
+ ## Lifecycle
348
+
349
+ Tasks follow a predictable call pattern with specific states and statuses:
350
+
351
+ > [!CAUTION]
352
+ > Tasks are single-use objects. Once executed, they are frozen and cannot be executed again.
353
+
354
+ | Stage | State | Status | Description |
355
+ |-------|-------|--------|-------------|
356
+ | **Instantiation** | `initialized` | `success` | Task created with context |
357
+ | **Validation** | `executing` | `success`/`failed` | Attributes validated |
358
+ | **Execution** | `executing` | `success`/`failed`/`skipped` | `work` method runs |
359
+ | **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized |
360
+ | **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable |
361
+
362
+ ---
363
+
364
+ url: https://github.com/drexed/cmdx/blob/main/docs/basics/execution.md
365
+ ---
366
+
367
+ # Basics - Execution
368
+
369
+ Task execution in CMDx provides two distinct methods that handle success and halt scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow in your application workflows.
370
+
371
+ ## Methods Overview
372
+
373
+ Tasks are single-use objects. Once executed, they are frozen and cannot be executed again.
374
+ Create a new instance for subsequent executions.
375
+
376
+ | Method | Returns | Exceptions | Use Case |
377
+ |--------|---------|------------|----------|
378
+ | `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
379
+ | `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
380
+
381
+ ## Non-bang Execution
382
+
383
+ The `execute` method always returns a `CMDx::Result` object regardless of execution outcome.
384
+ This is the preferred method for most use cases.
385
+
386
+ Any unhandled exceptions will be caught and returned as a task failure.
387
+
388
+ ```ruby
389
+ result = CreateAccount.execute(email: "user@example.com")
390
+
391
+ # Check execution state
392
+ result.success? #=> true/false
393
+ result.failed? #=> true/false
394
+ result.skipped? #=> true/false
395
+
396
+ # Access result data
397
+ result.context.email #=> "user@example.com"
398
+ result.state #=> "complete"
399
+ result.status #=> "success"
400
+ ```
401
+
402
+ ## Bang Execution
403
+
404
+ The bang `execute!` method raises a `CMDx::Fault` based exception when tasks fail or are skipped, and returns a `CMDx::Result` object only on success.
405
+
406
+ It raises any unhandled non-fault exceptions caused during execution.
407
+
408
+ | Exception | Raised When |
409
+ |-----------|-------------|
410
+ | `CMDx::FailFault` | Task execution fails |
411
+ | `CMDx::SkipFault` | Task execution is skipped |
412
+
413
+ > [!IMPORTANT]
414
+ > `execute!` behavior depends on the `task_breakpoints` or `workflow_breakpoints` configuration. By default, it raises exceptions only on failures.
415
+
416
+ ```ruby
417
+ begin
418
+ result = CreateAccount.execute!(email: "user@example.com")
419
+ SendWelcomeEmail.execute(result.context)
420
+ rescue CMDx::Fault => e
421
+ ScheduleAccountRetryJob.perform_later(e.result.context.email)
422
+ rescue CMDx::SkipFault => e
423
+ Rails.logger.info("Account creation skipped: #{e.result.reason}")
424
+ rescue Exception => e
425
+ ErrorTracker.capture(unhandled_exception: e)
426
+ end
427
+ ```
428
+
429
+ ## Direct Instantiation
430
+
431
+ Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
432
+
433
+ ```ruby
434
+ # Direct instantiation
435
+ task = CreateAccount.new(email: "user@example.com", send_welcome: true)
436
+
437
+ # Access properties before execution
438
+ task.id #=> "abc123..." (unique task ID)
439
+ task.context.email #=> "user@example.com"
440
+ task.context.send_welcome #=> true
441
+ task.result.state #=> "initialized"
442
+ result.status #=> "success"
443
+
444
+ # Manual execution
445
+ task.execute
446
+ # or
447
+ task.execute!
448
+
449
+ task.result.success? #=> true/false
450
+ ```
451
+
452
+ ## Result Details
453
+
454
+ The `Result` object provides comprehensive execution information:
455
+
456
+ ```ruby
457
+ result = CreateAccount.execute(email: "user@example.com")
458
+
459
+ # Execution metadata
460
+ result.id #=> "abc123..." (unique execution ID)
461
+ result.task #=> CreateAccount instance (frozen)
462
+ result.chain #=> Task execution chain
463
+
464
+ # Context and metadata
465
+ result.context #=> Context with all task data
466
+ result.metadata #=> Hash with execution metadata
467
+
468
+ ---
469
+
470
+ url: https://github.com/drexed/cmdx/blob/main/docs/basics/context.md
471
+ ---
472
+
473
+ # Basics - Context
474
+
475
+ Task context provides flexible data storage, access, and sharing within task execution. It serves as the primary data container for all task inputs, intermediate results, and outputs.
476
+
477
+ ## Assigning Data
478
+
479
+ Context is automatically populated with all inputs passed to a task. All keys are normalized to symbols for consistent access:
480
+
481
+ ```ruby
482
+ # Direct execution
483
+ CalculateShipping.execute(weight: 2.5, destination: "CA")
484
+
485
+ # Instance creation
486
+ CalculateShipping.new(weight: 2.5, "destination" => "CA")
487
+ ```
488
+
489
+ > [!IMPORTANT]
490
+ > String keys are automatically converted to symbols. Use symbols for consistency in your code.
491
+
492
+ ## Accessing Data
493
+
494
+ Context provides multiple access patterns with automatic nil safety:
495
+
496
+ ```ruby
497
+ class CalculateShipping < CMDx::Task
498
+ def work
499
+ # Method style access (preferred)
500
+ weight = context.weight
501
+ destination = context.destination
502
+
503
+ # Hash style access
504
+ service_type = context[:service_type]
505
+ options = context["options"]
506
+
507
+ # Safe access with defaults
508
+ rush_delivery = context.fetch!(:rush_delivery, false)
509
+ carrier = context.dig(:options, :carrier)
510
+
511
+ # Shorter alias
512
+ cost = ctx.weight * ctx.rate_per_pound # ctx aliases context
513
+ end
514
+ end
515
+ ```
516
+
517
+ > [!IMPORTANT]
518
+ > Accessing undefined context attributes returns `nil` instead of raising errors, enabling graceful handling of optional attributes.
519
+
520
+ ## Modifying Context
521
+
522
+ Context supports dynamic modification during task execution:
523
+
524
+ ```ruby
525
+ class CalculateShipping < CMDx::Task
526
+ def work
527
+ # Direct assignment
528
+ context.carrier = Carrier.find_by(code: context.carrier_code)
529
+ context.package = Package.new(weight: context.weight)
530
+ context.calculated_at = Time.now
531
+
532
+ # Hash-style assignment
533
+ context[:status] = "calculating"
534
+ context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}"
535
+
536
+ # Conditional assignment
537
+ context.insurance_included ||= false
538
+
539
+ # Batch updates
540
+ context.merge!(
541
+ status: "completed",
542
+ shipping_cost: calculate_cost,
543
+ estimated_delivery: Time.now + 3.days
544
+ )
545
+
546
+ # Remove sensitive data
547
+ context.delete!(:credit_card_token)
548
+ end
549
+
550
+ private
551
+
552
+ def calculate_cost
553
+ base_rate = context.weight * context.rate_per_pound
554
+ base_rate + (base_rate * context.tax_percentage)
555
+ end
556
+ end
557
+ ```
558
+
559
+ > [!TIP]
560
+ > Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline.
561
+
562
+ ## Data Sharing
563
+
564
+ Context enables seamless data flow between related tasks in complex workflows:
565
+
566
+ ```ruby
567
+ # During execution
568
+ class CalculateShipping < CMDx::Task
569
+ def work
570
+ # Validate shipping data
571
+ validation_result = ValidateAddress.execute(context)
572
+
573
+ # Via context
574
+ CalculateInsurance.execute(context)
575
+
576
+ # Via result
577
+ NotifyShippingCalculated.execute(validation_result)
578
+
579
+ # Context now contains accumulated data from all tasks
580
+ context.address_validated #=> true (from validation)
581
+ context.insurance_calculated #=> true (from insurance)
582
+ context.notification_sent #=> true (from notification)
583
+ end
584
+ end
585
+
586
+ # After execution
587
+ result = CalculateShipping.execute(destination: "New York, NY")
588
+
589
+ CreateShippingLabel.execute(result)
590
+ ```
591
+
592
+ ---
593
+
594
+ url: https://github.com/drexed/cmdx/blob/main/docs/basics/chain.md
595
+ ---
596
+
597
+ # Basics - Chain
598
+
599
+ Chains automatically group related task executions within a thread, providing unified tracking, correlation, and execution context management. Each thread maintains its own chain through thread-local storage, eliminating the need for manual coordination.
600
+
601
+ ## Management
602
+
603
+ Each thread maintains its own chain context through thread-local storage, providing automatic isolation without manual coordination.
604
+
605
+ > [!WARNING]
606
+ > Chain operations are thread-local. Never share chain references across threads as this can lead to race conditions and data corruption.
607
+
608
+ ```ruby
609
+ # Thread A
610
+ Thread.new do
611
+ result = ImportDataset.execute(file_path: "/data/batch1.csv")
612
+ result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
613
+ end
614
+
615
+ # Thread B (completely separate chain)
616
+ Thread.new do
617
+ result = ImportDataset.execute(file_path: "/data/batch2.csv")
618
+ result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
619
+ end
620
+
621
+ # Access current thread's chain
622
+ CMDx::Chain.current #=> Returns current chain or nil
623
+ CMDx::Chain.clear #=> Clears current thread's chain
624
+ ```
625
+
626
+ ## Links
627
+
628
+ Every task execution automatically creates or joins the current thread's chain:
629
+
630
+ > [!IMPORTANT]
631
+ > Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle.
632
+
633
+ ```ruby
634
+ class ImportDataset < CMDx::Task
635
+ def work
636
+ # First task creates new chain
637
+ result1 = ValidateHeaders.execute(file_path: context.file_path)
638
+ result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
639
+ result1.chain.results.size #=> 1
640
+
641
+ # Second task joins existing chain
642
+ result2 = SendNotification.execute(to: "admin@company.com")
643
+ result2.chain.id == result1.chain.id #=> true
644
+ result2.chain.results.size #=> 2
645
+
646
+ # Both results reference the same chain
647
+ result1.chain.results == result2.chain.results #=> true
648
+ end
649
+ end
650
+ ```
651
+
652
+ ## Inheritance
653
+
654
+ When tasks call subtasks within the same thread, all executions automatically inherit the current chain, creating a unified execution trail.
655
+
656
+ ```ruby
657
+ class ImportDataset < CMDx::Task
658
+ def work
659
+ context.dataset = Dataset.find(context.dataset_id)
660
+
661
+ # Subtasks automatically inherit current chain
662
+ ValidateSchema.execute
663
+ TransformData.execute!(context)
664
+ SaveToDatabase.execute(dataset_id: context.dataset_id)
665
+ end
666
+ end
667
+
668
+ result = ImportDataset.execute(dataset_id: 456)
669
+ chain = result.chain
670
+
671
+ # All tasks share the same chain
672
+ chain.results.size #=> 4 (main task + 3 subtasks)
673
+ chain.results.map { |r| r.task.class }
674
+ #=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]
675
+ ```
676
+
677
+ ## Structure
678
+
679
+ Chains provide comprehensive execution information with state delegation:
680
+
681
+ > [!IMPORTANT]
682
+ > Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states.
683
+
684
+ ```ruby
685
+ result = ImportDataset.execute(dataset_id: 456)
686
+ chain = result.chain
687
+
688
+ # Chain identification
689
+ chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
690
+ chain.results #=> Array of all results in execution order
691
+
692
+ # State delegation (from first/outer-most result)
693
+ chain.state #=> "complete"
694
+ chain.status #=> "success"
695
+ chain.outcome #=> "success"
696
+
697
+ # Access individual results
698
+ chain.results.each_with_index do |result, index|
699
+ puts "#{index}: #{result.task.class} - #{result.status}"
700
+ end
701
+ ```
702
+
703
+ ---
704
+
705
+ url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/halt.md
706
+ ---
707
+
708
+ # Interruptions - Halt
709
+
710
+ Halting stops task execution with explicit intent signaling. Tasks provide two primary halt methods that control execution flow and result in different outcomes.
711
+
712
+ ## Skipping
713
+
714
+ `skip!` communicates that the task is to be intentionally bypassed. This represents a controlled, intentional interruption where the task determines that execution is not necessary or appropriate.
715
+
716
+ > [!IMPORTANT]
717
+ > Skipping is a no-op, not a failure or error and are considered successful outcomes.
718
+
719
+ ```ruby
720
+ class ProcessInventory < CMDx::Task
721
+ def work
722
+ # Without a reason
723
+ skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
724
+
725
+ # With a reason
726
+ skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
727
+
728
+ inventory = Inventory.find(context.inventory_id)
729
+
730
+ if inventory.already_counted?
731
+ skip!("Inventory already counted today")
732
+ else
733
+ inventory.count!
734
+ end
735
+ end
736
+ end
737
+
738
+ result = ProcessInventory.execute(inventory_id: 456)
739
+
740
+ # Executed
741
+ result.status #=> "skipped"
742
+
743
+ # Without a reason
744
+ result.reason #=> "no reason given"
745
+
746
+ # With a reason
747
+ result.reason #=> "Warehouse closed"
748
+ ```
749
+
750
+ ## Failing
751
+
752
+ `fail!` communicates that the task encountered an impediment that prevents successful completion. This represents controlled failure where the task explicitly determines that execution cannot continue.
753
+
754
+ ```ruby
755
+ class ProcessRefund < CMDx::Task
756
+ def work
757
+ # Without a reason
758
+ fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
759
+
760
+ refund = Refund.find(context.refund_id)
761
+
762
+ # With a reason
763
+ if refund.expired?
764
+ fail!("Refund period has expired")
765
+ elsif !refund.amount.positive?
766
+ fail!("Refund amount must be positive")
767
+ else
768
+ refund.process!
769
+ end
770
+ end
771
+ end
772
+
773
+ result = ProcessRefund.execute(refund_id: 789)
774
+
775
+ # Executed
776
+ result.status #=> "failed"
777
+
778
+ # Without a reason
779
+ result.reason #=> "no reason given"
780
+
781
+ # With a reason
782
+ result.reason #=> "Refund period has expired"
783
+ ```
784
+
785
+ ## Metadata Enrichment
786
+
787
+ Both halt methods accept metadata to provide additional context about the interruption. Metadata is stored as a hash and becomes available through the result object.
788
+
789
+ ```ruby
790
+ class ProcessRenewal < CMDx::Task
791
+ def work
792
+ license = License.find(context.license_id)
793
+
794
+ if license.already_renewed?
795
+ # Without metadata
796
+ skip!("License already renewed")
797
+ end
798
+
799
+ unless license.renewal_eligible?
800
+ # With metadata
801
+ fail!(
802
+ "License not eligible for renewal",
803
+ error_code: "LICENSE.NOT_ELIGIBLE",
804
+ retry_after: Time.current + 30.days
805
+ )
806
+ end
807
+
808
+ process_renewal
809
+ end
810
+ end
811
+
812
+ result = ProcessRenewal.execute(license_id: 567)
813
+
814
+ # Without metadata
815
+ result.metadata #=> {}
816
+
817
+ # With metadata
818
+ result.metadata #=> {
819
+ # error_code: "LICENSE.NOT_ELIGIBLE",
820
+ # retry_after: <Time 30 days from now>
821
+ # }
822
+ ```
823
+
824
+ ## State Transitions
825
+
826
+ Halt methods trigger specific state and status transitions:
827
+
828
+ | Method | State | Status | Outcome |
829
+ |--------|-------|--------|---------|
830
+ | `skip!` | `interrupted` | `skipped` | `good? = true`, `bad? = true` |
831
+ | `fail!` | `interrupted` | `failed` | `good? = false`, `bad? = true` |
832
+
833
+ ```ruby
834
+ result = ProcessRenewal.execute(license_id: 567)
835
+
836
+ # State information
837
+ result.state #=> "interrupted"
838
+ result.status #=> "skipped" or "failed"
839
+ result.interrupted? #=> true
840
+ result.complete? #=> false
841
+
842
+ # Outcome categorization
843
+ result.good? #=> true for skipped, false for failed
844
+ result.bad? #=> true for both skipped and failed
845
+ ```
846
+
847
+ ## Execution Behavior
848
+
849
+ Halt methods behave differently depending on the call method used:
850
+
851
+ ### Non-bang execution
852
+
853
+ Returns result object without raising exceptions:
854
+
855
+ ```ruby
856
+ result = ProcessRefund.execute(refund_id: 789)
857
+
858
+ case result.status
859
+ when "success"
860
+ puts "Refund processed: $#{result.context.refund.amount}"
861
+ when "skipped"
862
+ puts "Refund skipped: #{result.reason}"
863
+ when "failed"
864
+ puts "Refund failed: #{result.reason}"
865
+ handle_refund_error(result.metadata[:error_code])
866
+ end
867
+ ```
868
+
869
+ ### Bang execution
870
+
871
+ Raises exceptions for halt conditions based on `task_breakpoints` configuration:
872
+
873
+ ```ruby
874
+ begin
875
+ result = ProcessRefund.execute!(refund_id: 789)
876
+ puts "Success: Refund processed"
877
+ rescue CMDx::SkipFault => e
878
+ puts "Skipped: #{e.message}"
879
+ rescue CMDx::FailFault => e
880
+ puts "Failed: #{e.message}"
881
+ handle_refund_failure(e.result.metadata[:error_code])
882
+ end
883
+ ```
884
+
885
+ ## Best Practices
886
+
887
+ Always try to provide a `reason` when using halt methods. This provides clear context for debugging and creates meaningful exception messages.
888
+
889
+ ```ruby
890
+ # Good: Clear, specific reason
891
+ skip!("Document processing paused for compliance review")
892
+ fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
893
+
894
+ # Acceptable: Generic, non-specific reason
895
+ skip!("Paused")
896
+ fail!("Unsupported")
897
+
898
+ # Bad: Default, cannot determine reason
899
+ skip! #=> "no reason given"
900
+ fail! #=> "no reason given"
901
+
902
+ ---
903
+
904
+ url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/faults.md
905
+ ---
906
+
907
+ # Interruptions - Faults
908
+
909
+ Faults are exception mechanisms that halt task execution via `skip!` and `fail!` methods. When tasks execute with the `execute!` method, fault exceptions matching the task's interruption status are raised, enabling sophisticated exception handling and control flow patterns.
910
+
911
+ ## Fault Types
912
+
913
+ | Type | Triggered By | Use Case |
914
+ |------|--------------|----------|
915
+ | `CMDx::Fault` | Base class | Catch-all for any interruption |
916
+ | `CMDx::SkipFault` | `skip!` method | Optional processing, early returns |
917
+ | `CMDx::FailFault` | `fail!` method | Validation errors, processing failures |
918
+
919
+ > [!IMPORTANT]
920
+ > All fault exceptions inherit from `CMDx::Fault` and provide access to the complete task execution context including result, task, context, and chain information.
921
+
922
+ ## Fault Handling
923
+
924
+ ```ruby
925
+ begin
926
+ ProcessTicket.execute!(ticket_id: 456)
927
+ rescue CMDx::SkipFault => e
928
+ logger.info "Ticket processing skipped: #{e.message}"
929
+ schedule_retry(e.context.ticket_id)
930
+ rescue CMDx::FailFault => e
931
+ logger.error "Ticket processing failed: #{e.message}"
932
+ notify_admin(e.context.assigned_agent, e.result.metadata[:error_code])
933
+ rescue CMDx::Fault => e
934
+ logger.warn "Ticket processing interrupted: #{e.message}"
935
+ rollback_changes
936
+ end
937
+ ```
938
+
939
+ ## Data Access
940
+
941
+ Faults provide comprehensive access to execution context, eg:
942
+
943
+ ```ruby
944
+ begin
945
+ LicenseActivation.execute!(license_key: key, machine_id: machine)
946
+ rescue CMDx::Fault => e
947
+ # Result information
948
+ e.result.state #=> "interrupted"
949
+ e.result.status #=> "failed" or "skipped"
950
+ e.result.reason #=> "License key already activated"
951
+
952
+ # Task information
953
+ e.task.class #=> <LicenseActivation>
954
+ e.task.id #=> "abc123..."
955
+
956
+ # Context data
957
+ e.context.license_key #=> "ABC-123-DEF"
958
+ e.context.machine_id #=> "[FILTERED]"
959
+
960
+ # Chain information
961
+ e.chain.id #=> "def456..."
962
+ e.chain.size #=> 3
963
+ end
964
+ ```
965
+
966
+ ## Advanced Matching
967
+
968
+ ### Task-Specific Matching
969
+
970
+ Use `for?` to handle faults only from specific task classes, enabling targeted exception handling in complex workflows.
971
+
972
+ ```ruby
973
+ begin
974
+ DocumentWorkflow.execute!(document_data: data)
975
+ rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e
976
+ # Handle only document-related failures
977
+ retry_with_alternate_parser(e.context)
978
+ rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e
979
+ # Handle security-related skips
980
+ quarantine_for_review(e.context.document_id)
981
+ end
982
+ ```
983
+
984
+ ### Custom Logic Matching
985
+
986
+ ```ruby
987
+ begin
988
+ ReportGenerator.execute!(report: report_data)
989
+ rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e
990
+ escalate_large_dataset_failure(e)
991
+ rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e
992
+ abandon_report_generation(e)
993
+ rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
994
+ increase_memory_and_retry(e)
995
+ end
996
+ ```
997
+
998
+ ## Fault Propagation
999
+
1000
+ Use `throw!` to propagate failures while preserving fault context and maintaining the error chain for debugging.
1001
+
1002
+ ### Basic Propagation
1003
+
1004
+ ```ruby
1005
+ class ReportGenerator < CMDx::Task
1006
+ def work
1007
+ # Throw if skipped or failed
1008
+ validation_result = DataValidator.execute(context)
1009
+ throw!(validation_result)
1010
+
1011
+ # Only throw if skipped
1012
+ check_permissions = CheckPermissions.execute(context)
1013
+ throw!(check_permissions) if check_permissions.skipped?
1014
+
1015
+ # Only throw if failed
1016
+ data_result = DataProcessor.execute(context)
1017
+ throw!(data_result) if data_result.failed?
1018
+
1019
+ # Continue processing
1020
+ generate_report
1021
+ end
1022
+ end
1023
+ ```
1024
+
1025
+ ### Additional Metadata
1026
+
1027
+ ```ruby
1028
+ class BatchProcessor < CMDx::Task
1029
+ def work
1030
+ step_result = FileValidation.execute(context)
1031
+
1032
+ if step_result.failed?
1033
+ throw!(step_result, {
1034
+ batch_stage: "validation",
1035
+ can_retry: true,
1036
+ next_step: "file_repair"
1037
+ })
1038
+ end
1039
+
1040
+ continue_batch
1041
+ end
1042
+ end
1043
+ ```
1044
+
1045
+ ## Chain Analysis
1046
+
1047
+ Results provide methods to analyze fault propagation and identify original failure sources in complex execution chains.
1048
+
1049
+ ```ruby
1050
+ result = DocumentWorkflow.execute(invalid_data)
1051
+
1052
+ if result.failed?
1053
+ # Trace the original failure
1054
+ original = result.caused_failure
1055
+ if original
1056
+ puts "Original failure: #{original.task.class.name}"
1057
+ puts "Reason: #{original.reason}"
1058
+ end
1059
+
1060
+ # Find what propagated the failure
1061
+ thrower = result.threw_failure
1062
+ puts "Propagated by: #{thrower.task.class.name}" if thrower
1063
+
1064
+ # Analyze failure type
1065
+ case
1066
+ when result.caused_failure?
1067
+ puts "This task was the original source"
1068
+ when result.threw_failure?
1069
+ puts "This task propagated a failure"
1070
+ when result.thrown_failure?
1071
+ puts "This task failed due to propagation"
1072
+ end
1073
+ end
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/exceptions.md
1079
+ ---
1080
+
1081
+ # Interruptions - Exceptions
1082
+
1083
+ CMDx provides robust exception handling that differs between the `execute` and `execute!` methods. Understanding how unhandled exceptions are processed is crucial for building reliable task execution flows and implementing proper error handling strategies.
1084
+
1085
+ ## Exception Handling
1086
+
1087
+ > [!IMPORTANT]
1088
+ > When designing tasks try not to `raise` your own exceptions directly, instead use `skip!` or `fail!` to signal intent clearly.
1089
+
1090
+ ### Non-bang execution
1091
+
1092
+ The `execute` method captures **all** unhandled exceptions and converts them to failed results, ensuring predictable behavior and consistent result processing.
1093
+
1094
+ ```ruby
1095
+ class CompressDocument < CMDx::Task
1096
+ def work
1097
+ document = Document.find(context.document_id)
1098
+ document.compress!
1099
+ end
1100
+ end
1101
+
1102
+ result = CompressDocument.execute(document_id: "unknown-doc-id")
1103
+ result.state #=> "interrupted"
1104
+ result.status #=> "failed"
1105
+ result.failed? #=> true
1106
+ result.reason #=> "[ActiveRecord::NotFoundError] record not found"
1107
+ result.cause #=> <ActiveRecord::NotFoundError>
1108
+ ```
1109
+
1110
+ ### Bang execution
1111
+
1112
+ The `execute!` method allows unhandled exceptions to propagate, enabling standard Ruby exception handling while respecting CMDx fault configuration.
1113
+
1114
+ ```ruby
1115
+ class CompressDocument < CMDx::Task
1116
+ def work
1117
+ document = Document.find(context.document_id)
1118
+ document.compress!
1119
+ end
1120
+ end
1121
+
1122
+ begin
1123
+ CompressDocument.execute!(document_id: "unknown-doc-id")
1124
+ rescue ActiveRecord::NotFoundError => e
1125
+ puts "Handle exception: #{e.message}"
1126
+ end
1127
+ ```
1128
+
1129
+ ---
1130
+
1131
+ url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/result.md
1132
+ ---
1133
+
1134
+ # Outcomes - Result
1135
+
1136
+ The result object is the comprehensive return value of task execution, providing complete information about the execution outcome, state, timing, and any data produced during the task lifecycle. Results serve as the primary interface for inspecting task execution outcomes and chaining task operations.
1137
+
1138
+ ## Result Attributes
1139
+
1140
+ Every result provides access to essential execution information:
1141
+
1142
+ > [!IMPORTANT]
1143
+ > Result objects are immutable after task execution completes and reflect the final state.
1144
+
1145
+ ```ruby
1146
+ result = BuildApplication.execute(version: "1.2.3")
1147
+
1148
+ # Object data
1149
+ result.task #=> <BuildApplication>
1150
+ result.context #=> <CMDx::Context>
1151
+ result.chain #=> <CMDx::Chain>
1152
+
1153
+ # Execution data
1154
+ result.state #=> "interrupted"
1155
+ result.status #=> "failed"
1156
+
1157
+ # Fault data
1158
+ result.reason #=> "Build tool not found"
1159
+ result.cause #=> <CMDx::FailFault>
1160
+ result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
1161
+ ```
1162
+
1163
+ ## Lifecycle Information
1164
+
1165
+ Results provide comprehensive methods for checking execution state and status:
1166
+
1167
+ ```ruby
1168
+ result = BuildApplication.execute(version: "1.2.3")
1169
+
1170
+ # State predicates (execution lifecycle)
1171
+ result.complete? #=> true (successful completion)
1172
+ result.interrupted? #=> false (no interruption)
1173
+ result.executed? #=> true (execution finished)
1174
+
1175
+ # Status predicates (execution outcome)
1176
+ result.success? #=> true (successful execution)
1177
+ result.failed? #=> false (no failure)
1178
+ result.skipped? #=> false (not skipped)
1179
+
1180
+ # Outcome categorization
1181
+ result.good? #=> true (success or skipped)
1182
+ result.bad? #=> false (skipped or failed)
1183
+ ```
1184
+
1185
+ ## Outcome Analysis
1186
+
1187
+ Results provide unified outcome determination depending on the fault causal chain:
1188
+
1189
+ ```ruby
1190
+ result = BuildApplication.execute(version: "1.2.3")
1191
+
1192
+ result.outcome #=> "success" (state and status)
1193
+ ```
1194
+
1195
+ ## Chain Analysis
1196
+
1197
+ Use these methods to trace the root cause of faults or trace the cause points.
1198
+
1199
+ ```ruby
1200
+ result = DeploymentWorkflow.execute(app_name: "webapp")
1201
+
1202
+ if result.failed?
1203
+ # Find the original cause of failure
1204
+ if original_failure = result.caused_failure
1205
+ puts "Root cause: #{original_failure.task.class.name}"
1206
+ puts "Reason: #{original_failure.reason}"
1207
+ end
1208
+
1209
+ # Find what threw the failure to this result
1210
+ if throwing_task = result.threw_failure
1211
+ puts "Failure source: #{throwing_task.task.class.name}"
1212
+ puts "Reason: #{throwing_task.reason}"
1213
+ end
1214
+
1215
+ # Failure classification
1216
+ result.caused_failure? #=> true if this result was the original cause
1217
+ result.threw_failure? #=> true if this result threw a failure
1218
+ result.thrown_failure? #=> true if this result received a thrown failure
1219
+ end
1220
+ ```
1221
+
1222
+ ## Index and Position
1223
+
1224
+ Results track their position within execution chains:
1225
+
1226
+ ```ruby
1227
+ result = BuildApplication.execute(version: "1.2.3")
1228
+
1229
+ # Position in execution sequence
1230
+ result.index #=> 0 (first task in chain)
1231
+
1232
+ # Access via chain
1233
+ result.chain.results[result.index] == result #=> true
1234
+ ```
1235
+
1236
+ ## Handlers
1237
+
1238
+ Use result handlers for clean, functional-style conditional logic. Handlers return the result object, enabling method chaining and fluent interfaces.
1239
+
1240
+ ```ruby
1241
+ result = BuildApplication.execute(version: "1.2.3")
1242
+
1243
+ # Status-based handlers
1244
+ result
1245
+ .on_success { |result| notify_deployment_ready(result) }
1246
+ .on_failed { |result| handle_build_failure(result) }
1247
+ .on_skipped { |result| log_skip_reason(result) }
1248
+
1249
+ # State-based handlers
1250
+ result
1251
+ .on_complete { |result| update_build_status(result) }
1252
+ .on_interrupted { |result| cleanup_partial_artifacts(result) }
1253
+
1254
+ # Outcome-based handlers
1255
+ result
1256
+ .on_good { |result| increment_success_counter(result) }
1257
+ .on_bad { |result| alert_operations_team(result) }
1258
+ ```
1259
+
1260
+ ## Pattern Matching
1261
+
1262
+ Results support Ruby's pattern matching through array and hash deconstruction:
1263
+
1264
+ > [!IMPORTANT]
1265
+ > Pattern matching requires Ruby 3.0+
1266
+
1267
+ ### Array Pattern
1268
+
1269
+ ```ruby
1270
+ result = BuildApplication.execute(version: "1.2.3")
1271
+
1272
+ case result
1273
+ in ["complete", "success"]
1274
+ redirect_to build_success_page
1275
+ in ["interrupted", "failed"]
1276
+ retry_build_with_backoff(result)
1277
+ in ["interrupted", "skipped"]
1278
+ log_skip_and_continue
1279
+ end
1280
+ ```
1281
+
1282
+ ### Hash Pattern
1283
+
1284
+ ```ruby
1285
+ result = BuildApplication.execute(version: "1.2.3")
1286
+
1287
+ case result
1288
+ in { state: "complete", status: "success" }
1289
+ celebrate_build_success
1290
+ in { status: "failed", metadata: { retryable: true } }
1291
+ schedule_build_retry(result)
1292
+ in { bad: true, metadata: { reason: String => reason } }
1293
+ escalate_build_error("Build failed: #{reason}")
1294
+ end
1295
+ ```
1296
+
1297
+ ### Pattern Guards
1298
+
1299
+ ```ruby
1300
+ case result
1301
+ in { status: "failed", metadata: { attempts: n } } if n < 3
1302
+ retry_build_with_delay(result, n * 2)
1303
+ in { status: "failed", metadata: { attempts: n } } if n >= 3
1304
+ mark_build_permanently_failed(result)
1305
+ in { runtime: time } if time > performance_threshold
1306
+ investigate_build_performance(result)
1307
+ end
1308
+ ```
1309
+
1310
+ ---
1311
+
1312
+ url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/states.md
1313
+ ---
1314
+
1315
+ # Outcomes - States
1316
+
1317
+ States represent the execution lifecycle condition of task execution, tracking
1318
+ the progress of tasks through their complete execution journey. States provide
1319
+ insight into where a task is in its lifecycle and enable lifecycle-based
1320
+ decision making and monitoring.
1321
+
1322
+ ## Definitions
1323
+
1324
+ | State | Description |
1325
+ | ----- | ----------- |
1326
+ | `initialized` | Task created but execution not yet started. Default state for new tasks. |
1327
+ | `executing` | Task is actively running its business logic. Transient state during execution. |
1328
+ | `complete` | Task finished execution successfully without any interruption or halt. |
1329
+ | `interrupted` | Task execution was stopped due to a fault, exception, or explicit halt. |
1330
+
1331
+ State-Status combinations:
1332
+
1333
+ | State | Status | Meaning |
1334
+ | ----- | ------ | ------- |
1335
+ | `initialized` | `success` | Task created, not yet executed |
1336
+ | `executing` | `success` | Task currently running |
1337
+ | `complete` | `success` | Task finished successfully |
1338
+ | `complete` | `skipped` | Task finished by skipping execution |
1339
+ | `interrupted` | `failed` | Task stopped due to failure |
1340
+ | `interrupted` | `skipped` | Task stopped by skip condition |
1341
+
1342
+ ## Transitions
1343
+
1344
+ > [!CAUTION]
1345
+ > States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework.
1346
+
1347
+ ```ruby
1348
+ # Valid state transition flow
1349
+ initialized → executing → complete (successful execution)
1350
+ initialized → executing → interrupted (skipped/failed execution)
1351
+ ```
1352
+
1353
+ ## Predicates
1354
+
1355
+ Use state predicates to check the current execution lifecycle:
1356
+
1357
+ ```ruby
1358
+ result = ProcessVideoUpload.execute
1359
+
1360
+ # Individual state checks
1361
+ result.initialized? #=> false (after execution)
1362
+ result.executing? #=> false (after execution)
1363
+ result.complete? #=> true (successful completion)
1364
+ result.interrupted? #=> false (no interruption)
1365
+
1366
+ # State categorization
1367
+ result.executed? #=> true (complete OR interrupted)
1368
+ ```
1369
+
1370
+ ## Handlers
1371
+
1372
+ Use state-based handlers for lifecycle event handling. The `on_executed` handler is particularly useful for cleanup operations that should run regardless of success, skipped, or failure.
1373
+
1374
+ ```ruby
1375
+ result = ProcessVideoUpload.execute
1376
+
1377
+ # Individual state handlers
1378
+ result
1379
+ .on_complete { |result| send_upload_notification(result) }
1380
+ .on_interrupted { |result| cleanup_temp_files(result) }
1381
+ .on_executed { |result| log_upload_metrics(result) }
1382
+ ```
1383
+
1384
+ ---
1385
+
1386
+ url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/statuses.md
1387
+ ---
1388
+
1389
+ # Outcomes - Statuses
1390
+
1391
+ Statuses represent the business outcome of task execution logic, indicating how the task's business logic concluded. Statuses differ from execution states by focusing on the business outcome rather than the technical execution lifecycle. Understanding statuses is crucial for implementing proper business logic branching and error handling.
1392
+
1393
+ ## Definitions
1394
+
1395
+ | Status | Description |
1396
+ | ------ | ----------- |
1397
+ | `success` | Task execution completed successfully with expected business outcome. Default status for all tasks. |
1398
+ | `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary. |
1399
+ | `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions. |
1400
+
1401
+ ## Transitions
1402
+
1403
+ > [!IMPORTANT]
1404
+ > Status transitions are unidirectional and final. Once a task is marked as skipped or failed, it cannot return to success status. Design your business logic accordingly.
1405
+
1406
+ ```ruby
1407
+ # Valid status transitions
1408
+ success → skipped # via skip!
1409
+ success → failed # via fail! or exception
1410
+
1411
+ # Invalid transitions (will raise errors)
1412
+ skipped → success # ❌ Cannot transition
1413
+ skipped → failed # ❌ Cannot transition
1414
+ failed → success # ❌ Cannot transition
1415
+ failed → skipped # ❌ Cannot transition
1416
+ ```
1417
+
1418
+ ## Predicates
1419
+
1420
+ Use status predicates to check execution outcomes:
1421
+
1422
+ ```ruby
1423
+ result = ProcessNotification.execute
1424
+
1425
+ # Individual status checks
1426
+ result.success? #=> true/false
1427
+ result.skipped? #=> true/false
1428
+ result.failed? #=> true/false
1429
+
1430
+ # Outcome categorization
1431
+ result.good? #=> true if success OR skipped
1432
+ result.bad? #=> true if skipped OR failed (not success)
1433
+ ```
1434
+
1435
+ ## Handlers
1436
+
1437
+ Use status-based handlers for business logic branching. The `on_good` and `on_bad` handlers are particularly useful for handling success/skip vs failed outcomes respectively.
1438
+
1439
+ ```ruby
1440
+ result = ProcessNotification.execute
1441
+
1442
+ # Individual status handlers
1443
+ result
1444
+ .on_success { |result| mark_notification_sent(result) }
1445
+ .on_skipped { |result| log_notification_skipped(result) }
1446
+ .on_failed { |result| queue_retry_notification(result) }
1447
+
1448
+ # Outcome-based handlers
1449
+ result
1450
+ .on_good { |result| update_message_stats(result) }
1451
+ .on_bad { |result| track_delivery_failure(result) }
1452
+ ```
1453
+
1454
+ ---
1455
+
1456
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/definitions.md
1457
+ ---
1458
+
1459
+ # Attributes - Definitions
1460
+
1461
+ Attributes define the interface between task callers and implementation, enabling automatic validation, type coercion, and method generation. They provide a contract to verify that task execution arguments match expected requirements and structure.
1462
+
1463
+ ## Declarations
1464
+
1465
+ > [!TIP]
1466
+ > Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
1467
+
1468
+ ### Optional
1469
+
1470
+ Optional attributes return `nil` when not provided.
1471
+
1472
+ ```ruby
1473
+ class ScheduleEvent < CMDx::Task
1474
+ attribute :title
1475
+ attributes :duration, :location
1476
+
1477
+ # Alias for attributes (preferred)
1478
+ optional :description
1479
+ optional :visibility, :attendees
1480
+
1481
+ def work
1482
+ title #=> "Team Standup"
1483
+ duration #=> 30
1484
+ location #=> nil
1485
+ description #=> nil
1486
+ visibility #=> nil
1487
+ attendees #=> ["alice@company.com", "bob@company.com"]
1488
+ end
1489
+ end
1490
+
1491
+ # Attributes passed as keyword arguments
1492
+ ScheduleEvent.execute(
1493
+ title: "Team Standup",
1494
+ duration: 30,
1495
+ attendees: ["alice@company.com", "bob@company.com"]
1496
+ )
1497
+ ```
1498
+
1499
+ ### Required
1500
+
1501
+ Required attributes must be provided in call arguments or task execution will fail.
1502
+
1503
+ ```ruby
1504
+ class PublishArticle < CMDx::Task
1505
+ attribute :title, required: true
1506
+ attributes :content, :author_id, required: true
1507
+
1508
+ # Alias for attributes => required: true (preferred)
1509
+ required :category
1510
+ required :status, :tags
1511
+
1512
+ def work
1513
+ title #=> "Getting Started with Ruby"
1514
+ content #=> "This is a comprehensive guide..."
1515
+ author_id #=> 42
1516
+ category #=> "programming"
1517
+ status #=> :published
1518
+ tags #=> ["ruby", "beginner"]
1519
+ end
1520
+ end
1521
+
1522
+ # Attributes passed as keyword arguments
1523
+ PublishArticle.execute(
1524
+ title: "Getting Started with Ruby",
1525
+ content: "This is a comprehensive guide...",
1526
+ author_id: 42,
1527
+ category: "programming",
1528
+ status: :published,
1529
+ tags: ["ruby", "beginner"]
1530
+ )
1531
+ ```
1532
+
1533
+ ## Sources
1534
+
1535
+ Attributes delegate to accessible objects within the task. The default source is `:context`, but any accessible method or object can serve as an attribute source.
1536
+
1537
+ ### Context
1538
+
1539
+ ```ruby
1540
+ class BackupDatabase < CMDx::Task
1541
+ # Default source is :context
1542
+ required :database_name
1543
+ optional :compression_level
1544
+
1545
+ # Explicitly specify context source
1546
+ attribute :backup_path, source: :context
1547
+
1548
+ def work
1549
+ database_name #=> context.database_name
1550
+ backup_path #=> context.backup_path
1551
+ compression_level #=> context.compression_level
1552
+ end
1553
+ end
1554
+ ```
1555
+
1556
+ ### Symbol References
1557
+
1558
+ Reference instance methods by symbol for dynamic source values:
1559
+
1560
+ ```ruby
1561
+ class BackupDatabase < CMDx::Task
1562
+ attributes :host, :credentials, source: :database_config
1563
+
1564
+ # Access from declared attributes
1565
+ attribute :connection_string, source: :credentials
1566
+
1567
+ def work
1568
+ # Your logic here...
1569
+ end
1570
+
1571
+ private
1572
+
1573
+ def database_config
1574
+ @database_config ||= DatabaseConfig.find(context.database_name)
1575
+ end
1576
+ end
1577
+ ```
1578
+
1579
+ ### Proc or Lambda
1580
+
1581
+ Use anonymous functions for dynamic source values:
1582
+
1583
+ ```ruby
1584
+ class BackupDatabase < CMDx::Task
1585
+ # Proc
1586
+ attribute :timestamp, source: proc { Time.current }
1587
+
1588
+ # Lambda
1589
+ attribute :server, source: -> { Current.server }
1590
+ end
1591
+ ```
1592
+
1593
+ ### Class or Module
1594
+
1595
+ For complex source logic, use classes or modules:
1596
+
1597
+ ```ruby
1598
+ class DatabaseResolver
1599
+ def self.call(task)
1600
+ Database.find(task.context.database_name)
1601
+ end
1602
+ end
1603
+
1604
+ class BackupDatabase < CMDx::Task
1605
+ # Class or Module
1606
+ attribute :schema, source: DatabaseResolver
1607
+
1608
+ # Instance
1609
+ attribute :metadata, source: DatabaseResolver.new
1610
+ end
1611
+ ```
1612
+
1613
+ ## Nesting
1614
+
1615
+ Nested attributes enable complex attribute structures where child attributes automatically inherit their parent as the source. This allows validation and access of structured data.
1616
+
1617
+ > [!NOTE]
1618
+ > All options available to top-level attributes are available to nested attributes, eg: naming, coercions, and validations
1619
+
1620
+ ```ruby
1621
+ class ConfigureServer < CMDx::Task
1622
+ # Required parent with required children
1623
+ required :network_config do
1624
+ required :hostname, :port, :protocol, :subnet
1625
+ optional :load_balancer
1626
+ attribute :firewall_rules
1627
+ end
1628
+
1629
+ # Optional parent with conditional children
1630
+ optional :ssl_config do
1631
+ required :certificate_path, :private_key # Only required if ssl_config provided
1632
+ optional :enable_http2, prefix: true
1633
+ end
1634
+
1635
+ # Multi-level nesting
1636
+ attribute :monitoring do
1637
+ required :provider
1638
+
1639
+ optional :alerting do
1640
+ required :threshold_percentage
1641
+ optional :notification_channel
1642
+ end
1643
+ end
1644
+
1645
+ def work
1646
+ network_config #=> { hostname: "api.company.com" ... }
1647
+ hostname #=> "api.company.com"
1648
+ load_balancer #=> nil
1649
+ end
1650
+ end
1651
+
1652
+ ConfigureServer.execute(
1653
+ server_id: "srv-001",
1654
+ network_config: {
1655
+ hostname: "api.company.com",
1656
+ port: 443,
1657
+ protocol: "https",
1658
+ subnet: "10.0.1.0/24",
1659
+ firewall_rules: "allow_web_traffic"
1660
+ },
1661
+ monitoring: {
1662
+ provider: "datadog",
1663
+ alerting: {
1664
+ threshold_percentage: 85.0,
1665
+ notification_channel: "slack"
1666
+ }
1667
+ }
1668
+ )
1669
+ ```
1670
+
1671
+ > [!IMPORTANT]
1672
+ > Child attributes are only required when their parent attribute is provided, enabling flexible optional structures.
1673
+
1674
+ ## Error Handling
1675
+
1676
+ Attribute validation failures result in structured error information with details about each failed attribute.
1677
+
1678
+ > [!NOTE]
1679
+ > Nested attributes are only ever evaluated when the parent attribute is available and valid.
1680
+
1681
+ ```ruby
1682
+ class ConfigureServer < CMDx::Task
1683
+ required :server_id, :environment
1684
+ required :network_config do
1685
+ required :hostname, :port
1686
+ end
1687
+
1688
+ def work
1689
+ # Your logic here...
1690
+ end
1691
+ end
1692
+
1693
+ # Missing required top-level attributes
1694
+ result = ConfigureServer.execute(server_id: "srv-001")
1695
+
1696
+ result.state #=> "interrupted"
1697
+ result.status #=> "failed"
1698
+ result.reason #=> "environment is required. network_config is required."
1699
+ result.metadata #=> {
1700
+ # messages: {
1701
+ # environment: ["is required"],
1702
+ # network_config: ["is required"]
1703
+ # }
1704
+ # }
1705
+
1706
+ # Missing required nested attributes
1707
+ result = ConfigureServer.execute(
1708
+ server_id: "srv-001",
1709
+ environment: "production",
1710
+ network_config: { hostname: "api.company.com" } # Missing port
1711
+ )
1712
+
1713
+ result.state #=> "interrupted"
1714
+ result.status #=> "failed"
1715
+ result.reason #=> "port is required."
1716
+ result.metadata #=> {
1717
+ # messages: {
1718
+ # port: ["is required"]
1719
+ # }
1720
+ # }
1721
+
1722
+ ---
1723
+
1724
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/naming.md
1725
+ ---
1726
+
1727
+ # Attributes - Naming
1728
+
1729
+ Attribute naming provides method name customization to prevent conflicts and enable flexible attribute access patterns. When attributes share names with existing methods or when multiple attributes from different sources have the same name, affixing ensures clean method resolution within tasks.
1730
+
1731
+ > [!NOTE]
1732
+ > Affixing modifies only the generated accessor method names within tasks.
1733
+
1734
+ ## Prefix
1735
+
1736
+ Adds a prefix to the generated accessor method name.
1737
+
1738
+ ```ruby
1739
+ class GenerateReport < CMDx::Task
1740
+ # Dynamic from attribute source
1741
+ attribute :template, prefix: true
1742
+
1743
+ # Static
1744
+ attribute :format, prefix: "report_"
1745
+
1746
+ def work
1747
+ context_template #=> "monthly_sales"
1748
+ report_format #=> "pdf"
1749
+ end
1750
+ end
1751
+
1752
+ # Attributes passed as original attribute names
1753
+ GenerateReport.execute(template: "monthly_sales", format: "pdf")
1754
+ ```
1755
+
1756
+ ## Suffix
1757
+
1758
+ Adds a suffix to the generated accessor method name.
1759
+
1760
+ ```ruby
1761
+ class DeployApplication < CMDx::Task
1762
+ # Dynamic from attribute source
1763
+ attribute :branch, suffix: true
1764
+
1765
+ # Static
1766
+ attribute :version, suffix: "_tag"
1767
+
1768
+ def work
1769
+ branch_context #=> "main"
1770
+ version_tag #=> "v1.2.3"
1771
+ end
1772
+ end
1773
+
1774
+ # Attributes passed as original attribute names
1775
+ DeployApplication.execute(branch: "main", version: "v1.2.3")
1776
+ ```
1777
+
1778
+ ## As
1779
+
1780
+ Completely renames the generated accessor method.
1781
+
1782
+ ```ruby
1783
+ class ScheduleMaintenance < CMDx::Task
1784
+ attribute :scheduled_at, as: :when
1785
+
1786
+ def work
1787
+ when #=> <DateTime>
1788
+ end
1789
+ end
1790
+
1791
+ # Attributes passed as original attribute names
1792
+ ScheduleMaintenance.execute(scheduled_at: DateTime.new(2024, 12, 15, 2, 0, 0))
1793
+ ```
1794
+
1795
+ ---
1796
+
1797
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/coercions.md
1798
+ ---
1799
+
1800
+ # Attributes - Coercions
1801
+
1802
+ Attribute coercions automatically convert task arguments to expected types, ensuring type safety while providing flexible input handling. Coercions transform raw input values into the specified types, supporting simple conversions like string-to-integer and complex operations like JSON parsing.
1803
+
1804
+ ## Usage
1805
+
1806
+ Define attribute types to enable automatic coercion:
1807
+
1808
+ ```ruby
1809
+ class ParseMetrics < CMDx::Task
1810
+ # Coerce into a symbol
1811
+ attribute :measurement_type, type: :symbol
1812
+
1813
+ # Coerce into a rational fallback to big decimal
1814
+ attribute :value, type: [:rational, :big_decimal]
1815
+
1816
+ # Coerce with options
1817
+ attribute :recorded_at, type: :date, strptime: "%m-%d-%Y"
1818
+
1819
+ def work
1820
+ measurement_type #=> :temperature
1821
+ recorded_at #=> <Date 2024-01-23>
1822
+ value #=> 98.6 (Float)
1823
+ end
1824
+ end
1825
+
1826
+ ParseMetrics.execute(
1827
+ measurement_type: "temperature",
1828
+ recorded_at: "01-23-2020",
1829
+ value: "98.6"
1830
+ )
1831
+ ```
1832
+
1833
+ > [!TIP]
1834
+ > Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds.
1835
+
1836
+ ## Built-in Coercions
1837
+
1838
+ | Type | Options | Description | Examples |
1839
+ |------|---------|-------------|----------|
1840
+ | `:array` | | Array conversion with JSON support | `"val"` → `["val"]`<br>`"[1,2,3]"` → `[1, 2, 3]` |
1841
+ | `:big_decimal` | `:precision` | High-precision decimal | `"123.456"` → `BigDecimal("123.456")` |
1842
+ | `:boolean` | | Boolean with text patterns | `"yes"` → `true`, `"no"` → `false` |
1843
+ | `:complex` | | Complex numbers | `"1+2i"` → `Complex(1, 2)` |
1844
+ | `:date` | `:strptime` | Date objects | `"2024-01-23"` → `Date.new(2024, 1, 23)` |
1845
+ | `:datetime` | `:strptime` | DateTime objects | `"2024-01-23 10:30"` → `DateTime.new(2024, 1, 23, 10, 30)` |
1846
+ | `:float` | | Floating-point numbers | `"123.45"` → `123.45` |
1847
+ | `:hash` | | Hash conversion with JSON support | `'{"a":1}'` → `{"a" => 1}` |
1848
+ | `:integer` | | Integer with hex/octal support | `"0xFF"` → `255`, `"077"` → `63` |
1849
+ | `:rational` | | Rational numbers | `"1/2"` → `Rational(1, 2)` |
1850
+ | `:string` | | String conversion | `123` → `"123"` |
1851
+ | `:symbol` | | Symbol conversion | `"abc"` → `:abc` |
1852
+ | `:time` | `:strptime` | Time objects | `"10:30:00"` → `Time.new(2024, 1, 23, 10, 30)` |
1853
+
1854
+ ## Declarations
1855
+
1856
+ > [!IMPORTANT]
1857
+ > Coercions must raise a CMDx::CoercionError and its message is used as part of the fault reason and metadata.
1858
+
1859
+ ### Proc or Lambda
1860
+
1861
+ Use anonymous functions for simple coercion logic:
1862
+
1863
+ ```ruby
1864
+ class TransformCoordinates < CMDx::Task
1865
+ # Proc
1866
+ register :callback, :geolocation, proc do |value, options = {}|
1867
+ begin
1868
+ Geolocation(value)
1869
+ rescue StandardError
1870
+ raise CMDx::CoercionError, "could not convert into a geolocation"
1871
+ end
1872
+ end
1873
+
1874
+ # Lambda
1875
+ register :callback, :geolocation, ->(value, options = {}) {
1876
+ begin
1877
+ Geolocation(value)
1878
+ rescue StandardError
1879
+ raise CMDx::CoercionError, "could not convert into a geolocation"
1880
+ end
1881
+ }
1882
+ end
1883
+ ```
1884
+
1885
+ ### Class or Module
1886
+
1887
+ Register custom coercion logic for specialized type handling:
1888
+
1889
+ ```ruby
1890
+ class GeolocationCoercion
1891
+ def self.call(value, options = {})
1892
+ Geolocation(value)
1893
+ rescue StandardError
1894
+ raise CMDx::CoercionError, "could not convert into a geolocation"
1895
+ end
1896
+ end
1897
+
1898
+ class TransformCoordinates < CMDx::Task
1899
+ register :coercion, :geolocation, GeolocationCoercion
1900
+
1901
+ attribute :latitude, type: :geolocation
1902
+ end
1903
+ ```
1904
+
1905
+ ## Removals
1906
+
1907
+ Remove custom coercions when no longer needed:
1908
+
1909
+ > [!WARNING]
1910
+ > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
1911
+
1912
+ ```ruby
1913
+ class TransformCoordinates < CMDx::Task
1914
+ deregister :coercion, :geolocation
1915
+ end
1916
+ ```
1917
+
1918
+ ## Error Handling
1919
+
1920
+ Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons:
1921
+
1922
+ ```ruby
1923
+ class AnalyzePerformance < CMDx::Task
1924
+ attribute :iterations, type: :integer
1925
+ attribute :score, type: [:float, :big_decimal]
1926
+
1927
+ def work
1928
+ # Your logic here...
1929
+ end
1930
+ end
1931
+
1932
+ result = AnalyzePerformance.execute(
1933
+ iterations: "not-a-number",
1934
+ score: "invalid-float"
1935
+ )
1936
+
1937
+ result.state #=> "interrupted"
1938
+ result.status #=> "failed"
1939
+ result.reason #=> "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal."
1940
+ result.metadata #=> {
1941
+ # messages: {
1942
+ # iterations: ["could not coerce into an integer"],
1943
+ # score: ["could not coerce into one of: float, big_decimal"]
1944
+ # }
1945
+ # }
1946
+
1947
+ ---
1948
+
1949
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/validations.md
1950
+ ---
1951
+
1952
+ # Attributes - Validations
1953
+
1954
+ Attribute validations ensure task arguments meet specified requirements before execution begins. Validations run after coercions and provide declarative rules for data integrity, supporting both built-in validators and custom validation logic.
1955
+
1956
+ ## Usage
1957
+
1958
+ Define validation rules on attributes to enforce data requirements:
1959
+
1960
+ ```ruby
1961
+ class ProcessSubscription < CMDx::Task
1962
+ # Required field with presence validation
1963
+ attribute :user_id, presence: true
1964
+
1965
+ # String with length constraints
1966
+ attribute :preferences, length: { minimum: 10, maximum: 500 }
1967
+
1968
+ # Numeric range validation
1969
+ attribute :tier_level, inclusion: { in: 1..5 }
1970
+
1971
+ # Format validation for email
1972
+ attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
1973
+
1974
+ def work
1975
+ user_id #=> "98765"
1976
+ preferences #=> "Send weekly digest emails"
1977
+ tier_level #=> 3
1978
+ contact_email #=> "user@company.com"
1979
+ end
1980
+ end
1981
+
1982
+ ProcessSubscription.execute(
1983
+ user_id: "98765",
1984
+ preferences: "Send weekly digest emails",
1985
+ tier_level: 3,
1986
+ contact_email: "user@company.com"
1987
+ )
1988
+ ```
1989
+
1990
+ > [!TIP]
1991
+ > Validations run after coercions, so you can validate the final coerced values rather than raw input.
1992
+
1993
+ ## Built-in Validators
1994
+
1995
+ ### Common Options
1996
+
1997
+ This list of options is available to all validators:
1998
+
1999
+ | Option | Description |
2000
+ |--------|-------------|
2001
+ | `:allow_nil` | Skip validation when value is `nil` |
2002
+ | `:if` | Symbol, proc, lambda, or callable determining when to validate |
2003
+ | `:unless` | Symbol, proc, lambda, or callable determining when to skip validation |
2004
+ | `:message` | Custom error message for validation failures |
2005
+
2006
+ ### Exclusion
2007
+
2008
+ ```ruby
2009
+ class ProcessProduct < CMDx::Task
2010
+ attribute :status, exclusion: { in: %w[recalled archived] }
2011
+
2012
+ def work
2013
+ # Your logic here...
2014
+ end
2015
+ end
2016
+ ```
2017
+
2018
+ | Options | Description |
2019
+ |---------|-------------|
2020
+ | `:in` | The collection of forbidden values or range |
2021
+ | `:within` | Alias for :in option |
2022
+ | `:of_message` | Custom message for discrete value exclusions |
2023
+ | `:in_message` | Custom message for range-based exclusions |
2024
+ | `:within_message` | Alias for :in_message option |
2025
+
2026
+ ### Format
2027
+
2028
+ ```ruby
2029
+ class ProcessProduct < CMDx::Task
2030
+ attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
2031
+
2032
+ attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
2033
+
2034
+ def work
2035
+ # Your logic here...
2036
+ end
2037
+ end
2038
+ ```
2039
+
2040
+ | Options | Description |
2041
+ |---------|-------------|
2042
+ | `regexp` | Alias for :with option |
2043
+ | `:with` | Regex pattern that the value must match |
2044
+ | `:without` | Regex pattern that the value must not match |
2045
+
2046
+ ### Inclusion
2047
+
2048
+ ```ruby
2049
+ class ProcessProduct < CMDx::Task
2050
+ attribute :availability, inclusion: { in: %w[available limited] }
2051
+
2052
+ def work
2053
+ # Your logic here...
2054
+ end
2055
+ end
2056
+ ```
2057
+
2058
+ | Options | Description |
2059
+ |---------|-------------|
2060
+ | `:in` | The collection of allowed values or range |
2061
+ | `:within` | Alias for :in option |
2062
+ | `:of_message` | Custom message for discrete value inclusions |
2063
+ | `:in_message` | Custom message for range-based inclusions |
2064
+ | `:within_message` | Alias for :in_message option |
2065
+
2066
+ ### Length
2067
+
2068
+ ```ruby
2069
+ class CreateBlogPost < CMDx::Task
2070
+ attribute :title, length: { within: 5..100 }
2071
+
2072
+ def work
2073
+ # Your logic here...
2074
+ end
2075
+ end
2076
+ ```
2077
+
2078
+ | Options | Description |
2079
+ |---------|-------------|
2080
+ | `:within` | Range that the length must fall within (inclusive) |
2081
+ | `:not_within` | Range that the length must not fall within |
2082
+ | `:in` | Alias for :within |
2083
+ | `:not_in` | Range that the length must not fall within |
2084
+ | `:min` | Minimum allowed length |
2085
+ | `:max` | Maximum allowed length |
2086
+ | `:is` | Exact required length |
2087
+ | `:is_not` | Length that is not allowed |
2088
+ | `:within_message` | Custom message for within/range validations |
2089
+ | `:in_message` | Custom message for :in validation |
2090
+ | `:not_within_message` | Custom message for not_within validation |
2091
+ | `:not_in_message` | Custom message for not_in validation |
2092
+ | `:min_message` | Custom message for minimum length validation |
2093
+ | `:max_message` | Custom message for maximum length validation |
2094
+ | `:is_message` | Custom message for exact length validation |
2095
+ | `:is_not_message` | Custom message for is_not validation |
2096
+
2097
+ ### Numeric
2098
+
2099
+ ```ruby
2100
+ class CreateBlogPost < CMDx::Task
2101
+ attribute :word_count, numeric: { min: 100 }
2102
+
2103
+ def work
2104
+ # Your logic here...
2105
+ end
2106
+ end
2107
+ ```
2108
+
2109
+ | Options | Description |
2110
+ |---------|-------------|
2111
+ | `:within` | Range that the value must fall within (inclusive) |
2112
+ | `:not_within` | Range that the value must not fall within |
2113
+ | `:in` | Alias for :within option |
2114
+ | `:not_in` | Alias for :not_within option |
2115
+ | `:min` | Minimum allowed value (inclusive, >=) |
2116
+ | `:max` | Maximum allowed value (inclusive, <=) |
2117
+ | `:is` | Exact value that must match |
2118
+ | `:is_not` | Value that must not match |
2119
+ | `:within_message` | Custom message for range validations |
2120
+ | `:not_within_message` | Custom message for exclusion validations |
2121
+ | `:min_message` | Custom message for minimum validation |
2122
+ | `:max_message` | Custom message for maximum validation |
2123
+ | `:is_message` | Custom message for exact match validation |
2124
+ | `:is_not_message` | Custom message for exclusion validation |
2125
+
2126
+ ### Presence
2127
+
2128
+ ```ruby
2129
+ class CreateBlogPost < CMDx::Task
2130
+ attribute :content, presence: true
2131
+
2132
+ attribute :content, presence: { message: "cannot be blank" }
2133
+
2134
+ def work
2135
+ # Your logic here...
2136
+ end
2137
+ end
2138
+ ```
2139
+
2140
+ | Options | Description |
2141
+ |---------|-------------|
2142
+ | `true` | Ensures value is not nil, empty string, or whitespace |
2143
+
2144
+ ## Declarations
2145
+
2146
+ > [!IMPORTANT]
2147
+ > Custom validators must raise a `CMDx::ValidationError` and its message is used as part of the fault reason and metadata.
2148
+
2149
+ ### Proc or Lambda
2150
+
2151
+ Use anonymous functions for simple validation logic:
2152
+
2153
+ ```ruby
2154
+ class SetupApplication < CMDx::Task
2155
+ # Proc
2156
+ register :validator, :api_key, proc do |value, options = {}|
2157
+ unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2158
+ raise CMDx::ValidationError, "invalid API key format"
2159
+ end
2160
+ end
2161
+
2162
+ # Lambda
2163
+ register :validator, :api_key, ->(value, options = {}) {
2164
+ unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2165
+ raise CMDx::ValidationError, "invalid API key format"
2166
+ end
2167
+ }
2168
+ end
2169
+ ```
2170
+
2171
+ ### Class or Module
2172
+
2173
+ Register custom validation logic for specialized requirements:
2174
+
2175
+ ```ruby
2176
+ class ApiKeyValidator
2177
+ def self.call(value, options = {})
2178
+ unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2179
+ raise CMDx::ValidationError, "invalid API key format"
2180
+ end
2181
+ end
2182
+ end
2183
+
2184
+ class SetupApplication < CMDx::Task
2185
+ register :validator, :api_key, ApiKeyValidator
2186
+
2187
+ attribute :access_key, api_key: true
2188
+ end
2189
+ ```
2190
+
2191
+ ## Removals
2192
+
2193
+ Remove custom validators when no longer needed:
2194
+
2195
+ > [!WARNING]
2196
+ > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2197
+
2198
+ ```ruby
2199
+ class SetupApplication < CMDx::Task
2200
+ deregister :validator, :api_key
2201
+ end
2202
+ ```
2203
+
2204
+ ## Error Handling
2205
+
2206
+ Validation failures provide detailed error information including attribute paths, validation rules, and specific failure reasons:
2207
+
2208
+ ```ruby
2209
+ class CreateProject < CMDx::Task
2210
+ attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 }
2211
+ attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 }
2212
+ attribute :priority, inclusion: { in: [:low, :medium, :high] }
2213
+ attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
2214
+
2215
+ def work
2216
+ # Your logic here...
2217
+ end
2218
+ end
2219
+
2220
+ result = CreateProject.execute(
2221
+ project_name: "AB", # Too short
2222
+ budget: 500, # Too low
2223
+ priority: :urgent, # Not in allowed list
2224
+ contact_email: "invalid-email" # Invalid format
2225
+ )
2226
+
2227
+ result.state #=> "interrupted"
2228
+ result.status #=> "failed"
2229
+ result.reason #=> "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid."
2230
+ result.metadata #=> {
2231
+ # messages: {
2232
+ # project_name: ["is too short (minimum is 3 characters)"],
2233
+ # budget: ["must be greater than 1000"],
2234
+ # priority: ["is not included in the list"],
2235
+ # contact_email: ["is invalid"]
2236
+ # }
2237
+ # }
2238
+ ```
2239
+
2240
+ ---
2241
+
2242
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/defaults.md
2243
+ ---
2244
+
2245
+ # Attributes - Defaults
2246
+
2247
+ Attribute defaults provide fallback values when arguments are not provided or resolve to `nil`. Defaults ensure tasks have sensible values for optional attributes while maintaining flexibility for callers to override when needed.
2248
+
2249
+ ## Declarations
2250
+
2251
+ Defaults apply when attributes are not provided or resolve to `nil`. They work seamlessly with coercion, validation, and nested attributes.
2252
+
2253
+ ### Static Values
2254
+
2255
+ ```ruby
2256
+ class OptimizeDatabase < CMDx::Task
2257
+ attribute :strategy, default: :incremental
2258
+ attribute :level, default: "basic"
2259
+ attribute :notify_admin, default: true
2260
+ attribute :timeout_minutes, default: 30
2261
+ attribute :indexes, default: []
2262
+ attribute :options, default: {}
2263
+
2264
+ def work
2265
+ strategy #=> :incremental
2266
+ level #=> "basic"
2267
+ notify_admin #=> true
2268
+ timeout_minutes #=> 30
2269
+ indexes #=> []
2270
+ options #=> {}
2271
+ end
2272
+ end
2273
+ ```
2274
+
2275
+ ### Symbol References
2276
+
2277
+ Reference instance methods by symbol for dynamic default values:
2278
+
2279
+ ```ruby
2280
+ class ProcessAnalytics < CMDx::Task
2281
+ attribute :granularity, default: :default_granularity
2282
+
2283
+ def work
2284
+ # Your logic here...
2285
+ end
2286
+
2287
+ private
2288
+
2289
+ def default_granularity
2290
+ Current.user.premium? ? "hourly" : "daily"
2291
+ end
2292
+ end
2293
+ ```
2294
+
2295
+ ### Proc or Lambda
2296
+
2297
+ Use anonymous functions for dynamic default values:
2298
+
2299
+ ```ruby
2300
+ class CacheContent < CMDx::Task
2301
+ # Proc
2302
+ attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 }
2303
+
2304
+ # Lambda
2305
+ attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" }
2306
+ end
2307
+ ```
2308
+
2309
+ ## Coercions and Validations
2310
+
2311
+ Defaults are subject to the same coercion and validation rules as provided values, ensuring consistency and catching configuration errors early.
2312
+
2313
+ ```ruby
2314
+ class ScheduleBackup < CMDx::Task
2315
+ # Coercions
2316
+ attribute :retention_days, default: "7", type: :integer
2317
+
2318
+ # Validations
2319
+ optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] }
2320
+ end
2321
+ ```
2322
+
2323
+ ---
2324
+
2325
+ url: https://github.com/drexed/cmdx/blob/main/docs/callbacks.md
2326
+ ---
2327
+
2328
+ # Callbacks
2329
+
2330
+ Callbacks provide precise control over task execution lifecycle, running custom logic at specific transition points. Callback callables have access to the same context and result information as the `execute` method, enabling rich integration patterns.
2331
+
2332
+ > [!IMPORTANT]
2333
+ > Callbacks execute in the order they are declared within each hook type. Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
2334
+
2335
+ ## Available Callbacks
2336
+
2337
+ Callbacks execute in precise lifecycle order. Here is the complete execution sequence:
2338
+
2339
+ ```ruby
2340
+ 1. before_validation # Pre-validation setup
2341
+ 2. before_execution # Setup and preparation
2342
+
2343
+ # --- Task#work executed ---
2344
+
2345
+ 3. on_[complete|interrupted] # Based on execution state
2346
+ 4. on_executed # Task finished (any outcome)
2347
+ 5. on_[success|skipped|failed] # Based on execution status
2348
+ 6. on_[good|bad] # Based on outcome classification
2349
+ ```
2350
+
2351
+ ## Declarations
2352
+
2353
+ ### Symbol References
2354
+
2355
+ Reference instance methods by symbol for simple callback logic:
2356
+
2357
+ ```ruby
2358
+ class ProcessBooking < CMDx::Task
2359
+ before_execution :find_reservation
2360
+
2361
+ # Batch declarations (works for any type)
2362
+ on_complete :notify_guest, :update_availability
2363
+
2364
+ def work
2365
+ # Your logic here...
2366
+ end
2367
+
2368
+ private
2369
+
2370
+ def find_reservation
2371
+ @reservation ||= Reservation.find(context.reservation_id)
2372
+ end
2373
+
2374
+ def notify_guest
2375
+ GuestNotifier.call(context.guest, result)
2376
+ end
2377
+
2378
+ def update_availability
2379
+ AvailabilityService.update(context.room_ids, result)
2380
+ end
2381
+ end
2382
+ ```
2383
+
2384
+ ### Proc or Lambda
2385
+
2386
+ Use anonymous functions for inline callback logic:
2387
+
2388
+ ```ruby
2389
+ class ProcessBooking < CMDx::Task
2390
+ # Proc
2391
+ on_interrupted proc { |task| ReservationSystem.pause! }
2392
+
2393
+ # Lambda
2394
+ on_complete -> { ReservationSystem.resume! }
2395
+ end
2396
+ ```
2397
+
2398
+ ### Class or Module
2399
+
2400
+ Implement reusable callback logic in dedicated classes:
2401
+
2402
+ ```ruby
2403
+ class BookingConfirmationCallback
2404
+ def call(task)
2405
+ if task.result.success?
2406
+ MessagingApi.send_confirmation(task.context.guest)
2407
+ else
2408
+ MessagingApi.send_issue_alert(task.context.manager)
2409
+ end
2410
+ end
2411
+ end
2412
+
2413
+ class ProcessBooking < CMDx::Task
2414
+ # Class or Module
2415
+ on_success BookingConfirmationCallback
2416
+
2417
+ # Instance
2418
+ on_interrupted BookingConfirmationCallback.new
2419
+ end
2420
+ ```
2421
+
2422
+ ### Conditional Execution
2423
+
2424
+ Control callback execution with conditional logic:
2425
+
2426
+ ```ruby
2427
+ class MessagingPermissionCheck
2428
+ def call(task)
2429
+ task.context.guest.can?(:receive_messages)
2430
+ end
2431
+ end
2432
+
2433
+ class ProcessBooking < CMDx::Task
2434
+ # If and/or Unless
2435
+ before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked?
2436
+
2437
+ # Proc
2438
+ on_failure :increment_failure, if: ->(task) { Rails.env.production? && task.class.name.include?("Legacy") }
2439
+
2440
+ # Lambda
2441
+ on_success :ping_housekeeping, if: proc { |task| task.context.rooms_need_cleaning? }
2442
+
2443
+ # Class or Module
2444
+ on_complete :send_confirmation, unless: MessagingPermissionCheck
2445
+
2446
+ # Instance
2447
+ on_complete :send_confirmation, if: MessagingPermissionCheck.new
2448
+
2449
+ def work
2450
+ # Your logic here...
2451
+ end
2452
+
2453
+ private
2454
+
2455
+ def messaging_enabled?
2456
+ context.guest.messaging_preference.present?
2457
+ end
2458
+
2459
+ def messaging_blocked?
2460
+ context.guest.communication_status == :blocked
2461
+ end
2462
+ end
2463
+ ```
2464
+
2465
+ ## Callback Removal
2466
+
2467
+ Remove callbacks at runtime for dynamic behavior control:
2468
+
2469
+ > [!IMPORTANT]
2470
+ > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2471
+
2472
+ ```ruby
2473
+ class ProcessBooking < CMDx::Task
2474
+ # Symbol
2475
+ deregister :callback, :before_execution, :notify_guest
2476
+
2477
+ # Class or Module (no instances)
2478
+ deregister :callback, :on_complete, BookingConfirmationCallback
2479
+ end
2480
+ ```
2481
+
2482
+ ---
2483
+
2484
+ url: https://github.com/drexed/cmdx/blob/main/docs/middlewares.md
2485
+ ---
2486
+
2487
+ # Middlewares
2488
+
2489
+ Middleware provides Rack-style wrappers around task execution for cross-cutting concerns like authentication, logging, caching, and error handling.
2490
+
2491
+ ## Order
2492
+
2493
+ Middleware executes in a nested fashion, creating an onion-like execution pattern:
2494
+
2495
+ > [!NOTE]
2496
+ > Middleware executes in the order they are registered, with the first registered middleware being the outermost wrapper.
2497
+
2498
+ ```ruby
2499
+ class ProcessCampaign < CMDx::Task
2500
+ register :middleware, AuditMiddleware # 1st: outermost wrapper
2501
+ register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
2502
+ register :middleware, CacheMiddleware # 3rd: innermost wrapper
2503
+
2504
+ def work
2505
+ # Your logic here...
2506
+ end
2507
+ end
2508
+
2509
+ # Execution flow:
2510
+ # 1. AuditMiddleware (before)
2511
+ # 2. AuthorizationMiddleware (before)
2512
+ # 3. CacheMiddleware (before)
2513
+ # 4. [task execution]
2514
+ # 5. CacheMiddleware (after)
2515
+ # 6. AuthorizationMiddleware (after)
2516
+ # 7. AuditMiddleware (after)
2517
+ ```
2518
+
2519
+ ## Declarations
2520
+
2521
+ ### Proc or Lambda
2522
+
2523
+ Use anonymous functions for simple middleware logic:
2524
+
2525
+ ```ruby
2526
+ class ProcessCampaign < CMDx::Task
2527
+ # Proc
2528
+ register :middleware, proc do |task, options, &block|
2529
+ result = block.call
2530
+ Analytics.track(result.status)
2531
+ result
2532
+ end
2533
+
2534
+ # Lambda
2535
+ register :middleware, ->(task, options, &block) {
2536
+ result = block.call
2537
+ Analytics.track(result.status)
2538
+ result
2539
+ }
2540
+ end
2541
+ ```
2542
+
2543
+ ### Class or Module
2544
+
2545
+ For complex middleware logic, use classes or modules:
2546
+
2547
+ ```ruby
2548
+ class TelemetryMiddleware
2549
+ def call(task, options)
2550
+ result = yield
2551
+ Telemetry.record(result.status)
2552
+ ensure
2553
+ result # Always return result
2554
+ end
2555
+ end
2556
+
2557
+ class ProcessCampaign < CMDx::Task
2558
+ # Class or Module
2559
+ register :middleware, TelemetryMiddleware
2560
+
2561
+ # Instance
2562
+ register :middleware, TelemetryMiddleware.new
2563
+
2564
+ # With options
2565
+ register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
2566
+ register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
2567
+ end
2568
+ ```
2569
+
2570
+ ## Removals
2571
+
2572
+ Class and Module based declarations can be removed at a global and task level.
2573
+
2574
+ > [!WARNING]
2575
+ > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2576
+
2577
+ ```ruby
2578
+ class ProcessCampaign < CMDx::Task
2579
+ # Class or Module (no instances)
2580
+ deregister :middleware, TelemetryMiddleware
2581
+ end
2582
+ ```
2583
+
2584
+ ## Built-in
2585
+
2586
+ ### Timeout
2587
+
2588
+ Ensures task execution doesn't exceed a specified time limit:
2589
+
2590
+ ```ruby
2591
+ class ProcessReport < CMDx::Task
2592
+ # Default timeout: 3 seconds
2593
+ register :middleware, CMDx::Middlewares::Timeout
2594
+
2595
+ # Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
2596
+ register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
2597
+
2598
+ # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2599
+ register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
2600
+
2601
+ def work
2602
+ # Your logic here...
2603
+ end
2604
+
2605
+ private
2606
+
2607
+ def max_processing_time
2608
+ Rails.env.production? ? 2 : 10
2609
+ end
2610
+ end
2611
+
2612
+ # Slow task
2613
+ result = ProcessReport.execute
2614
+
2615
+ result.state #=> "interrupted"
2616
+ result.status #=> "failure"
2617
+ result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
2618
+ result.cause #=> <CMDx::TimeoutError>
2619
+ result.metadata #=> { limit: 3 }
2620
+ ```
2621
+
2622
+ ### Correlate
2623
+
2624
+ Tags tasks with a global correlation ID for distributed tracing:
2625
+
2626
+ ```ruby
2627
+ class ProcessExport < CMDx::Task
2628
+ # Default correlation ID generation
2629
+ register :middleware, CMDx::Middlewares::Correlate
2630
+
2631
+ # Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
2632
+ register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
2633
+
2634
+ # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2635
+ register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
2636
+
2637
+ def work
2638
+ # Your logic here...
2639
+ end
2640
+
2641
+ private
2642
+
2643
+ def correlation_enabled?
2644
+ ENV["CORRELATION_ENABLED"] == "true"
2645
+ end
2646
+ end
2647
+
2648
+ result = ProcessExport.execute
2649
+ result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
2650
+ ```
2651
+
2652
+ ### Runtime
2653
+
2654
+ The runtime middleware tags tasks with how long it took to execute the task.
2655
+ The calculation uses a monotonic clock and the time is returned in milliseconds.
2656
+
2657
+ ```ruby
2658
+ class PerformanceMonitoringCheck
2659
+ def call(task)
2660
+ task.context.tenant.monitoring_enabled?
2661
+ end
2662
+ end
2663
+
2664
+ class ProcessExport < CMDx::Task
2665
+ # Default timeout is 3 seconds
2666
+ register :middleware, CMDx::Middlewares::Runtime
2667
+
2668
+ # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2669
+ register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
2670
+ end
2671
+
2672
+ result = ProcessExport.execute
2673
+ result.metadata #=> { runtime: 1247 } (ms)
2674
+ ```
2675
+
2676
+ ---
2677
+
2678
+ url: https://github.com/drexed/cmdx/blob/main/docs/logging.md
2679
+ ---
2680
+
2681
+ # Logging
2682
+
2683
+ CMDx provides comprehensive automatic logging for task execution with structured data, customizable formatters, and intelligent severity mapping. All task results are logged after completion with rich metadata for debugging and monitoring.
2684
+
2685
+ ## Formatters
2686
+
2687
+ CMDx supports multiple log formatters to integrate with various logging systems:
2688
+
2689
+ | Formatter | Use Case | Output Style |
2690
+ |-----------|----------|--------------|
2691
+ | `Line` | Traditional logging | Single-line format |
2692
+ | `Json` | Structured systems | Compact JSON |
2693
+ | `KeyValue` | Log parsing | `key=value` pairs |
2694
+ | `Logstash` | ELK stack | JSON with @version/@timestamp |
2695
+ | `Raw` | Minimal output | Message content only |
2696
+
2697
+ Sample output:
2698
+
2699
+ ```log
2700
+ <!-- Success (INFO level) -->
2701
+ I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
2702
+ index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task"
2703
+ class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
2704
+
2705
+ <!-- Skipped (WARN level) -->
2706
+ W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
2707
+ index=1 state="interrupted" status="skipped" reason="Customer already validated"
2708
+
2709
+ <!-- Failed (ERROR level) -->
2710
+ E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
2711
+ index=2 state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
2712
+
2713
+ <!-- Failed Chain -->
2714
+ E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
2715
+ caused_failure={index: 2, class: "CalculateTax", status: "failed"}
2716
+ threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
2717
+ ```
2718
+
2719
+ > [!TIP]
2720
+ > Logging can be used as low-level eventing system, ingesting all tasks performed within a small action or long running request. This ie where correlation is especially handy.
2721
+
2722
+ ## Structure
2723
+
2724
+ All log entries include comprehensive execution metadata. Field availability depends on execution context and outcome.
2725
+
2726
+ ### Core Fields
2727
+
2728
+ | Field | Description | Example |
2729
+ |-------|-------------|---------|
2730
+ | `severity` | Log level | `INFO`, `WARN`, `ERROR` |
2731
+ | `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
2732
+ | `pid` | Process ID | `3784` |
2733
+
2734
+ ### Task Information
2735
+
2736
+ | Field | Description | Example |
2737
+ |-------|-------------|---------|
2738
+ | `index` | Execution sequence position | `0`, `1`, `2` |
2739
+ | `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
2740
+ | `type` | Execution unit type | `Task`, `Workflow` |
2741
+ | `class` | Task class name | `GenerateInvoiceTask` |
2742
+ | `id` | Unique task instance ID | `018c2b95-b764-7615...` |
2743
+ | `tags` | Custom categorization | `["billing", "financial"]` |
2744
+
2745
+ ### Execution Data
2746
+
2747
+ | Field | Description | Example |
2748
+ |-------|-------------|---------|
2749
+ | `state` | Lifecycle state | `complete`, `interrupted` |
2750
+ | `status` | Business outcome | `success`, `skipped`, `failed` |
2751
+ | `outcome` | Final classification | `success`, `interrupted` |
2752
+ | `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
2753
+
2754
+ ### Failure Chain
2755
+
2756
+ | Field | Description |
2757
+ |-------|-------------|
2758
+ | `reason` | Reason given for the stoppage |
2759
+ | `caused` | Cause exception details |
2760
+ | `caused_failure` | Original failing task details |
2761
+ | `threw_failure` | Task that propagated the failure |
2762
+
2763
+ ## Usage
2764
+
2765
+ Tasks have access to the frameworks logger.
2766
+
2767
+ ```ruby
2768
+ class ProcessSubscription < CMDx::Task
2769
+ def work
2770
+ logger.debug { "Activated feature flags: #{Features.active_flags}" }
2771
+ # Your logic here...
2772
+ logger.info("Subscription processed")
2773
+ end
2774
+ end
2775
+ ```
2776
+
2777
+ ---
2778
+
2779
+ url: https://github.com/drexed/cmdx/blob/main/docs/internationalization.md
2780
+ ---
2781
+
2782
+ # Internationalization (i18n)
2783
+
2784
+ CMDx provides comprehensive internationalization support for all error messages, attribute validation failures, coercion errors, and fault messages. All user-facing text is automatically localized based on the current `I18n.locale`, ensuring your applications can serve global audiences with native-language error reporting.
2785
+
2786
+ ## Localization
2787
+
2788
+ > [!NOTE]
2789
+ > CMDx automatically localizes all error messages based on the `I18n.locale` setting.
2790
+
2791
+ ```ruby
2792
+ class ProcessQuote < CMDx::Task
2793
+ attribute :price, type: :float
2794
+
2795
+ def work
2796
+ # Your logic here...
2797
+ end
2798
+ end
2799
+
2800
+ I18n.with_locale(:fr) do
2801
+ result = ProcessQuote.execute(price: "invalid")
2802
+ result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
2803
+ end
2804
+ ```
2805
+
2806
+ ---
2807
+
2808
+ url: https://github.com/drexed/cmdx/blob/main/docs/deprecation.md
2809
+ ---
2810
+
2811
+ # Task Deprecation
2812
+
2813
+ Task deprecation provides a systematic approach to managing legacy tasks in CMDx applications. The deprecation system enables controlled migration paths by issuing warnings, logging messages, or preventing execution of deprecated tasks entirely, helping teams maintain code quality while providing clear upgrade paths.
2814
+
2815
+ ## Modes
2816
+
2817
+ ### Raise
2818
+
2819
+ `:raise` mode prevents task execution entirely. Use this for tasks that should no longer be used under any circumstances.
2820
+
2821
+ > [!WARNING]
2822
+ > Use `:raise` mode carefully in production environments as it will break existing workflows immediately.
2823
+
2824
+ ```ruby
2825
+ class ProcessObsoleteAPI < CMDx::Task
2826
+ settings(deprecated: :raise)
2827
+
2828
+ def work
2829
+ # Will never execute...
2830
+ end
2831
+ end
2832
+
2833
+ result = ProcessObsoleteAPI.execute
2834
+ #=> raises CMDx::DeprecationError: "ProcessObsoleteAPI usage prohibited"
2835
+ ```
2836
+
2837
+ ### Log
2838
+
2839
+ `:log` mode allows continued usage while tracking deprecation warnings. Perfect for gradual migration scenarios where immediate replacement isn't feasible.
2840
+
2841
+ ```ruby
2842
+ class ProcessLegacyFormat < CMDx::Task
2843
+ settings(deprecated: :log)
2844
+
2845
+ # Same
2846
+ settings(deprecated: true)
2847
+
2848
+ def work
2849
+ # Executes but logs deprecation warning...
2850
+ end
2851
+ end
2852
+
2853
+ result = ProcessLegacyFormat.execute
2854
+ result.successful? #=> true
2855
+
2856
+ # Deprecation warning appears in logs:
2857
+ # WARN -- : DEPRECATED: ProcessLegacyFormat - migrate to replacement or discontinue use
2858
+ ```
2859
+
2860
+ ### Warn
2861
+
2862
+ `:warn` mode issues Ruby warnings visible in development and testing environments. Useful for alerting developers without affecting production logging.
2863
+
2864
+ ```ruby
2865
+ class ProcessOldData < CMDx::Task
2866
+ settings(deprecated: :warn)
2867
+
2868
+ def work
2869
+ # Executes but emits Ruby warning...
2870
+ end
2871
+ end
2872
+
2873
+ result = ProcessOldData.execute
2874
+ result.successful? #=> true
2875
+
2876
+ # Ruby warning appears in stderr:
2877
+ # [ProcessOldData] DEPRECATED: migrate to replacement or discontinue use
2878
+ ```
2879
+
2880
+ ## Declarations
2881
+
2882
+ ### Symbol or String
2883
+
2884
+ ```ruby
2885
+ class OutdatedConnector < CMDx::Task
2886
+ # Symbol
2887
+ settings(deprecated: :raise)
2888
+
2889
+ # String
2890
+ settings(deprecated: "warn")
2891
+ end
2892
+ ```
2893
+
2894
+ ### Boolean or Nil
2895
+
2896
+ ```ruby
2897
+ class OutdatedConnector < CMDx::Task
2898
+ # Deprecates with default :log mode
2899
+ settings(deprecated: true)
2900
+
2901
+ # Skips deprecation
2902
+ settings(deprecated: false)
2903
+ settings(deprecated: nil)
2904
+ end
2905
+ ```
2906
+
2907
+ ### Method
2908
+
2909
+ ```ruby
2910
+ class OutdatedConnector < CMDx::Task
2911
+ # Symbol
2912
+ settings(deprecated: :deprecated?)
2913
+
2914
+ def work
2915
+ # Your logic here...
2916
+ end
2917
+
2918
+ private
2919
+
2920
+ def deprecated?
2921
+ Time.now.year > 2024 ? :raise : false
2922
+ end
2923
+ end
2924
+ ```
2925
+
2926
+ ### Proc or Lambda
2927
+
2928
+ ```ruby
2929
+ class OutdatedConnector < CMDx::Task
2930
+ # Proc
2931
+ settings(deprecated: proc { Rails.env.development? ? :raise : :log })
2932
+
2933
+ # Lambda
2934
+ settings(deprecated: -> { Current.tenant.legacy_mode? ? :warn : :raise })
2935
+ end
2936
+ ```
2937
+
2938
+ ### Class or Module
2939
+
2940
+ ```ruby
2941
+ class OutdatedTaskDeprecator
2942
+ def call(task)
2943
+ task.class.name.include?("Outdated")
2944
+ end
2945
+ end
2946
+
2947
+ class OutdatedConnector < CMDx::Task
2948
+ # Class or Module
2949
+ settings(deprecated: OutdatedTaskDeprecator)
2950
+
2951
+ # Instance
2952
+ settings(deprecated: OutdatedTaskDeprecator.new)
2953
+ end
2954
+ ```
2955
+
2956
+ ---
2957
+
2958
+ url: https://github.com/drexed/cmdx/blob/main/docs/workflows.md
2959
+ ---
2960
+
2961
+ # Workflows
2962
+
2963
+ Workflow orchestrates sequential execution of multiple tasks in a linear pipeline. Workflows provide a declarative DSL for composing complex business logic from individual task components, with support for conditional execution, context propagation, and configurable halt behavior.
2964
+
2965
+ ## Declarations
2966
+
2967
+ Tasks execute in declaration order (FIFO). The workflow context propagates to each task, allowing access to data from previous executions.
2968
+
2969
+ > [!IMPORTANT]
2970
+ > Do **NOT** define a `work` method in workflow tasks. The included module automatically provides the execution logic.
2971
+
2972
+ ### Task
2973
+
2974
+ ```ruby
2975
+ class OnboardingWorkflow < CMDx::Task
2976
+ include CMDx::Workflow
2977
+
2978
+ task CreateUserProfile
2979
+ task SetupAccountPreferences
2980
+
2981
+ tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
2982
+ end
2983
+ ```
2984
+
2985
+ ### Group
2986
+
2987
+ Group related tasks for better organization and shared configuration:
2988
+
2989
+ > [!IMPORTANT]
2990
+ > Settings and conditionals for a group apply to all tasks within that group.
2991
+
2992
+ ```ruby
2993
+ class ContentModerationWorkflow < CMDx::Task
2994
+ include CMDx::Workflow
2995
+
2996
+ # Screening phase
2997
+ tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
2998
+
2999
+ # Review phase
3000
+ tasks ApplyFilters, ScoreContent, FlagSuspicious
3001
+
3002
+ # Decision phase
3003
+ tasks PublishContent, QueueForReview, NotifyModerators
3004
+ end
3005
+ ```
3006
+
3007
+ ### Conditionals
3008
+
3009
+ Conditionals support multiple syntaxes for flexible execution control:
3010
+
3011
+ ```ruby
3012
+ class ContentAccessCheck
3013
+ def call(task)
3014
+ task.context.user.can?(:publish_content)
3015
+ end
3016
+ end
3017
+
3018
+ class OnboardingWorkflow < CMDx::Task
3019
+ include CMDx::Workflow
3020
+
3021
+ # If and/or Unless
3022
+ task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
3023
+
3024
+ # Proc
3025
+ task SendWelcomeEmail, if: ->(workflow) { Rails.env.production? && workflow.class.name.include?("Premium") }
3026
+
3027
+ # Lambda
3028
+ task SendWelcomeEmail, if: proc { |workflow| workflow.context.features_enabled? }
3029
+
3030
+ # Class or Module
3031
+ task SendWelcomeEmail, unless: ContentAccessCheck
3032
+
3033
+ # Instance
3034
+ task SendWelcomeEmail, if: ContentAccessCheck.new
3035
+
3036
+ # Conditional applies to all tasks of this declaration group
3037
+ tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
3038
+
3039
+ private
3040
+
3041
+ def email_configured?
3042
+ context.user.email_address.present?
3043
+ end
3044
+
3045
+ def email_disabled?
3046
+ context.user.communication_preference == :disabled
3047
+ end
3048
+ end
3049
+ ```
3050
+
3051
+ ## Halt Behavior
3052
+
3053
+ By default skipped tasks are considered no-op executions and does not stop workflow execution.
3054
+ This is configurable via global and task level breakpoint settings. Task and group configurations
3055
+ can be used together within a workflow.
3056
+
3057
+ ```ruby
3058
+ class AnalyticsWorkflow < CMDx::Task
3059
+ include CMDx::Workflow
3060
+
3061
+ task CollectMetrics # If fails → workflow stops
3062
+ task FilterOutliers # If skipped → workflow continues
3063
+ task GenerateDashboard # Only runs if no failures occurred
3064
+ end
3065
+ ```
3066
+
3067
+ ### Task Configuration
3068
+
3069
+ Configure halt behavior for the entire workflow:
3070
+
3071
+ ```ruby
3072
+ class SecurityWorkflow < CMDx::Task
3073
+ include CMDx::Workflow
3074
+
3075
+ # Halt on both failed and skipped results
3076
+ settings(workflow_breakpoints: ["skipped", "failed"])
3077
+
3078
+ task PerformSecurityScan
3079
+ task ValidateSecurityRules
3080
+ end
3081
+
3082
+ class OptionalTasksWorkflow < CMDx::Task
3083
+ include CMDx::Workflow
3084
+
3085
+ # Never halt, always continue
3086
+ settings(breakpoints: [])
3087
+
3088
+ task TryBackupData
3089
+ task TryCleanupLogs
3090
+ task TryOptimizeCache
3091
+ end
3092
+ ```
3093
+
3094
+ ### Group Configuration
3095
+
3096
+ Different task groups can have different halt behavior:
3097
+
3098
+ ```ruby
3099
+ class SubscriptionWorkflow < CMDx::Task
3100
+ include CMDx::Workflow
3101
+
3102
+ task CreateSubscription, ValidatePayment, workflow_breakpoints: ["skipped", "failed"]
3103
+
3104
+ # Never halt, always continue
3105
+ task SendConfirmationEmail, UpdateBilling, breakpoints: []
3106
+ end
3107
+ ```
3108
+
3109
+ ## Nested Workflows
3110
+
3111
+ Workflows can task other workflows for hierarchical composition:
3112
+
3113
+ ```ruby
3114
+ class EmailPreparationWorkflow < CMDx::Task
3115
+ include CMDx::Workflow
3116
+
3117
+ task ValidateRecipients
3118
+ task CompileTemplate
3119
+ end
3120
+
3121
+ class EmailDeliveryWorkflow < CMDx::Task
3122
+ include CMDx::Workflow
3123
+
3124
+ tasks SendEmails, TrackDeliveries
3125
+ end
3126
+
3127
+ class CompleteEmailWorkflow < CMDx::Task
3128
+ include CMDx::Workflow
3129
+
3130
+ task EmailPreparationWorkflow
3131
+ task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
3132
+ task GenerateDeliveryReport
3133
+ end
3134
+ ```
3135
+
3136
+ ---
3137
+
3138
+ url: https://github.com/drexed/cmdx/blob/main/docs/tips_and_tricks.md
3139
+ ---
3140
+
3141
+ # Tips and Tricks
3142
+
3143
+ This guide covers advanced patterns and optimization techniques for getting the most out of CMDx in production applications.
3144
+
3145
+ ## Project Organization
3146
+
3147
+ ### Directory Structure
3148
+
3149
+ Create a well-organized command structure for maintainable applications:
3150
+
3151
+ ```text
3152
+ /app/
3153
+ └── /tasks/
3154
+ ├── /invoices/
3155
+ │ ├── calculate_tax.rb
3156
+ │ ├── validate_invoice.rb
3157
+ │ ├── send_invoice.rb
3158
+ │ └── process_invoice.rb # workflow
3159
+ ├── /reports/
3160
+ │ ├── generate_pdf.rb
3161
+ │ ├── compile_data.rb
3162
+ │ ├── export_csv.rb
3163
+ │ └── create_reports.rb # workflow
3164
+ ├── application_task.rb # base class
3165
+ ├── authenticate_session.rb
3166
+ └── activate_account.rb
3167
+ ```
3168
+
3169
+ ### Naming Conventions
3170
+
3171
+ Follow consistent naming patterns for clarity and maintainability:
3172
+
3173
+ ```ruby
3174
+ # Verb + Noun
3175
+ class ExportData < CMDx::Task; end
3176
+ class CompressFile < CMDx::Task; end
3177
+ class ValidateSchema < CMDx::Task; end
3178
+
3179
+ # Use present tense verbs for actions
3180
+ class GenerateToken < CMDx::Task; end # ✓ Good
3181
+ class GeneratingToken < CMDx::Task; end # ❌ Avoid
3182
+ class TokenGeneration < CMDx::Task; end # ❌ Avoid
3183
+ ```
3184
+
3185
+ ### Story Telling
3186
+
3187
+ Consider using descriptive methods to express the task’s flow, rather than concentrating all logic inside the `work` method.
3188
+
3189
+ ```ruby
3190
+ class ProcessOrder < CMDx::Task
3191
+ def work
3192
+ charge_payment_method
3193
+ assign_to_warehouse
3194
+ send_notification
3195
+ end
3196
+
3197
+ private
3198
+
3199
+ def charge_payment_method
3200
+ order.primary_payment_method.charge!
3201
+ end
3202
+
3203
+ def assign_to_warehouse
3204
+ order.ready_for_shipping!
3205
+ end
3206
+
3207
+ def send_notification
3208
+ if order.products_out_of_stock?
3209
+ OrderMailer.pending(order).deliver
3210
+ else
3211
+ OrderMailer.preparing(order).deliver
3212
+ end
3213
+ end
3214
+ end
3215
+ ```
3216
+
3217
+ ### Style Guide
3218
+
3219
+ Follow a style pattern for consistent task design:
3220
+
3221
+ ```ruby
3222
+ class ExportReport < CMDx::Task
3223
+
3224
+ # 1. Register functions
3225
+ register :middleware, CMDx::Middlewares::Correlate
3226
+ register :validator, :format, FormatValidator
3227
+
3228
+ # 2. Define callbacks
3229
+ before_execution :find_report
3230
+ on_complete :track_export_metrics, if: ->(task) { Current.tenant.analytics? }
3231
+
3232
+ # 3. Declare attributes
3233
+ attributes :user_id
3234
+ required :report_id
3235
+ optional :format_type
3236
+
3237
+ # 4. Define work method
3238
+ def work
3239
+ report.compile!
3240
+ report.export!
3241
+
3242
+ context.exported_at = Time.now
3243
+ end
3244
+
3245
+ # TIP: Favor private business logic to reduce the surface of the public API.
3246
+ private
3247
+
3248
+ # 5. Build helper functions
3249
+ def find_report
3250
+ @report ||= Report.find(report_id)
3251
+ end
3252
+
3253
+ def track_export_metrics
3254
+ Analytics.increment(:report_exported)
3255
+ end
3256
+
3257
+ end
3258
+ ```
3259
+
3260
+ ## Attribute Options
3261
+
3262
+ Use Rails `with_options` to reduce duplication and improve readability:
3263
+
3264
+ ```ruby
3265
+ class ConfigureCompany < CMDx::Task
3266
+ # Apply common options to multiple attributes
3267
+ with_options(type: :string, presence: true) do
3268
+ attributes :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
3269
+ required :company_name, :industry
3270
+ optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
3271
+ end
3272
+
3273
+ # Nested attributes with shared prefix
3274
+ required :headquarters do
3275
+ with_options(prefix: :hq_) do
3276
+ attributes :street, :city, :zip_code, type: :string
3277
+ required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
3278
+ optional :region, type: :string
3279
+ end
3280
+ end
3281
+
3282
+ def work
3283
+ # Your logic here...
3284
+ end
3285
+ end
3286
+ ```
3287
+
3288
+ ## ActiveRecord Query Tagging
3289
+
3290
+ Automatically tag SQL queries for better debugging:
3291
+
3292
+ ```ruby
3293
+ # config/application.rb
3294
+ config.active_record.query_log_tags_enabled = true
3295
+ config.active_record.query_log_tags << :cmdx_task_class
3296
+ config.active_record.query_log_tags << :cmdx_chain_id
3297
+
3298
+ # app/tasks/application_task.rb
3299
+ class ApplicationTask < CMDx::Task
3300
+ before_execution :set_execution_context
3301
+
3302
+ private
3303
+
3304
+ def set_execution_context
3305
+ # NOTE: This could easily be made into a middleware
3306
+ ActiveSupport::ExecutionContext.set(
3307
+ cmdx_task_class: self.class.name,
3308
+ cmdx_chain_id: chain.id
3309
+ )
3310
+ end
3311
+ end
3312
+
3313
+ # SQL queries will now include comments like:
3314
+ # /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
3315
+ ```
3316
+
3317
+ ---