durable_workflow 0.1.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 (116) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/todo/01.amend.md +133 -0
  3. data/.claude/todo/02.amend.md +444 -0
  4. data/.claude/todo/phase-1-core/01-GEMSPEC.md +193 -0
  5. data/.claude/todo/phase-1-core/02-TYPES.md +462 -0
  6. data/.claude/todo/phase-1-core/03-EXECUTION.md +551 -0
  7. data/.claude/todo/phase-1-core/04-STEPS.md +603 -0
  8. data/.claude/todo/phase-1-core/05-PARSER.md +719 -0
  9. data/.claude/todo/phase-1-core/todo.md +574 -0
  10. data/.claude/todo/phase-2-runtime/01-STORAGE.md +641 -0
  11. data/.claude/todo/phase-2-runtime/02-RUNNERS.md +511 -0
  12. data/.claude/todo/phase-3-extensions/01-EXTENSION-SYSTEM.md +298 -0
  13. data/.claude/todo/phase-3-extensions/02-AI-PLUGIN.md +936 -0
  14. data/.claude/todo/phase-3-extensions/todo.md +262 -0
  15. data/.claude/todo/phase-4-ai-rework/01-DEPENDENCIES.md +107 -0
  16. data/.claude/todo/phase-4-ai-rework/02-CONFIGURATION.md +123 -0
  17. data/.claude/todo/phase-4-ai-rework/03-TOOL-REGISTRY.md +237 -0
  18. data/.claude/todo/phase-4-ai-rework/04-MCP-SERVER.md +432 -0
  19. data/.claude/todo/phase-4-ai-rework/05-MCP-CLIENT.md +333 -0
  20. data/.claude/todo/phase-4-ai-rework/06-EXECUTORS.md +397 -0
  21. data/.claude/todo/phase-4-ai-rework/todo.md +265 -0
  22. data/.claude/todo/phase-5-validation/.DS_Store +0 -0
  23. data/.claude/todo/phase-5-validation/01-TEST-GAPS.md +615 -0
  24. data/.claude/todo/phase-5-validation/01-TESTS.md +2378 -0
  25. data/.claude/todo/phase-5-validation/02-EXAMPLES-SIMPLE.md +744 -0
  26. data/.claude/todo/phase-5-validation/02-EXAMPLES.md +1857 -0
  27. data/.claude/todo/phase-5-validation/03-EXAMPLE-SUPPORT-AGENT.md +95 -0
  28. data/.claude/todo/phase-5-validation/04-EXAMPLE-ORDER-FULFILLMENT.md +94 -0
  29. data/.claude/todo/phase-5-validation/05-EXAMPLE-DATA-PIPELINE.md +145 -0
  30. data/.env.example +3 -0
  31. data/.rubocop.yml +64 -0
  32. data/0.3.amend.md +89 -0
  33. data/CHANGELOG.md +5 -0
  34. data/CODE_OF_CONDUCT.md +84 -0
  35. data/Gemfile +22 -0
  36. data/Gemfile.lock +192 -0
  37. data/LICENSE.txt +21 -0
  38. data/README.md +39 -0
  39. data/Rakefile +16 -0
  40. data/durable_workflow.gemspec +43 -0
  41. data/examples/approval_request.rb +106 -0
  42. data/examples/calculator.rb +154 -0
  43. data/examples/file_search_demo.rb +77 -0
  44. data/examples/hello_workflow.rb +57 -0
  45. data/examples/item_processor.rb +96 -0
  46. data/examples/order_fulfillment/Gemfile +6 -0
  47. data/examples/order_fulfillment/README.md +84 -0
  48. data/examples/order_fulfillment/run.rb +85 -0
  49. data/examples/order_fulfillment/services.rb +146 -0
  50. data/examples/order_fulfillment/workflow.yml +188 -0
  51. data/examples/parallel_fetch.rb +102 -0
  52. data/examples/service_integration.rb +137 -0
  53. data/examples/support_agent/Gemfile +6 -0
  54. data/examples/support_agent/README.md +91 -0
  55. data/examples/support_agent/config/claude_desktop.json +12 -0
  56. data/examples/support_agent/mcp_server.rb +49 -0
  57. data/examples/support_agent/run.rb +67 -0
  58. data/examples/support_agent/services.rb +113 -0
  59. data/examples/support_agent/workflow.yml +286 -0
  60. data/lib/durable_workflow/core/condition.rb +45 -0
  61. data/lib/durable_workflow/core/engine.rb +145 -0
  62. data/lib/durable_workflow/core/executors/approval.rb +51 -0
  63. data/lib/durable_workflow/core/executors/assign.rb +18 -0
  64. data/lib/durable_workflow/core/executors/base.rb +90 -0
  65. data/lib/durable_workflow/core/executors/call.rb +76 -0
  66. data/lib/durable_workflow/core/executors/end.rb +19 -0
  67. data/lib/durable_workflow/core/executors/halt.rb +24 -0
  68. data/lib/durable_workflow/core/executors/loop.rb +118 -0
  69. data/lib/durable_workflow/core/executors/parallel.rb +77 -0
  70. data/lib/durable_workflow/core/executors/registry.rb +34 -0
  71. data/lib/durable_workflow/core/executors/router.rb +26 -0
  72. data/lib/durable_workflow/core/executors/start.rb +61 -0
  73. data/lib/durable_workflow/core/executors/transform.rb +71 -0
  74. data/lib/durable_workflow/core/executors/workflow.rb +32 -0
  75. data/lib/durable_workflow/core/parser.rb +189 -0
  76. data/lib/durable_workflow/core/resolver.rb +61 -0
  77. data/lib/durable_workflow/core/schema_validator.rb +47 -0
  78. data/lib/durable_workflow/core/types/base.rb +41 -0
  79. data/lib/durable_workflow/core/types/condition.rb +25 -0
  80. data/lib/durable_workflow/core/types/configs.rb +103 -0
  81. data/lib/durable_workflow/core/types/entry.rb +26 -0
  82. data/lib/durable_workflow/core/types/results.rb +41 -0
  83. data/lib/durable_workflow/core/types/state.rb +95 -0
  84. data/lib/durable_workflow/core/types/step_def.rb +15 -0
  85. data/lib/durable_workflow/core/types/workflow_def.rb +43 -0
  86. data/lib/durable_workflow/core/types.rb +29 -0
  87. data/lib/durable_workflow/core/validator.rb +318 -0
  88. data/lib/durable_workflow/extensions/ai/ai.rb +149 -0
  89. data/lib/durable_workflow/extensions/ai/configuration.rb +41 -0
  90. data/lib/durable_workflow/extensions/ai/executors/agent.rb +150 -0
  91. data/lib/durable_workflow/extensions/ai/executors/file_search.rb +52 -0
  92. data/lib/durable_workflow/extensions/ai/executors/guardrail.rb +152 -0
  93. data/lib/durable_workflow/extensions/ai/executors/handoff.rb +33 -0
  94. data/lib/durable_workflow/extensions/ai/executors/mcp.rb +47 -0
  95. data/lib/durable_workflow/extensions/ai/mcp/adapter.rb +73 -0
  96. data/lib/durable_workflow/extensions/ai/mcp/client.rb +77 -0
  97. data/lib/durable_workflow/extensions/ai/mcp/rack_app.rb +66 -0
  98. data/lib/durable_workflow/extensions/ai/mcp/server.rb +122 -0
  99. data/lib/durable_workflow/extensions/ai/tool_registry.rb +63 -0
  100. data/lib/durable_workflow/extensions/ai/types.rb +213 -0
  101. data/lib/durable_workflow/extensions/ai.rb +6 -0
  102. data/lib/durable_workflow/extensions/base.rb +77 -0
  103. data/lib/durable_workflow/runners/adapters/inline.rb +42 -0
  104. data/lib/durable_workflow/runners/adapters/sidekiq.rb +69 -0
  105. data/lib/durable_workflow/runners/async.rb +100 -0
  106. data/lib/durable_workflow/runners/stream.rb +126 -0
  107. data/lib/durable_workflow/runners/sync.rb +40 -0
  108. data/lib/durable_workflow/storage/active_record.rb +148 -0
  109. data/lib/durable_workflow/storage/redis.rb +133 -0
  110. data/lib/durable_workflow/storage/sequel.rb +144 -0
  111. data/lib/durable_workflow/storage/store.rb +43 -0
  112. data/lib/durable_workflow/utils.rb +25 -0
  113. data/lib/durable_workflow/version.rb +5 -0
  114. data/lib/durable_workflow.rb +70 -0
  115. data/sig/durable_workflow.rbs +4 -0
  116. metadata +275 -0
