job-workflow 0.1.3

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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +91 -0
  4. data/CHANGELOG.md +23 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +47 -0
  7. data/Rakefile +55 -0
  8. data/Steepfile +10 -0
  9. data/guides/API_REFERENCE.md +112 -0
  10. data/guides/BEST_PRACTICES.md +113 -0
  11. data/guides/CACHE_STORE_INTEGRATION.md +145 -0
  12. data/guides/CONDITIONAL_EXECUTION.md +66 -0
  13. data/guides/DEPENDENCY_WAIT.md +386 -0
  14. data/guides/DRY_RUN.md +390 -0
  15. data/guides/DSL_BASICS.md +216 -0
  16. data/guides/ERROR_HANDLING.md +187 -0
  17. data/guides/GETTING_STARTED.md +524 -0
  18. data/guides/INSTRUMENTATION.md +131 -0
  19. data/guides/LIFECYCLE_HOOKS.md +415 -0
  20. data/guides/NAMESPACES.md +75 -0
  21. data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
  22. data/guides/PARALLEL_PROCESSING.md +302 -0
  23. data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
  24. data/guides/QUEUE_MANAGEMENT.md +141 -0
  25. data/guides/README.md +174 -0
  26. data/guides/SCHEDULED_JOBS.md +165 -0
  27. data/guides/STRUCTURED_LOGGING.md +268 -0
  28. data/guides/TASK_OUTPUTS.md +240 -0
  29. data/guides/TESTING_STRATEGY.md +56 -0
  30. data/guides/THROTTLING.md +198 -0
  31. data/guides/TROUBLESHOOTING.md +53 -0
  32. data/guides/WORKFLOW_COMPOSITION.md +675 -0
  33. data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
  34. data/lib/job-workflow.rb +3 -0
  35. data/lib/job_workflow/argument_def.rb +16 -0
  36. data/lib/job_workflow/arguments.rb +40 -0
  37. data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
  38. data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
  39. data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
  40. data/lib/job_workflow/auto_scaling/executor.rb +43 -0
  41. data/lib/job_workflow/auto_scaling.rb +69 -0
  42. data/lib/job_workflow/cache_store_adapters.rb +46 -0
  43. data/lib/job_workflow/context.rb +352 -0
  44. data/lib/job_workflow/dry_run_config.rb +31 -0
  45. data/lib/job_workflow/dsl.rb +236 -0
  46. data/lib/job_workflow/error_hook.rb +24 -0
  47. data/lib/job_workflow/hook.rb +24 -0
  48. data/lib/job_workflow/hook_registry.rb +66 -0
  49. data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
  50. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
  51. data/lib/job_workflow/instrumentation.rb +257 -0
  52. data/lib/job_workflow/job_status.rb +92 -0
  53. data/lib/job_workflow/logger.rb +86 -0
  54. data/lib/job_workflow/namespace.rb +36 -0
  55. data/lib/job_workflow/output.rb +81 -0
  56. data/lib/job_workflow/output_def.rb +14 -0
  57. data/lib/job_workflow/queue.rb +74 -0
  58. data/lib/job_workflow/queue_adapter.rb +38 -0
  59. data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
  60. data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
  61. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
  62. data/lib/job_workflow/runner.rb +173 -0
  63. data/lib/job_workflow/schedule.rb +46 -0
  64. data/lib/job_workflow/semaphore.rb +71 -0
  65. data/lib/job_workflow/task.rb +83 -0
  66. data/lib/job_workflow/task_callable.rb +43 -0
  67. data/lib/job_workflow/task_context.rb +70 -0
  68. data/lib/job_workflow/task_dependency_wait.rb +66 -0
  69. data/lib/job_workflow/task_enqueue.rb +50 -0
  70. data/lib/job_workflow/task_graph.rb +43 -0
  71. data/lib/job_workflow/task_job_status.rb +70 -0
  72. data/lib/job_workflow/task_output.rb +51 -0
  73. data/lib/job_workflow/task_retry.rb +64 -0
  74. data/lib/job_workflow/task_throttle.rb +46 -0
  75. data/lib/job_workflow/version.rb +5 -0
  76. data/lib/job_workflow/workflow.rb +87 -0
  77. data/lib/job_workflow/workflow_status.rb +112 -0
  78. data/lib/job_workflow.rb +59 -0
  79. data/rbs_collection.lock.yaml +172 -0
  80. data/rbs_collection.yaml +14 -0
  81. data/sig/generated/job-workflow.rbs +2 -0
  82. data/sig/generated/job_workflow/argument_def.rbs +14 -0
  83. data/sig/generated/job_workflow/arguments.rbs +26 -0
  84. data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
  85. data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
  86. data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
  87. data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
  88. data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
  89. data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
  90. data/sig/generated/job_workflow/context.rbs +155 -0
  91. data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
  92. data/sig/generated/job_workflow/dsl.rbs +117 -0
  93. data/sig/generated/job_workflow/error_hook.rbs +18 -0
  94. data/sig/generated/job_workflow/hook.rbs +18 -0
  95. data/sig/generated/job_workflow/hook_registry.rbs +47 -0
  96. data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
  97. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
  98. data/sig/generated/job_workflow/instrumentation.rbs +138 -0
  99. data/sig/generated/job_workflow/job_status.rbs +46 -0
  100. data/sig/generated/job_workflow/logger.rbs +56 -0
  101. data/sig/generated/job_workflow/namespace.rbs +24 -0
  102. data/sig/generated/job_workflow/output.rbs +39 -0
  103. data/sig/generated/job_workflow/output_def.rbs +12 -0
  104. data/sig/generated/job_workflow/queue.rbs +49 -0
  105. data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
  106. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
  107. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
  108. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
  109. data/sig/generated/job_workflow/runner.rbs +66 -0
  110. data/sig/generated/job_workflow/schedule.rbs +34 -0
  111. data/sig/generated/job_workflow/semaphore.rbs +37 -0
  112. data/sig/generated/job_workflow/task.rbs +60 -0
  113. data/sig/generated/job_workflow/task_callable.rbs +30 -0
  114. data/sig/generated/job_workflow/task_context.rbs +52 -0
  115. data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
  116. data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
  117. data/sig/generated/job_workflow/task_graph.rbs +27 -0
  118. data/sig/generated/job_workflow/task_job_status.rbs +42 -0
  119. data/sig/generated/job_workflow/task_output.rbs +29 -0
  120. data/sig/generated/job_workflow/task_retry.rbs +30 -0
  121. data/sig/generated/job_workflow/task_throttle.rbs +20 -0
  122. data/sig/generated/job_workflow/version.rbs +5 -0
  123. data/sig/generated/job_workflow/workflow.rbs +48 -0
  124. data/sig/generated/job_workflow/workflow_status.rbs +55 -0
  125. data/sig/generated/job_workflow.rbs +8 -0
  126. data/sig-private/activejob.rbs +35 -0
  127. data/sig-private/activesupport.rbs +23 -0
  128. data/sig-private/aws.rbs +32 -0
  129. data/sig-private/opentelemetry.rbs +40 -0
  130. data/sig-private/solid_queue.rbs +108 -0
  131. data/tmp/.keep +0 -0
  132. metadata +190 -0
