cmdx 1.13.0 → 1.14.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -76
  3. data/LICENSE.txt +3 -20
  4. data/README.md +8 -7
  5. data/lib/cmdx/attribute.rb +21 -5
  6. data/lib/cmdx/context.rb +16 -0
  7. data/lib/cmdx/executor.rb +9 -9
  8. data/lib/cmdx/result.rb +27 -7
  9. data/lib/cmdx/task.rb +19 -0
  10. data/lib/cmdx/version.rb +1 -1
  11. data/mkdocs.yml +62 -36
  12. metadata +3 -57
  13. data/.cursor/prompts/docs.md +0 -12
  14. data/.cursor/prompts/llms.md +0 -8
  15. data/.cursor/prompts/rspec.md +0 -24
  16. data/.cursor/prompts/yardoc.md +0 -15
  17. data/.cursor/rules/cursor-instructions.mdc +0 -68
  18. data/.irbrc +0 -18
  19. data/.rspec +0 -4
  20. data/.rubocop.yml +0 -95
  21. data/.ruby-version +0 -1
  22. data/.yard-lint.yml +0 -174
  23. data/.yardopts +0 -7
  24. data/docs/.DS_Store +0 -0
  25. data/docs/assets/favicon.ico +0 -0
  26. data/docs/assets/favicon.svg +0 -1
  27. data/docs/attributes/coercions.md +0 -155
  28. data/docs/attributes/defaults.md +0 -77
  29. data/docs/attributes/definitions.md +0 -283
  30. data/docs/attributes/naming.md +0 -68
  31. data/docs/attributes/transformations.md +0 -63
  32. data/docs/attributes/validations.md +0 -336
  33. data/docs/basics/chain.md +0 -108
  34. data/docs/basics/context.md +0 -121
  35. data/docs/basics/execution.md +0 -152
  36. data/docs/basics/setup.md +0 -107
  37. data/docs/callbacks.md +0 -157
  38. data/docs/configuration.md +0 -314
  39. data/docs/deprecation.md +0 -143
  40. data/docs/getting_started.md +0 -137
  41. data/docs/index.md +0 -134
  42. data/docs/internationalization.md +0 -126
  43. data/docs/interruptions/exceptions.md +0 -52
  44. data/docs/interruptions/faults.md +0 -169
  45. data/docs/interruptions/halt.md +0 -216
  46. data/docs/logging.md +0 -90
  47. data/docs/middlewares.md +0 -191
  48. data/docs/outcomes/result.md +0 -197
  49. data/docs/outcomes/states.md +0 -66
  50. data/docs/outcomes/statuses.md +0 -65
  51. data/docs/retries.md +0 -121
  52. data/docs/stylesheets/extra.css +0 -42
  53. data/docs/tips_and_tricks.md +0 -157
  54. data/docs/workflows.md +0 -226
  55. data/examples/active_record_database_transaction.md +0 -27
  56. data/examples/active_record_query_tagging.md +0 -46
  57. data/examples/flipper_feature_flags.md +0 -50
  58. data/examples/paper_trail_whatdunnit.md +0 -39
  59. data/examples/redis_idempotency.md +0 -71
  60. data/examples/sentry_error_tracking.md +0 -46
  61. data/examples/sidekiq_async_execution.md +0 -29
  62. data/examples/stoplight_circuit_breaker.md +0 -36
  63. data/src/cmdx-dark-logo.png +0 -0
  64. data/src/cmdx-favicon.svg +0 -1
  65. data/src/cmdx-light-logo.png +0 -0
  66. data/src/cmdx-logo.svg +0 -1