@@ -0,0 +1,744 @@
1
+ # 02-EXAMPLES-SIMPLE: Single-File Example Scripts
2
+
3
+ ## Goal
4
+
5
+ Simple, runnable Ruby scripts demonstrating core workflow features. Each is self-contained.
6
+
7
+ ## Directory Structure
8
+
9
+ ```
10
+ examples/
11
+ hello_workflow.rb # Simplest possible workflow
12
+ calculator.rb # Routing + service calls
13
+ item_processor.rb # Service-based data processing
14
+ approval_request.rb # Halt and resume
15
+ parallel_fetch.rb # Concurrent execution
16
+ service_integration.rb # External service calls
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Key Constraints
22
+
23
+ **IMPORTANT**: The resolver only supports `$ref` substitution. There is NO Ruby expression evaluation.
24
+
25
+ - ✅ `$input.name` - reference substitution
26
+ - ✅ `$result.value` - nested reference
27
+ - ✅ `"Hello, $input.name!"` - string interpolation
28
+ - ❌ `$a + $b` - no arithmetic
29
+ - ❌ `$items.length` - no method calls
30
+ - ❌ `$total * 0.08` - no expressions
31
+
32
+ **For any computation, use a service via the `call` step.**
33
+
34
+ Other constraints:
35
+ - Services resolved via `Object.const_get(name)` - must be globally accessible Ruby constants
36
+ - `runner.run({ key: value })` - pass hash, not keyword args
37
+ - Router `field:` does NOT include `$` prefix (evaluator adds it internally)
38
+ - Call step uses `input:` not `args:`
39
+ - Parallel branches is an array of step definitions, not a named hash
40
+
41
+ ---
42
+
43
+ ## 1. `examples/hello_workflow.rb`
44
+
45
+ **Demonstrates:** Basic workflow structure, assign step, input/output, `$ref` substitution
46
+
47
+ ```ruby
48
+ #!/usr/bin/env ruby
49
+ # frozen_string_literal: true
50
+
51
+ # Hello Workflow - Simplest possible durable workflow
52
+ #
53
+ # Run: ruby examples/hello_workflow.rb
54
+ # Requires: Redis running on localhost:6379
55
+
56
+ require "bundler/setup"
57
+ require "durable_workflow"
58
+ require "durable_workflow/storage/redis"
59
+
60
+ # Configure storage
61
+ DurableWorkflow.configure do |c|
62
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
63
+ end
64
+
65
+ # Define workflow inline
66
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
67
+ id: hello_world
68
+ name: Hello World
69
+ version: "1.0"
70
+
71
+ inputs:
72
+ name:
73
+ type: string
74
+ required: true
75
+
76
+ steps:
77
+ - id: start
78
+ type: start
79
+ next: greet
80
+
81
+ - id: greet
82
+ type: assign
83
+ set:
84
+ greeting: "Hello, $input.name!"
85
+ timestamp: "$now"
86
+ next: end
87
+
88
+ - id: end
89
+ type: end
90
+ result:
91
+ message: "$greeting"
92
+ generated_at: "$timestamp"
93
+ YAML
94
+
95
+ # Run it (pass input as a hash, not kwargs)
96
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
97
+ result = runner.run({ name: "World" })
98
+
99
+ puts "Status: #{result.status}"
100
+ puts "Output: #{result.output}"
101
+ # => Status: completed
102
+ # => Output: {:message=>"Hello, World!", :generated_at=>2024-...}
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 2. `examples/calculator.rb`
108
+
109
+ **Demonstrates:** Router step, conditional branching, service calls for computation
110
+
111
+ ```ruby
112
+ #!/usr/bin/env ruby
113
+ # frozen_string_literal: true
114
+
115
+ # Calculator Workflow - Routing based on input
116
+ #
117
+ # Run: ruby examples/calculator.rb
118
+ # Requires: Redis running on localhost:6379
119
+
120
+ require "bundler/setup"
121
+ require "durable_workflow"
122
+ require "durable_workflow/storage/redis"
123
+
124
+ # Calculator service - computation happens in Ruby code
125
+ # Must be globally accessible (module at top level)
126
+ module Calculator
127
+ def self.add(a:, b:)
128
+ { result: a + b, operation: "addition" }
129
+ end
130
+
131
+ def self.subtract(a:, b:)
132
+ { result: a - b, operation: "subtraction" }
133
+ end
134
+
135
+ def self.multiply(a:, b:)
136
+ { result: a * b, operation: "multiplication" }
137
+ end
138
+
139
+ def self.divide(a:, b:)
140
+ { result: a.to_f / b, operation: "division" }
141
+ end
142
+ end
143
+
144
+ DurableWorkflow.configure do |c|
145
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
146
+ end
147
+
148
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
149
+ id: calculator
150
+ name: Calculator
151
+ version: "1.0"
152
+
153
+ inputs:
154
+ operation:
155
+ type: string
156
+ required: true
157
+ a:
158
+ type: number
159
+ required: true
160
+ b:
161
+ type: number
162
+ required: true
163
+
164
+ steps:
165
+ - id: start
166
+ type: start
167
+ next: route
168
+
169
+ - id: route
170
+ type: router
171
+ routes:
172
+ - when:
173
+ field: input.operation
174
+ op: eq
175
+ value: "add"
176
+ then: add
177
+ - when:
178
+ field: input.operation
179
+ op: eq
180
+ value: "subtract"
181
+ then: subtract
182
+ - when:
183
+ field: input.operation
184
+ op: eq
185
+ value: "multiply"
186
+ then: multiply
187
+ - when:
188
+ field: input.operation
189
+ op: eq
190
+ value: "divide"
191
+ then: divide
192
+ default: error
193
+
194
+ - id: add
195
+ type: call
196
+ service: Calculator
197
+ method: add
198
+ input:
199
+ a: "$input.a"
200
+ b: "$input.b"
201
+ output: calc_result
202
+ next: end
203
+
204
+ - id: subtract
205
+ type: call
206
+ service: Calculator
207
+ method: subtract
208
+ input:
209
+ a: "$input.a"
210
+ b: "$input.b"
211
+ output: calc_result
212
+ next: end
213
+
214
+ - id: multiply
215
+ type: call
216
+ service: Calculator
217
+ method: multiply
218
+ input:
219
+ a: "$input.a"
220
+ b: "$input.b"
221
+ output: calc_result
222
+ next: end
223
+
224
+ - id: divide
225
+ type: call
226
+ service: Calculator
227
+ method: divide
228
+ input:
229
+ a: "$input.a"
230
+ b: "$input.b"
231
+ output: calc_result
232
+ next: end
233
+
234
+ - id: error
235
+ type: assign
236
+ set:
237
+ calc_result:
238
+ error: "Unknown operation"
239
+ next: end
240
+
241
+ - id: end
242
+ type: end
243
+ result:
244
+ result: "$calc_result.result"
245
+ operation: "$calc_result.operation"
246
+ error: "$calc_result.error"
247
+ YAML
248
+
249
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
250
+
251
+ # Test all operations
252
+ [
253
+ { operation: "add", a: 10, b: 5 },
254
+ { operation: "subtract", a: 10, b: 5 },
255
+ { operation: "multiply", a: 10, b: 5 },
256
+ { operation: "divide", a: 10, b: 5 }
257
+ ].each do |input|
258
+ result = runner.run(input)
259
+ puts "#{input[:a]} #{input[:operation]} #{input[:b]} = #{result.output[:result]}"
260
+ end
261
+ # => 10 add 5 = 15
262
+ # => 10 subtract 5 = 5
263
+ # => 10 multiply 5 = 50
264
+ # => 10 divide 5 = 2.0
265
+ ```
266
+
267
+ ---
268
+
269
+ ## 3. `examples/item_processor.rb`
270
+
271
+ **Demonstrates:** Service-based data processing
272
+
273
+ Note: The workflow engine has a `loop` step type, but since we can't do arithmetic in YAML,
274
+ we delegate all processing to a service and keep the workflow simple.
275
+
276
+ ```ruby
277
+ #!/usr/bin/env ruby
278
+ # frozen_string_literal: true
279
+
280
+ # Item Processor - Process collection via service
281
+ #
282
+ # Run: ruby examples/item_processor.rb
283
+ # Requires: Redis running on localhost:6379
284
+
285
+ require "bundler/setup"
286
+ require "durable_workflow"
287
+ require "durable_workflow/storage/redis"
288
+
289
+ # Service for item processing (must be globally accessible)
290
+ module ItemProcessor
291
+ def self.process(items:)
292
+ total = 0
293
+ processed = []
294
+
295
+ items.each do |item|
296
+ line_total = item[:quantity] * item[:price]
297
+ total += line_total
298
+ processed << { name: item[:name], subtotal: line_total }
299
+ end
300
+
301
+ {
302
+ count: items.size,
303
+ total: total,
304
+ average: items.empty? ? 0 : total.to_f / items.size,
305
+ items: processed
306
+ }
307
+ end
308
+ end
309
+
310
+ DurableWorkflow.configure do |c|
311
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
312
+ end
313
+
314
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
315
+ id: item_processor
316
+ name: Item Processor
317
+ version: "1.0"
318
+
319
+ inputs:
320
+ items:
321
+ type: array
322
+ required: true
323
+
324
+ steps:
325
+ - id: start
326
+ type: start
327
+ next: process
328
+
329
+ - id: process
330
+ type: call
331
+ service: ItemProcessor
332
+ method: process
333
+ input:
334
+ items: "$input.items"
335
+ output: result
336
+ next: end
337
+
338
+ - id: end
339
+ type: end
340
+ result:
341
+ item_count: "$result.count"
342
+ total: "$result.total"
343
+ average: "$result.average"
344
+ items: "$result.items"
345
+ YAML
346
+
347
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
348
+
349
+ result = runner.run({
350
+ items: [
351
+ { name: "Widget", quantity: 3, price: 10.00 },
352
+ { name: "Gadget", quantity: 2, price: 25.00 },
353
+ { name: "Gizmo", quantity: 5, price: 5.00 }
354
+ ]
355
+ })
356
+
357
+ puts "Processed #{result.output[:item_count]} items"
358
+ puts "Total: $#{result.output[:total]}"
359
+ puts "Average: $#{result.output[:average].round(2)}"
360
+ puts "Breakdown:"
361
+ result.output[:items].each do |item|
362
+ puts " #{item[:name]}: $#{item[:subtotal]}"
363
+ end
364
+ # => Processed 3 items
365
+ # => Total: $105.0
366
+ # => Average: $35.0
367
+ # => Breakdown:
368
+ # => Widget: $30.0
369
+ # => Gadget: $50.0
370
+ # => Gizmo: $25.0
371
+ ```
372
+
373
+ ---
374
+
375
+ ## 4. `examples/approval_request.rb`
376
+
377
+ **Demonstrates:** Approval step, halt and resume, human-in-the-loop
378
+
379
+ ```ruby
380
+ #!/usr/bin/env ruby
381
+ # frozen_string_literal: true
382
+
383
+ # Approval Request - Workflow that halts for human input
384
+ #
385
+ # Run: ruby examples/approval_request.rb
386
+ # Requires: Redis running on localhost:6379
387
+
388
+ require "bundler/setup"
389
+ require "durable_workflow"
390
+ require "durable_workflow/storage/redis"
391
+
392
+ DurableWorkflow.configure do |c|
393
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
394
+ end
395
+
396
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
397
+ id: expense_approval
398
+ name: Expense Approval
399
+ version: "1.0"
400
+
401
+ inputs:
402
+ requester:
403
+ type: string
404
+ required: true
405
+ amount:
406
+ type: number
407
+ required: true
408
+ description:
409
+ type: string
410
+ required: true
411
+
412
+ steps:
413
+ - id: start
414
+ type: start
415
+ next: check_amount
416
+
417
+ - id: check_amount
418
+ type: router
419
+ routes:
420
+ - when:
421
+ field: input.amount
422
+ op: gt
423
+ value: 100
424
+ then: require_approval
425
+ default: auto_approve
426
+
427
+ - id: auto_approve
428
+ type: assign
429
+ set:
430
+ approved: true
431
+ approved_by: system
432
+ reason: "Amount under threshold"
433
+ next: end
434
+
435
+ - id: require_approval
436
+ type: approval
437
+ prompt: "Please approve expense request"
438
+ context:
439
+ requester: "$input.requester"
440
+ amount: "$input.amount"
441
+ description: "$input.description"
442
+ on_reject: rejected
443
+ next: approved
444
+
445
+ - id: approved
446
+ type: assign
447
+ set:
448
+ approved: true
449
+ approved_by: manager
450
+ next: end
451
+
452
+ - id: rejected
453
+ type: assign
454
+ set:
455
+ approved: false
456
+ approved_by: manager
457
+ reason: "Request rejected"
458
+ next: end
459
+
460
+ - id: end
461
+ type: end
462
+ result:
463
+ approved: "$approved"
464
+ approved_by: "$approved_by"
465
+ reason: "$reason"
466
+ YAML
467
+
468
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
469
+
470
+ # Small expense - auto-approved
471
+ result = runner.run({ requester: "Alice", amount: 50, description: "Office supplies" })
472
+ puts "Small expense: #{result.output}"
473
+ # => Small expense: {:approved=>true, :approved_by=>"system", :reason=>"Amount under threshold"}
474
+
475
+ # Large expense - requires approval (halts)
476
+ result = runner.run({ requester: "Bob", amount: 500, description: "Conference ticket" })
477
+ puts "\nLarge expense halted: #{result.status}"
478
+ puts "Halt data: #{result.halt&.data}"
479
+ # Workflow halts here - would resume with approved: true/false
480
+ ```
481
+
482
+ ---
483
+
484
+ ## 5. `examples/parallel_fetch.rb`
485
+
486
+ **Demonstrates:** Parallel step, concurrent execution
487
+
488
+ ```ruby
489
+ #!/usr/bin/env ruby
490
+ # frozen_string_literal: true
491
+
492
+ # Parallel Fetch - Execute multiple operations concurrently
493
+ #
494
+ # Run: ruby examples/parallel_fetch.rb
495
+ # Requires: Redis running on localhost:6379, async gem
496
+
497
+ require "bundler/setup"
498
+ require "durable_workflow"
499
+ require "durable_workflow/storage/redis"
500
+
501
+ DurableWorkflow.configure do |c|
502
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
503
+ end
504
+
505
+ # Mock services (must be globally accessible constants)
506
+ module UserService
507
+ def self.get_profile(user_id:)
508
+ sleep(0.1) # Simulate latency
509
+ { id: user_id, name: "User #{user_id}", email: "user#{user_id}@example.com" }
510
+ end
511
+ end
512
+
513
+ module OrderService
514
+ def self.get_recent(user_id:, limit:)
515
+ sleep(0.1)
516
+ limit.times.map { |i| { id: "ORD-#{i}", amount: rand(10..100) } }
517
+ end
518
+ end
519
+
520
+ module NotificationService
521
+ def self.get_unread(user_id:)
522
+ sleep(0.1)
523
+ rand(0..5).times.map { |i| { id: "NOTIF-#{i}", message: "Notification #{i}" } }
524
+ end
525
+ end
526
+
527
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
528
+ id: dashboard_data
529
+ name: Dashboard Data Fetch
530
+ version: "1.0"
531
+
532
+ inputs:
533
+ user_id:
534
+ type: string
535
+ required: true
536
+
537
+ steps:
538
+ - id: start
539
+ type: start
540
+ next: fetch_all
541
+
542
+ - id: fetch_all
543
+ type: parallel
544
+ branches:
545
+ - id: get_profile
546
+ type: call
547
+ service: UserService
548
+ method: get_profile
549
+ input:
550
+ user_id: "$input.user_id"
551
+ output: profile
552
+
553
+ - id: get_orders
554
+ type: call
555
+ service: OrderService
556
+ method: get_recent
557
+ input:
558
+ user_id: "$input.user_id"
559
+ limit: 5
560
+ output: orders
561
+
562
+ - id: get_notifications
563
+ type: call
564
+ service: NotificationService
565
+ method: get_unread
566
+ input:
567
+ user_id: "$input.user_id"
568
+ output: notifications
569
+ next: end
570
+
571
+ - id: end
572
+ type: end
573
+ result:
574
+ user: "$profile"
575
+ recent_orders: "$orders"
576
+ notifications: "$notifications"
577
+ YAML
578
+
579
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
580
+
581
+ start_time = Time.now
582
+ result = runner.run({ user_id: "USER-123" })
583
+ elapsed = Time.now - start_time
584
+
585
+ puts "Fetched dashboard data in #{elapsed.round(2)}s (parallel, not sequential 0.3s)"
586
+ puts "User: #{result.output[:user][:name]}"
587
+ puts "Orders: #{result.output[:recent_orders].size}"
588
+ puts "Notifications: #{result.output[:notifications].size}"
589
+ ```
590
+
591
+ ---
592
+
593
+ ## 6. `examples/service_integration.rb`
594
+
595
+ **Demonstrates:** Call step, service resolution via Object.const_get, routing
596
+
597
+ ```ruby
598
+ #!/usr/bin/env ruby
599
+ # frozen_string_literal: true
600
+
601
+ # Service Integration - Calling external services from workflow
602
+ #
603
+ # Run: ruby examples/service_integration.rb
604
+ # Requires: Redis running on localhost:6379
605
+
606
+ require "bundler/setup"
607
+ require "securerandom"
608
+ require "durable_workflow"
609
+ require "durable_workflow/storage/redis"
610
+
611
+ # Inventory service (must be globally accessible constant)
612
+ module InventoryService
613
+ STOCK = {
614
+ "PROD-001" => 50,
615
+ "PROD-002" => 0,
616
+ "PROD-003" => 10
617
+ }
618
+
619
+ def self.check_availability(product_id:, quantity:)
620
+ available = STOCK.fetch(product_id, 0)
621
+ {
622
+ product_id: product_id,
623
+ requested: quantity,
624
+ available: available,
625
+ in_stock: available >= quantity
626
+ }
627
+ end
628
+
629
+ def self.reserve(product_id:, quantity:)
630
+ current = STOCK.fetch(product_id, 0)
631
+ raise "Insufficient stock" if current < quantity
632
+
633
+ STOCK[product_id] = current - quantity
634
+ {
635
+ reservation_id: "RES-#{SecureRandom.hex(4)}",
636
+ product_id: product_id,
637
+ quantity: quantity,
638
+ remaining: STOCK[product_id]
639
+ }
640
+ end
641
+ end
642
+
643
+ DurableWorkflow.configure do |c|
644
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
645
+ end
646
+
647
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
648
+ id: inventory_check
649
+ name: Inventory Check and Reserve
650
+ version: "1.0"
651
+
652
+ inputs:
653
+ product_id:
654
+ type: string
655
+ required: true
656
+ quantity:
657
+ type: integer
658
+ required: true
659
+
660
+ steps:
661
+ - id: start
662
+ type: start
663
+ next: check
664
+
665
+ - id: check
666
+ type: call
667
+ service: InventoryService
668
+ method: check_availability
669
+ input:
670
+ product_id: "$input.product_id"
671
+ quantity: "$input.quantity"
672
+ output: availability
673
+ next: decide
674
+
675
+ - id: decide
676
+ type: router
677
+ routes:
678
+ - when:
679
+ field: availability.in_stock
680
+ op: eq
681
+ value: true
682
+ then: reserve
683
+ default: out_of_stock
684
+
685
+ - id: reserve
686
+ type: call
687
+ service: InventoryService
688
+ method: reserve
689
+ input:
690
+ product_id: "$input.product_id"
691
+ quantity: "$input.quantity"
692
+ output: reservation
693
+ next: success
694
+
695
+ - id: success
696
+ type: assign
697
+ set:
698
+ status: reserved
699
+ next: end
700
+
701
+ - id: out_of_stock
702
+ type: assign
703
+ set:
704
+ status: out_of_stock
705
+ error: "Insufficient stock available"
706
+ next: end
707
+
708
+ - id: end
709
+ type: end
710
+ result:
711
+ status: "$status"
712
+ availability: "$availability"
713
+ reservation: "$reservation"
714
+ error: "$error"
715
+ YAML
716
+
717
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
718
+
719
+ # Available product
720
+ result = runner.run({ product_id: "PROD-001", quantity: 5 })
721
+ puts "PROD-001 (qty 5): #{result.output[:status]}"
722
+ puts " Reservation: #{result.output[:reservation][:reservation_id]}" if result.output[:reservation]
723
+
724
+ # Out of stock
725
+ result = runner.run({ product_id: "PROD-002", quantity: 1 })
726
+ puts "\nPROD-002 (qty 1): #{result.output[:status]}"
727
+ puts " Error: #{result.output[:error]}"
728
+
729
+ # Partial availability
730
+ result = runner.run({ product_id: "PROD-003", quantity: 20 })
731
+ puts "\nPROD-003 (qty 20): #{result.output[:status]}"
732
+ puts " Error: #{result.output[:error]}"
733
+ ```
734
+
735
+ ---
736
+
737
+ ## Acceptance Criteria
738
+
739
+ 1. Each script is self-contained and runnable
740
+ 2. Each demonstrates a single core concept
741
+ 3. Output shows expected results
742
+ 4. Comments explain what's being demonstrated
743
+ 5. Uses realistic (not toy) examples
744
+ 6. All examples pass when run with `ruby examples/<name>.rb`