data/guides/DRY_RUN.md ADDED
@@ -0,0 +1,390 @@
1
+ # Dry-Run Mode
2
+
3
+ Dry-run mode allows you to test workflows without executing side effects. This is useful for:
4
+
5
+ - Validating workflow logic before production deployment
6
+ - Testing data transformations without modifying external systems
7
+ - Debugging complex workflows safely
8
+ - CI/CD pipeline integration for workflow validation
9
+
10
+ ## Basic Usage
11
+
12
+ ### Enabling Dry-Run Mode at Workflow Level
13
+
14
+ Use the `dry_run` DSL method to enable dry-run mode for the entire workflow:
15
+
16
+ ```ruby
17
+ class MyWorkflowJob < ActiveJob::Base
18
+ include JobWorkflow::DSL
19
+
20
+ # Always dry-run
21
+ dry_run true
22
+
23
+ task :send_email do |ctx|
24
+ if ctx.dry_run?
25
+ Rails.logger.info "[DRY-RUN] Would send email to #{ctx.arguments.email}"
26
+ else
27
+ Mailer.send_email(ctx.arguments.email)
28
+ end
29
+ end
30
+ end
31
+ ```
32
+
33
+ ### Dynamic Dry-Run Configuration
34
+
35
+ Use a Proc to dynamically determine dry-run mode based on context:
36
+
37
+ ```ruby
38
+ class MyWorkflowJob < ActiveJob::Base
39
+ include JobWorkflow::DSL
40
+
41
+ argument :dry_run_mode, "bool", default: false
42
+
43
+ # Enable dry-run based on argument
44
+ dry_run { |context| context.arguments.dry_run_mode }
45
+
46
+ task :process_data do |ctx|
47
+ # ctx.dry_run? returns true when dry_run_mode argument is true
48
+ end
49
+ end
50
+ ```
51
+
52
+ ### Task-Level Dry-Run
53
+
54
+ You can also configure dry-run at the task level:
55
+
56
+ ```ruby
57
+ class MyWorkflowJob < ActiveJob::Base
58
+ include JobWorkflow::DSL
59
+
60
+ task :safe_operation do |ctx|
61
+ # Normal execution
62
+ end
63
+
64
+ # This task always runs in dry-run mode
65
+ task :risky_operation, dry_run: true do |ctx|
66
+ ctx.skip_in_dry_run do
67
+ ExternalService.dangerous_call
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Priority Rules
74
+
75
+ When both workflow and task have dry-run configuration:
76
+
77
+ 1. **Workflow-level `dry_run: true`** takes priority - all tasks run in dry-run mode
78
+ 2. **Task-level settings** apply only when workflow doesn't enable dry-run
79
+
80
+ ```ruby
81
+ class MyWorkflowJob < ActiveJob::Base
82
+ include JobWorkflow::DSL
83
+
84
+ # Workflow-level: always dry-run
85
+ dry_run true
86
+
87
+ # Even with dry_run: false, this task still runs in dry-run mode
88
+ # because workflow-level setting takes priority
89
+ task :task_one, dry_run: false do |ctx|
90
+ ctx.dry_run? # => true (workflow setting wins)
91
+ end
92
+ end
93
+ ```
94
+
95
+ ## Checking Dry-Run Status
96
+
97
+ ### Using `ctx.dry_run?`
98
+
99
+ The `dry_run?` method returns the current dry-run status:
100
+
101
+ ```ruby
102
+ task :process_order do |ctx|
103
+ if ctx.dry_run?
104
+ Rails.logger.info "[DRY-RUN] Would process order: #{ctx.arguments.order_id}"
105
+ return
106
+ end
107
+
108
+ Order.process(ctx.arguments.order_id)
109
+ end
110
+ ```
111
+
112
+ ## Skipping Side Effects with `skip_in_dry_run`
113
+
114
+ The `skip_in_dry_run` method provides a convenient way to skip side effects in dry-run mode:
115
+
116
+ ### Basic Usage
117
+
118
+ ```ruby
119
+ task :charge_customer do |ctx|
120
+ ctx.skip_in_dry_run do
121
+ PaymentGateway.charge(ctx.arguments.amount)
122
+ end
123
+ end
124
+ ```
125
+
126
+ In dry-run mode:
127
+ - The block is **not executed**
128
+ - Returns `nil` by default
129
+
130
+ In normal mode:
131
+ - The block is executed normally
132
+ - Returns the block's return value
133
+
134
+ ### With Fallback Value
135
+
136
+ Specify a fallback value to return in dry-run mode:
137
+
138
+ ```ruby
139
+ task :get_payment_token do |ctx|
140
+ token = ctx.skip_in_dry_run(fallback: "dry_run_token_#{SecureRandom.hex(8)}") do
141
+ PaymentGateway.create_token(ctx.arguments.card_info)
142
+ end
143
+
144
+ ctx.output[:payment_token] = token
145
+ end
146
+ ```
147
+
148
+ ### Named Dry-Run Operations
149
+
150
+ Use named operations for better instrumentation and debugging:
151
+
152
+ ```ruby
153
+ task :complex_operation do |ctx|
154
+ # Named operation for payment
155
+ ctx.skip_in_dry_run(:payment) do
156
+ PaymentGateway.charge(ctx.arguments.amount)
157
+ end
158
+
159
+ # Named operation for notification
160
+ ctx.skip_in_dry_run(:notification) do
161
+ NotificationService.send(ctx.arguments.user_id, "Payment processed")
162
+ end
163
+ end
164
+ ```
165
+
166
+ ### Named Operations with Fallback Values
167
+
168
+ Combine operation names with fallback values for comprehensive testing:
169
+
170
+ ```ruby
171
+ task :process_payment do |ctx|
172
+ payment_result = ctx.skip_in_dry_run(
173
+ :payment_processing,
174
+ fallback: { success: true, transaction_id: "dry_run_#{Time.current.to_i}", amount: ctx.arguments.amount }
175
+ ) do
176
+ PaymentService.process(
177
+ amount: ctx.arguments.amount,
178
+ customer_id: ctx.arguments.customer_id
179
+ )
180
+ end
181
+
182
+ ctx.output[:payment_result] = payment_result
183
+ end
184
+ ```
185
+
186
+ ## Instrumentation
187
+
188
+ Dry-run operations emit ActiveSupport::Notifications events for monitoring:
189
+
190
+ ### Event: `dry_run.skip.job_workflow`
191
+
192
+ Emitted for each `skip_in_dry_run` call:
193
+
194
+ ```ruby
195
+ ActiveSupport::Notifications.subscribe("dry_run.skip.job_workflow") do |name, start, finish, id, payload|
196
+ puts "Dry-run operation: #{payload[:dry_run_name]}"
197
+ puts "Task: #{payload[:task_name]}"
198
+ puts "Index: #{payload[:dry_run_index]}"
199
+ puts "Skipped: #{payload[:dry_run]}"
200
+ end
201
+ ```
202
+
203
+ Payload includes:
204
+ - `job_id` - Job identifier
205
+ - `job_name` - Job class name
206
+ - `task_name` - Current task name
207
+ - `each_index` - Index in collection (for `each:` tasks)
208
+ - `dry_run_name` - Operation name (if provided)
209
+ - `dry_run_index` - Sequential index of skip_in_dry_run calls within the task
210
+ - `dry_run` - bool indicating if operation was skipped
211
+
212
+ ## Logging
213
+
214
+ When using the default log subscriber, dry-run events are automatically logged:
215
+
216
+ ```
217
+ [DRY-RUN] MyWorkflowJob#process_payment skip: payment (index: 0, skipped: true)
218
+ ```
219
+
220
+ ## Dry-Run vs Condition
221
+
222
+ Dry-run mode and task conditions serve different purposes:
223
+
224
+ | Feature | Dry-Run | Condition |
225
+ |---------|---------|-----------|
226
+ | **Purpose** | Skip side effects for safe testing | Control workflow logic flow |
227
+ | **Scope** | Test/debug toggle for entire workflow or task | Control individual task execution |
228
+ | **Usage** | Validate structure without external calls | Branch workflow based on data |
229
+ | **With side effect** | Skips block execution (returns fallback) | Prevents task execution entirely |
230
+ | **Instrumentation** | Emits `dry_run.skip/execute` events | Emits `task.skip` event |
231
+
232
+ **When to use dry-run:**
233
+ ```ruby
234
+ # Safe testing of workflow logic
235
+ ctx.skip_in_dry_run do
236
+ PaymentGateway.charge(amount)
237
+ end
238
+ ```
239
+
240
+ **When to use condition:**
241
+ ```ruby
242
+ # Control workflow flow based on data
243
+ task :send_email, condition: ->(ctx) { ctx.arguments.email_enabled } do |ctx|
244
+ Mailer.send_email(ctx.arguments.email)
245
+ end
246
+ ```
247
+
248
+ You can combine both for comprehensive control:
249
+ ```ruby
250
+ task :process_order, condition: ->(ctx) { ctx.arguments.order_id } do |ctx|
251
+ # Only runs if condition is true
252
+ # Within this task, you can still use skip_in_dry_run for side effects
253
+ ctx.skip_in_dry_run do
254
+ ExternalService.process(ctx.arguments.order_id)
255
+ end
256
+ end
257
+ ```
258
+
259
+ ## Best Practices
260
+
261
+ ### 1. Use Meaningful Operation Names
262
+
263
+ ```ruby
264
+ # Good - descriptive names help with debugging
265
+ ctx.skip_in_dry_run(:payment_processing) { ... }
266
+ ctx.skip_in_dry_run(:send_welcome_email) { ... }
267
+
268
+ # Avoid - unnamed operations are harder to trace
269
+ ctx.skip_in_dry_run { ... }
270
+ ```
271
+
272
+ ### 2. Provide Realistic Fallback Values
273
+
274
+ ```ruby
275
+ # Good - realistic fallback for testing
276
+ ctx.skip_in_dry_run(fallback: { id: "dry_run_123", status: "simulated" }) do
277
+ ExternalAPI.create_resource(data)
278
+ end
279
+
280
+ # Consider - nil might cause issues in subsequent tasks
281
+ ctx.skip_in_dry_run do
282
+ ExternalAPI.create_resource(data)
283
+ end
284
+ ```
285
+
286
+ ### 3. Log Dry-Run Actions
287
+
288
+ ```ruby
289
+ task :process_data do |ctx|
290
+ if ctx.dry_run?
291
+ Rails.logger.info "[DRY-RUN] Processing: #{ctx.arguments.inspect}"
292
+ end
293
+
294
+ ctx.skip_in_dry_run(:database_write) do
295
+ Database.write(ctx.arguments.data)
296
+ end
297
+ end
298
+ ```
299
+
300
+ ### 4. Use Environment-Based Configuration
301
+
302
+ ```ruby
303
+ class ProductionWorkflowJob < ActiveJob::Base
304
+ include JobWorkflow::DSL
305
+
306
+ # Dry-run in non-production environments
307
+ dry_run { |_ctx| !Rails.env.production? }
308
+
309
+ # Or based on feature flags
310
+ dry_run { |_ctx| FeatureFlag.enabled?(:workflow_dry_run) }
311
+ end
312
+ ```
313
+
314
+ ### 5. Test Both Modes
315
+
316
+ ```ruby
317
+ RSpec.describe MyWorkflowJob do
318
+ context "in normal mode" do
319
+ it "executes side effects" do
320
+ expect(ExternalService).to receive(:call)
321
+ described_class.perform_now(dry_run_mode: false)
322
+ end
323
+ end
324
+
325
+ context "in dry-run mode" do
326
+ it "skips side effects" do
327
+ expect(ExternalService).not_to receive(:call)
328
+ described_class.perform_now(dry_run_mode: true)
329
+ end
330
+ end
331
+ end
332
+ ```
333
+
334
+ ## Example: Complete Workflow
335
+
336
+ ```ruby
337
+ class OrderProcessingJob < ActiveJob::Base
338
+ include JobWorkflow::DSL
339
+
340
+ argument :order_id, "Integer"
341
+ argument :dry_run_mode, "bool", default: false
342
+
343
+ # Dynamic dry-run based on argument
344
+ dry_run { |ctx| ctx.arguments.dry_run_mode }
345
+
346
+ task :validate_order, output: { order: "Hash[Symbol, untyped]" } do |ctx|
347
+ order = Order.find(ctx.arguments.order_id)
348
+ { order: order.attributes }
349
+ end
350
+
351
+ task :charge_payment, depends_on: [:validate_order] do |ctx|
352
+ order = ctx.output[:validate_order].first.order
353
+
354
+ result = ctx.skip_in_dry_run(:payment_processing, fallback: { success: true, transaction_id: "dry_run" }) do
355
+ PaymentService.process(
356
+ amount: order[:total],
357
+ customer_id: order[:customer_id]
358
+ )
359
+ end
360
+
361
+ Rails.logger.info "[#{ctx.dry_run? ? 'DRY-RUN' : 'LIVE'}] Payment result: #{result}"
362
+ end
363
+
364
+ task :send_confirmation, depends_on: [:charge_payment] do |ctx|
365
+ order = ctx.output[:validate_order].first.order
366
+
367
+ ctx.skip_in_dry_run(:email_notification) do
368
+ OrderMailer.confirmation(order[:id]).deliver_later
369
+ end
370
+ end
371
+
372
+ task :update_inventory, depends_on: [:charge_payment] do |ctx|
373
+ order = ctx.output[:validate_order].first.order
374
+
375
+ ctx.skip_in_dry_run(:inventory_update) do
376
+ InventoryService.decrement(order[:items])
377
+ end
378
+ end
379
+ end
380
+
381
+ # Usage
382
+ OrderProcessingJob.perform_later(order_id: 123, dry_run_mode: true) # Dry-run
383
+ OrderProcessingJob.perform_later(order_id: 123, dry_run_mode: false) # Live
384
+ ```
385
+
386
+ ## See Also
387
+
388
+ - [DSL_BASICS.md](DSL_BASICS.md) - Task configuration basics
389
+ - [INSTRUMENTATION.md](INSTRUMENTATION.md) - Monitoring and observability
390
+ - [TESTING_STRATEGY.md](TESTING_STRATEGY.md) - Testing workflows
@@ -0,0 +1,216 @@
1
+ # DSL Basics
2
+
3
+ ## Defining Tasks
4
+
5
+ ### Simple Task
6
+
7
+ The simplest task requires only a name and a block. Tasks can return outputs that are accessible to dependent tasks:
8
+
9
+ ```ruby
10
+ task :simple_task, output: { result: "String" } do |ctx|
11
+ { result: "completed" }
12
+ end
13
+
14
+ # Access the output in another task
15
+ task :next_task, depends_on: [:simple_task] do |ctx|
16
+ result = ctx.output[:simple_task].first.result
17
+ puts result # => "completed"
18
+ end
19
+ ```
20
+
21
+ ### Specifying Dependencies
22
+
23
+ #### Single Dependency
24
+
25
+ ```ruby
26
+ task :fetch_data, output: { data: "Hash" } do |ctx|
27
+ { data: API.fetch }
28
+ end
29
+
30
+ task :process_data, depends_on: [:fetch_data], output: { result: "String" } do |ctx|
31
+ data = ctx.output[:fetch_data].first.data
32
+ { result: process(data) }
33
+ end
34
+ ```
35
+
36
+ #### Multiple Dependencies
37
+
38
+ ```ruby
39
+ task :task_a, output: { a: "Integer" } do |ctx|
40
+ { a: 1 }
41
+ end
42
+
43
+ task :task_b, output: { b: "Integer" } do |ctx|
44
+ { b: 2 }
45
+ end
46
+
47
+ task :task_c, depends_on: [:task_a, :task_b], output: { result: "Integer" } do |ctx|
48
+ a = ctx.output[:task_a].first.a
49
+ b = ctx.output[:task_b].first.b
50
+ { result: a + b } # => 3
51
+ end
52
+ ```
53
+
54
+ ### Dependency Resolution Order
55
+
56
+ JobWorkflow automatically topologically sorts dependencies.
57
+
58
+ ```ruby
59
+ # Correct order is executed regardless of definition order
60
+ task :step3, depends_on: [:step2], output: { final: "bool" } do |ctx|
61
+ { final: true }
62
+ end
63
+
64
+ task :step1, output: { initial: "bool" } do |ctx|
65
+ { initial: true }
66
+ end
67
+
68
+ task :step2, depends_on: [:step1], output: { middle: "bool" } do |ctx|
69
+ { middle: true }
70
+ end
71
+
72
+ # Execution order: step1 → step2 → step3
73
+ ```
74
+
75
+ ## Working with Arguments
76
+
77
+ ### Defining Arguments
78
+
79
+ Type information is specified as **strings**. This is used for RBS generation and documentation; runtime type checking is not performed.
80
+
81
+ ```ruby
82
+ class TypedWorkflowJob < ApplicationJob
83
+ include JobWorkflow::DSL
84
+
85
+ # Type information specified as strings (for RBS generation)
86
+ argument :user_id, "Integer"
87
+ argument :email, "String"
88
+ argument :created_at, "Time"
89
+ argument :metadata, "Hash"
90
+
91
+ # Arrays and generics as strings too
92
+ argument :items, "Array[String]"
93
+ argument :config, "Hash[Symbol, String]"
94
+
95
+ # Fields with default values
96
+ argument :optional_field, "String", default: ""
97
+ end
98
+ ```
99
+
100
+ ### Accessing Arguments
101
+
102
+ **Arguments are immutable and read-only**. Access them via `ctx.arguments`:
103
+
104
+ ```ruby
105
+ task :example do |ctx|
106
+ # Reading arguments
107
+ user_id = ctx.arguments.user_id
108
+ email = ctx.arguments.email
109
+
110
+ # Check if argument has value
111
+ if ctx.arguments.optional_field.present?
112
+ # Process
113
+ end
114
+ end
115
+ ```
116
+
117
+ **Important**: Arguments cannot be modified. To pass data between tasks, use task outputs:
118
+
119
+ ```ruby
120
+ # ✅ Correct: Use outputs to pass data
121
+ task :fetch, output: { result: "String" } do |ctx|
122
+ { result: "data" }
123
+ end
124
+
125
+ task :process, depends_on: [:fetch] do |ctx|
126
+ result = ctx.output[:fetch].first.result
127
+ process_data(result)
128
+ end
129
+
130
+ # ❌ Wrong: Cannot modify arguments
131
+ task :wrong do |ctx|
132
+ ctx.arguments.user_id = 123 # Error: Arguments are immutable
133
+ end
134
+ ```
135
+
136
+ ## Task Options
137
+
138
+ ### Retry Configuration
139
+
140
+ ```ruby
141
+ argument :api_key, "String"
142
+
143
+ # Simple retry (up to 3 times)
144
+ task :flaky_api, retry: 3, output: { response: "Hash" } do |ctx|
145
+ api_key = ctx.arguments.api_key
146
+ { response: ExternalAPI.call(api_key) }
147
+ end
148
+
149
+ # Advanced retry configuration with exponential backoff
150
+ task :advanced_retry,
151
+ retry: {
152
+ count: 5, # Maximum retry attempts
153
+ strategy: :exponential, # :linear or :exponential
154
+ base_delay: 2, # Initial wait time in seconds
155
+ jitter: true # Add ±randomness to prevent thundering herd
156
+ },
157
+ output: { result: "String" } do |ctx|
158
+ { result: unreliable_operation }
159
+ # Retry intervals: 2±1s, 4±2s, 8±4s, 16±8s, 32±16s
160
+ end
161
+ ```
162
+
163
+ ### Conditional Execution
164
+
165
+ ```ruby
166
+ argument :user, "User"
167
+ argument :amount, "Integer"
168
+ argument :verified, "bool"
169
+
170
+ # condition: Execute only if condition returns true
171
+ task :premium_feature,
172
+ condition: ->(ctx) { ctx.arguments.user.premium? },
173
+ output: { premium_result: "String" } do |ctx|
174
+ { premium_result: premium_process }
175
+ end
176
+
177
+ # Inverse condition using negation
178
+ task :free_tier_limit,
179
+ condition: ->(ctx) { !ctx.arguments.user.premium? },
180
+ output: { limited_result: "String" } do |ctx|
181
+ { limited_result: limited_process }
182
+ end
183
+
184
+ # Complex condition
185
+ task :complex,
186
+ condition: ->(ctx) { ctx.arguments.amount > 1000 && ctx.arguments.verified },
187
+ output: { vip_process: "bool" } do |ctx|
188
+ { vip_process: true }
189
+ end
190
+ ```
191
+
192
+ ### Throttling
193
+
194
+ ```ruby
195
+ argument :api_params, "Hash"
196
+
197
+ # Simple syntax: Integer (recommended)
198
+ task :api_call,
199
+ throttle: 10, # Max 10 concurrent executions, default key
200
+ output: { response: "Hash" } do |ctx|
201
+ params = ctx.arguments.api_params
202
+ { response: RateLimitedAPI.call(params) }
203
+ end
204
+
205
+ # Advanced syntax: Hash
206
+ task :api_call_advanced,
207
+ throttle: {
208
+ key: "external_api", # Custom semaphore key
209
+ limit: 10, # Concurrency limit
210
+ ttl: 120 # Lease TTL in seconds (default: 180)
211
+ },
212
+ output: { response: "Hash" } do |ctx|
213
+ params = ctx.arguments.api_params
214
+ { response: RateLimitedAPI.call(params) }
215
+ end
216
+ ```