@@ -1,336 +0,0 @@
1
- # Attributes - Validations
2
-
3
- Ensure inputs meet requirements before execution. Validations run after coercions, giving you declarative data integrity checks.
4
-
5
- See [Global Configuration](../getting_started.md#validators) for custom validator setup.
6
-
7
- ## Usage
8
-
9
- Define validation rules on attributes to enforce data requirements:
10
-
11
- ```ruby
12
- class ProcessSubscription < CMDx::Task
13
- # Required field with presence validation
14
- attribute :user_id, presence: true
15
-
16
- # String with length constraints
17
- optional :preferences, length: { minimum: 10, maximum: 500 }
18
-
19
- # Numeric range validation
20
- required :tier_level, inclusion: { in: 1..5 }
21
-
22
- # Format validation for email
23
- attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
24
-
25
- def work
26
- user_id #=> "98765"
27
- preferences #=> "Send weekly digest emails"
28
- tier_level #=> 3
29
- contact_email #=> "user@company.com"
30
- end
31
- end
32
-
33
- ProcessSubscription.execute(
34
- user_id: "98765",
35
- preferences: "Send weekly digest emails",
36
- tier_level: 3,
37
- contact_email: "user@company.com"
38
- )
39
- ```
40
-
41
- !!! tip
42
-
43
- Validations run after coercions, so you can validate the final coerced values rather than raw input.
44
-
45
- ## Built-in Validators
46
-
47
- ### Common Options
48
-
49
- ```ruby
50
- class ProcessProduct < CMDx::Task
51
- # Allow nil
52
- attribute :tier_level, inclusion: {
53
- in: 1..5,
54
- allow_nil: true
55
- }
56
-
57
- # Conditionals
58
- optional :contact_email, format: {
59
- with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
60
- if: ->(value) { value.includes?("@") }
61
- }
62
- required :status, exclusion: {
63
- in: %w[recalled archived],
64
- unless: :product_sunsetted?
65
- }
66
-
67
- # Custom message
68
- attribute :title, length: {
69
- within: 5..100,
70
- message: "must be in optimal size"
71
- }
72
-
73
- def work
74
- # Your logic here...
75
- end
76
-
77
- private
78
-
79
- def product_defunct?(value)
80
- context.company.out_of_business? || value == "deprecated"
81
- end
82
- end
83
- ```
84
-
85
- This list of options is available to all validators:
86
-
87
- | Option | Description |
88
- |--------|-------------|
89
- | `:allow_nil` | Skip validation when value is `nil` |
90
- | `:if` | Symbol, proc, lambda, or callable determining when to validate |
91
- | `:unless` | Symbol, proc, lambda, or callable determining when to skip validation |
92
- | `:message` | Custom error message for validation failures |
93
-
94
- ### Exclusion
95
-
96
- ```ruby
97
- class ProcessProduct < CMDx::Task
98
- attribute :status, exclusion: { in: %w[recalled archived] }
99
-
100
- def work
101
- # Your logic here...
102
- end
103
- end
104
- ```
105
-
106
- | Options | Description |
107
- |---------|-------------|
108
- | `:in` | The collection of forbidden values or range |
109
- | `:within` | Alias for :in option |
110
- | `:of_message` | Custom message for discrete value exclusions |
111
- | `:in_message` | Custom message for range-based exclusions |
112
- | `:within_message` | Alias for :in_message option |
113
-
114
- ### Format
115
-
116
- ```ruby
117
- class ProcessProduct < CMDx::Task
118
- attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
119
-
120
- attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
121
-
122
- def work
123
- # Your logic here...
124
- end
125
- end
126
- ```
127
-
128
- | Options | Description |
129
- |---------|-------------|
130
- | `regexp` | Alias for :with option |
131
- | `:with` | Regex pattern that the value must match |
132
- | `:without` | Regex pattern that the value must not match |
133
-
134
- ### Inclusion
135
-
136
- ```ruby
137
- class ProcessProduct < CMDx::Task
138
- attribute :availability, inclusion: { in: %w[available limited] }
139
-
140
- def work
141
- # Your logic here...
142
- end
143
- end
144
- ```
145
-
146
- | Options | Description |
147
- |---------|-------------|
148
- | `:in` | The collection of allowed values or range |
149
- | `:within` | Alias for :in option |
150
- | `:of_message` | Custom message for discrete value inclusions |
151
- | `:in_message` | Custom message for range-based inclusions |
152
- | `:within_message` | Alias for :in_message option |
153
-
154
- ### Length
155
-
156
- ```ruby
157
- class CreateBlogPost < CMDx::Task
158
- attribute :title, length: { within: 5..100 }
159
-
160
- def work
161
- # Your logic here...
162
- end
163
- end
164
- ```
165
-
166
- | Options | Description |
167
- |---------|-------------|
168
- | `:within` | Range that the length must fall within (inclusive) |
169
- | `:not_within` | Range that the length must not fall within |
170
- | `:in` | Alias for :within |
171
- | `:not_in` | Range that the length must not fall within |
172
- | `:min` | Minimum allowed length |
173
- | `:max` | Maximum allowed length |
174
- | `:is` | Exact required length |
175
- | `:is_not` | Length that is not allowed |
176
- | `:within_message` | Custom message for within/range validations |
177
- | `:in_message` | Custom message for :in validation |
178
- | `:not_within_message` | Custom message for not_within validation |
179
- | `:not_in_message` | Custom message for not_in validation |
180
- | `:min_message` | Custom message for minimum length validation |
181
- | `:max_message` | Custom message for maximum length validation |
182
- | `:is_message` | Custom message for exact length validation |
183
- | `:is_not_message` | Custom message for is_not validation |
184
-
185
- ### Numeric
186
-
187
- ```ruby
188
- class CreateBlogPost < CMDx::Task
189
- attribute :word_count, numeric: { min: 100 }
190
-
191
- def work
192
- # Your logic here...
193
- end
194
- end
195
- ```
196
-
197
- | Options | Description |
198
- |---------|-------------|
199
- | `:within` | Range that the value must fall within (inclusive) |
200
- | `:not_within` | Range that the value must not fall within |
201
- | `:in` | Alias for :within option |
202
- | `:not_in` | Alias for :not_within option |
203
- | `:min` | Minimum allowed value (inclusive, >=) |
204
- | `:max` | Maximum allowed value (inclusive, <=) |
205
- | `:is` | Exact value that must match |
206
- | `:is_not` | Value that must not match |
207
- | `:within_message` | Custom message for range validations |
208
- | `:not_within_message` | Custom message for exclusion validations |
209
- | `:min_message` | Custom message for minimum validation |
210
- | `:max_message` | Custom message for maximum validation |
211
- | `:is_message` | Custom message for exact match validation |
212
- | `:is_not_message` | Custom message for exclusion validation |
213
-
214
- ### Presence
215
-
216
- ```ruby
217
- class CreateBlogPost < CMDx::Task
218
- attribute :content, presence: true
219
-
220
- attribute :content, presence: { message: "cannot be blank" }
221
-
222
- def work
223
- # Your logic here...
224
- end
225
- end
226
- ```
227
-
228
- | Options | Description |
229
- |---------|-------------|
230
- | `true` | Ensures value is not nil, empty string, or whitespace |
231
-
232
- ## Declarations
233
-
234
- !!! warning "Important"
235
-
236
- Custom validators must raise `CMDx::ValidationError` with a descriptive message.
237
-
238
- ### Proc or Lambda
239
-
240
- Use anonymous functions for simple validation logic:
241
-
242
- ```ruby
243
- class SetupApplication < CMDx::Task
244
- # Proc
245
- register :validator, :api_key, proc do |value, options = {}|
246
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
247
- raise CMDx::ValidationError, "invalid API key format"
248
- end
249
- end
250
-
251
- # Lambda
252
- register :validator, :api_key, ->(value, options = {}) {
253
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
254
- raise CMDx::ValidationError, "invalid API key format"
255
- end
256
- }
257
- end
258
- ```
259
-
260
- ### Class or Module
261
-
262
- Register custom validation logic for specialized requirements:
263
-
264
- ```ruby
265
- class ApiKeyValidator
266
- def self.call(value, options = {})
267
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
268
- raise CMDx::ValidationError, "invalid API key format"
269
- end
270
- end
271
- end
272
-
273
- class SetupApplication < CMDx::Task
274
- register :validator, :api_key, ApiKeyValidator
275
-
276
- attribute :access_key, api_key: true
277
- end
278
- ```
279
-
280
- ## Removals
281
-
282
- Remove unwanted validators:
283
-
284
- !!! warning
285
-
286
- Each `deregister` call removes one validator. Use multiple calls for batch removals.
287
-
288
- ```ruby
289
- class SetupApplication < CMDx::Task
290
- deregister :validator, :api_key
291
- end
292
- ```
293
-
294
- ## Error Handling
295
-
296
- Validation failures provide detailed, structured error messages:
297
-
298
- ```ruby
299
- class CreateProject < CMDx::Task
300
- attribute :project_name,
301
- presence: true,
302
- length: { minimum: 3, maximum: 50 }
303
- optional :budget,
304
- numeric: { greater_than: 1000, less_than: 1000000 }
305
- required :priority,
306
- inclusion: { in: [:low, :medium, :high] }
307
- attribute :contact_email,
308
- format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
309
-
310
- def work
311
- # Your logic here...
312
- end
313
- end
314
-
315
- result = CreateProject.execute(
316
- project_name: "AB", # Too short
317
- budget: 500, # Too low
318
- priority: :urgent, # Not in allowed list
319
- contact_email: "invalid-email" # Invalid format
320
- )
321
-
322
- result.state #=> "interrupted"
323
- result.status #=> "failed"
324
- result.reason #=> "Invalid"
325
- result.metadata #=> {
326
- # errors: {
327
- # full_message: "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.",
328
- # messages: {
329
- # project_name: ["is too short (minimum is 3 characters)"],
330
- # budget: ["must be greater than 1000"],
331
- # priority: ["is not included in the list"],
332
- # contact_email: ["is invalid"]
333
- # }
334
- # }
335
- # }
336
- ```
data/docs/basics/chain.md DELETED
@@ -1,108 +0,0 @@
1
- # Basics - Chain
2
-
3
- Chains automatically track related task executions within a thread. Think of them as execution traces that help you understand what happened and in what order.
4
-
5
- ## Management
6
-
7
- Each thread maintains its own isolated chain using thread-local storage.
8
-
9
- !!! warning
10
-
11
- Chains are thread-local. Don't share chain references across threads—it causes race conditions.
12
-
13
- ```ruby
14
- # Thread A
15
- Thread.new do
16
- result = ImportDataset.execute(file_path: "/data/batch1.csv")
17
- result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
18
- end
19
-
20
- # Thread B (completely separate chain)
21
- Thread.new do
22
- result = ImportDataset.execute(file_path: "/data/batch2.csv")
23
- result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
24
- end
25
-
26
- # Access current thread's chain
27
- CMDx::Chain.current #=> Returns current chain or nil
28
- CMDx::Chain.clear #=> Clears current thread's chain
29
- ```
30
-
31
- ## Links
32
-
33
- Tasks automatically create or join the current thread's chain:
34
-
35
- !!! warning "Important"
36
-
37
- Chain management is automatic—no manual lifecycle handling needed.
38
-
39
- ```ruby
40
- class ImportDataset < CMDx::Task
41
- def work
42
- # First task creates new chain
43
- result1 = ValidateHeaders.execute(file_path: context.file_path)
44
- result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
45
- result1.chain.results.size #=> 1
46
-
47
- # Second task joins existing chain
48
- result2 = SendNotification.execute(to: "admin@company.com")
49
- result2.chain.id == result1.chain.id #=> true
50
- result2.chain.results.size #=> 2
51
-
52
- # Both results reference the same chain
53
- result1.chain.results == result2.chain.results #=> true
54
- end
55
- end
56
- ```
57
-
58
- ## Inheritance
59
-
60
- Subtasks automatically inherit the current thread's chain, building a unified execution trail:
61
-
62
- ```ruby
63
- class ImportDataset < CMDx::Task
64
- def work
65
- context.dataset = Dataset.find(context.dataset_id)
66
-
67
- # Subtasks automatically inherit current chain
68
- ValidateSchema.execute
69
- TransformData.execute!(context)
70
- SaveToDatabase.execute(dataset_id: context.dataset_id)
71
- end
72
- end
73
-
74
- result = ImportDataset.execute(dataset_id: 456)
75
- chain = result.chain
76
-
77
- # All tasks share the same chain
78
- chain.results.size #=> 4 (main task + 3 subtasks)
79
- chain.results.map { |r| r.task.class }
80
- #=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]
81
- ```
82
-
83
- ## Structure
84
-
85
- Chains expose comprehensive execution information:
86
-
87
- !!! warning "Important"
88
-
89
- Chain state reflects the first (outermost) task result. Subtasks maintain their own states.
90
-
91
- ```ruby
92
- result = ImportDataset.execute(dataset_id: 456)
93
- chain = result.chain
94
-
95
- # Chain identification
96
- chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
97
- chain.results #=> Array of all results in execution order
98
-
99
- # State delegation (from first/outer-most result)
100
- chain.state #=> "complete"
101
- chain.status #=> "success"
102
- chain.outcome #=> "success"
103
-
104
- # Access individual results
105
- chain.results.each_with_index do |result, index|
106
- puts "#{index}: #{result.task.class} - #{result.status}"
107
- end
108
- ```
@@ -1,121 +0,0 @@
1
- # Basics - Context
2
-
3
- Context is your data container for inputs, intermediate values, and outputs. It makes sharing data between tasks effortless.
4
-
5
- ## Assigning Data
6
-
7
- Context automatically captures all task inputs, normalizing keys to symbols:
8
-
9
- ```ruby
10
- # Direct execution
11
- CalculateShipping.execute(weight: 2.5, destination: "CA")
12
-
13
- # Instance creation
14
- CalculateShipping.new(weight: 2.5, "destination" => "CA")
15
- ```
16
-
17
- !!! warning "Important"
18
-
19
- String keys convert to symbols automatically. Prefer symbols for consistency.
20
-
21
- ## Accessing Data
22
-
23
- Access context data using method notation, hash keys, or safe accessors:
24
-
25
- ```ruby
26
- class CalculateShipping < CMDx::Task
27
- def work
28
- # Method style access (preferred)
29
- weight = context.weight
30
- destination = context.destination
31
-
32
- # Hash style access
33
- service_type = context[:service_type]
34
- options = context["options"]
35
-
36
- # Safe access with defaults
37
- rush_delivery = context.fetch!(:rush_delivery, false)
38
- carrier = context.dig(:options, :carrier)
39
-
40
- # Shorter alias
41
- cost = ctx.weight * ctx.rate_per_pound # ctx aliases context
42
- end
43
- end
44
- ```
45
-
46
- !!! warning "Important"
47
-
48
- Undefined attributes return `nil` instead of raising errors—perfect for optional data.
49
-
50
- ## Modifying Context
51
-
52
- Context supports dynamic modification during task execution:
53
-
54
- ```ruby
55
- class CalculateShipping < CMDx::Task
56
- def work
57
- # Direct assignment
58
- context.carrier = Carrier.find_by(code: context.carrier_code)
59
- context.package = Package.new(weight: context.weight)
60
- context.calculated_at = Time.now
61
-
62
- # Hash-style assignment
63
- context[:status] = "calculating"
64
- context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}"
65
-
66
- # Conditional assignment
67
- context.insurance_included ||= false
68
-
69
- # Batch updates
70
- context.merge!(
71
- status: "completed",
72
- shipping_cost: calculate_cost,
73
- estimated_delivery: Time.now + 3.days
74
- )
75
-
76
- # Remove sensitive data
77
- context.delete!(:credit_card_token)
78
- end
79
-
80
- private
81
-
82
- def calculate_cost
83
- base_rate = context.weight * context.rate_per_pound
84
- base_rate + (base_rate * context.tax_percentage)
85
- end
86
- end
87
- ```
88
-
89
- !!! tip
90
-
91
- Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline.
92
-
93
- ## Data Sharing
94
-
95
- Share context across tasks for seamless data flow:
96
-
97
- ```ruby
98
- # During execution
99
- class CalculateShipping < CMDx::Task
100
- def work
101
- # Validate shipping data
102
- validation_result = ValidateAddress.execute(context)
103
-
104
- # Via context
105
- CalculateInsurance.execute(context)
106
-
107
- # Via result
108
- NotifyShippingCalculated.execute(validation_result)
109
-
110
- # Context now contains accumulated data from all tasks
111
- context.address_validated #=> true (from validation)
112
- context.insurance_calculated #=> true (from insurance)
113
- context.notification_sent #=> true (from notification)
114
- end
115
- end
116
-
117
- # After execution
118
- result = CalculateShipping.execute(destination: "New York, NY")
119
-
120
- CreateShippingLabel.execute(result)
121
- ```
@@ -1,152 +0,0 @@
1
- # Basics - Execution
2
-
3
- CMDx offers two execution methods with different error handling approaches. Choose based on your needs: safe result handling or exception-based control flow.
4
-
5
- ## Execution Methods
6
-
7
- Both methods return results, but handle failures differently:
8
-
9
- | Method | Returns | Exceptions | Use Case |
10
- |--------|---------|------------|----------|
11
- | `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
12
- | `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
13
-
14
- ```mermaid
15
- flowchart LR
16
- subgraph Methods
17
- E[execute]
18
- EB[execute!]
19
- end
20
-
21
- subgraph Returns [Returns CMDx::Result]
22
- Success
23
- Failed
24
- Skipped
25
- end
26
-
27
- subgraph Raises [Raises CMDx::Fault]
28
- FailFault
29
- SkipFault
30
- end
31
-
32
- E --> Success
33
- E --> Failed
34
- E --> Skipped
35
-
36
- EB --> Success
37
- EB --> FailFault
38
- EB --> SkipFault
39
- ```
40
-
41
- ## Non-bang Execution
42
-
43
- Always returns a `CMDx::Result`, never raises exceptions. Perfect for most use cases.
44
-
45
- ```ruby
46
- result = CreateAccount.execute(email: "user@example.com")
47
-
48
- # Check execution state
49
- result.success? #=> true/false
50
- result.failed? #=> true/false
51
- result.skipped? #=> true/false
52
-
53
- # Access result data
54
- result.context.email #=> "user@example.com"
55
- result.state #=> "complete"
56
- result.status #=> "success"
57
- ```
58
-
59
- ## Bang Execution
60
-
61
- Raises `CMDx::Fault` exceptions on failure or skip. Returns results only on success.
62
-
63
- | Exception | Raised When |
64
- |-----------|-------------|
65
- | `CMDx::FailFault` | Task execution fails |
66
- | `CMDx::SkipFault` | Task execution is skipped |
67
-
68
- !!! warning "Important"
69
-
70
- Behavior depends on `task_breakpoints` or `workflow_breakpoints` config. Default: only failures raise exceptions.
71
-
72
- ```ruby
73
- begin
74
- result = CreateAccount.execute!(email: "user@example.com")
75
- SendWelcomeEmail.execute(result.context)
76
- rescue CMDx::FailFault => e
77
- ScheduleAccountRetryJob.perform_later(e.result.context.email)
78
- rescue CMDx::SkipFault => e
79
- Rails.logger.info("Account creation skipped: #{e.result.reason}")
80
- rescue Exception => e
81
- ErrorTracker.capture(unhandled_exception: e)
82
- end
83
- ```
84
-
85
- ## Direct Instantiation
86
-
87
- Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
88
-
89
- ```ruby
90
- # Direct instantiation
91
- task = CreateAccount.new(email: "user@example.com", send_welcome: true)
92
-
93
- # Access properties before execution
94
- task.id #=> "abc123..." (unique task ID)
95
- task.context.email #=> "user@example.com"
96
- task.context.send_welcome #=> true
97
- task.result.state #=> "initialized"
98
- task.result.status #=> "success"
99
-
100
- # Manual execution
101
- task.execute
102
- # or
103
- task.execute!
104
-
105
- task.result.success? #=> true/false
106
- ```
107
-
108
- ## Result Details
109
-
110
- The `Result` object provides comprehensive execution information:
111
-
112
- ```ruby
113
- result = CreateAccount.execute(email: "user@example.com")
114
-
115
- # Execution metadata
116
- result.id #=> "abc123..." (unique execution ID)
117
- result.task #=> CreateAccount instance (frozen)
118
- result.chain #=> Task execution chain
119
-
120
- # Context and metadata
121
- result.context #=> Context with all task data
122
- result.metadata #=> Hash with execution metadata
123
- ```
124
-
125
- ## Dry Run
126
-
127
- Execute tasks in dry-run mode to simulate execution without performing side effects. Pass `dry_run: true` in the context when initializing or executing the task.
128
-
129
- Inside your task, use the `dry_run?` method to conditionally skip side effects.
130
-
131
- ```ruby
132
- class CloseStripeCard < CMDx::Task
133
- def work
134
- context.stripe_result =
135
- if dry_run?
136
- FactoryBot.build(:stripe_closed_card)
137
- else
138
- StripeApi.close_card(context.card_id)
139
- end
140
- end
141
- end
142
-
143
- # Execute in dry-run mode
144
- result = CloseStripeCard.execute(card_id: "card_abc123", dry_run: true)
145
- result.success? # => true
146
-
147
- # FactoryBot object
148
- result.context.stripe_result = {
149
- card_id: "card_abc123",
150
- status: "closed"
151
- }
152
- ```