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,1857 @@
1
+ # 02-EXAMPLES: Example Workflows
2
+
3
+ ## Goal
4
+
5
+ Provide complete, working example workflows demonstrating all features of the durable_workflow gem.
6
+
7
+ ## Dependencies
8
+
9
+ - Phase 1 complete
10
+ - Phase 2 complete
11
+ - Phase 3 complete
12
+
13
+ ## Directory Structure
14
+
15
+ ```
16
+ examples/
17
+ ├── basic/
18
+ │ ├── hello_world.yml
19
+ │ ├── calculator.yml
20
+ │ └── run_basic.rb
21
+ ├── routing/
22
+ │ ├── conditional_flow.yml
23
+ │ ├── multi_branch.yml
24
+ │ └── run_routing.rb
25
+ ├── loops/
26
+ │ ├── process_items.yml
27
+ │ ├── aggregation.yml
28
+ │ └── run_loops.rb
29
+ ├── halts/
30
+ │ ├── human_input.yml
31
+ │ ├── approval_flow.yml
32
+ │ └── run_halts.rb
33
+ ├── parallel/
34
+ │ ├── concurrent_tasks.yml
35
+ │ ├── fan_out_fan_in.yml
36
+ │ └── run_parallel.rb
37
+ ├── services/
38
+ │ ├── external_api.yml
39
+ │ ├── service_chain.yml
40
+ │ ├── services.rb
41
+ │ └── run_services.rb
42
+ ├── subworkflows/
43
+ │ ├── parent.yml
44
+ │ ├── child.yml
45
+ │ └── run_subworkflows.rb
46
+ ├── streaming/
47
+ │ ├── streamed_workflow.yml
48
+ │ └── run_streaming.rb
49
+ ├── ai/ (requires AI extension)
50
+ │ ├── chatbot.yml
51
+ │ ├── multi_agent.yml
52
+ │ └── run_ai.rb
53
+ └── complete/
54
+ ├── order_processing.yml
55
+ ├── document_review.yml
56
+ └── run_complete.rb
57
+ ```
58
+
59
+ ## Example Files
60
+
61
+ ### 1. `examples/basic/hello_world.yml`
62
+
63
+ ```yaml
64
+ # Simplest possible workflow
65
+ id: hello_world
66
+ name: Hello World
67
+ version: "1.0"
68
+
69
+ input_schema:
70
+ type: object
71
+ properties:
72
+ name:
73
+ type: string
74
+ required:
75
+ - name
76
+
77
+ steps:
78
+ - id: start
79
+ type: start
80
+ next: greet
81
+
82
+ - id: greet
83
+ type: assign
84
+ config:
85
+ assignments:
86
+ greeting: "'Hello, ' + $.input.name + '!'"
87
+ next: done
88
+
89
+ - id: done
90
+ type: end
91
+ ```
92
+
93
+ ### 2. `examples/basic/calculator.yml`
94
+
95
+ ```yaml
96
+ # Simple arithmetic workflow
97
+ id: calculator
98
+ name: Calculator
99
+ version: "1.0"
100
+
101
+ input_schema:
102
+ type: object
103
+ properties:
104
+ operation:
105
+ type: string
106
+ enum: ["add", "subtract", "multiply", "divide"]
107
+ a:
108
+ type: number
109
+ b:
110
+ type: number
111
+ required:
112
+ - operation
113
+ - a
114
+ - b
115
+
116
+ steps:
117
+ - id: start
118
+ type: start
119
+ next: route_operation
120
+
121
+ - id: route_operation
122
+ type: router
123
+ config:
124
+ routes:
125
+ - condition: "$.input.operation == 'add'"
126
+ next: add
127
+ - condition: "$.input.operation == 'subtract'"
128
+ next: subtract
129
+ - condition: "$.input.operation == 'multiply'"
130
+ next: multiply
131
+ - condition: "$.input.operation == 'divide'"
132
+ next: divide
133
+ default: error
134
+
135
+ - id: add
136
+ type: assign
137
+ config:
138
+ assignments:
139
+ result: "$.input.a + $.input.b"
140
+ next: done
141
+
142
+ - id: subtract
143
+ type: assign
144
+ config:
145
+ assignments:
146
+ result: "$.input.a - $.input.b"
147
+ next: done
148
+
149
+ - id: multiply
150
+ type: assign
151
+ config:
152
+ assignments:
153
+ result: "$.input.a * $.input.b"
154
+ next: done
155
+
156
+ - id: divide
157
+ type: assign
158
+ config:
159
+ assignments:
160
+ result: "$.input.a / $.input.b"
161
+ next: done
162
+
163
+ - id: error
164
+ type: assign
165
+ config:
166
+ assignments:
167
+ error: "'Unknown operation: ' + $.input.operation"
168
+ next: done
169
+
170
+ - id: done
171
+ type: end
172
+ ```
173
+
174
+ ### 3. `examples/basic/run_basic.rb`
175
+
176
+ ```ruby
177
+ #!/usr/bin/env ruby
178
+ # frozen_string_literal: true
179
+
180
+ require "bundler/setup"
181
+ require "durable_workflow"
182
+ require "durable_workflow/storage/redis"
183
+
184
+ # Configure with Redis
185
+ DurableWorkflow.configure do |c|
186
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
187
+ end
188
+
189
+ # Load and run Hello World
190
+ puts "=== Hello World ==="
191
+ hello = DurableWorkflow.load("examples/basic/hello_world.yml")
192
+ runner = DurableWorkflow::Runners::Sync.new(hello)
193
+
194
+ result = runner.run(name: "Alice")
195
+ puts "Result: #{result.output[:greeting]}"
196
+ # => Hello, Alice!
197
+
198
+ # Load and run Calculator
199
+ puts "\n=== Calculator ==="
200
+ calc = DurableWorkflow.load("examples/basic/calculator.yml")
201
+ runner = DurableWorkflow::Runners::Sync.new(calc)
202
+
203
+ [
204
+ { operation: "add", a: 10, b: 5 },
205
+ { operation: "subtract", a: 10, b: 5 },
206
+ { operation: "multiply", a: 10, b: 5 },
207
+ { operation: "divide", a: 10, b: 5 }
208
+ ].each do |input|
209
+ result = runner.run(input)
210
+ puts "#{input[:a]} #{input[:operation]} #{input[:b]} = #{result.output[:result]}"
211
+ end
212
+ # => 10 add 5 = 15
213
+ # => 10 subtract 5 = 5
214
+ # => 10 multiply 5 = 50
215
+ # => 10 divide 5 = 2
216
+ ```
217
+
218
+ ### 4. `examples/routing/conditional_flow.yml`
219
+
220
+ ```yaml
221
+ # Workflow with conditional branching
222
+ id: conditional_flow
223
+ name: Conditional Flow
224
+ version: "1.0"
225
+
226
+ input_schema:
227
+ type: object
228
+ properties:
229
+ score:
230
+ type: integer
231
+ minimum: 0
232
+ maximum: 100
233
+
234
+ steps:
235
+ - id: start
236
+ type: start
237
+ next: evaluate
238
+
239
+ - id: evaluate
240
+ type: router
241
+ config:
242
+ routes:
243
+ - condition: "$.input.score >= 90"
244
+ next: grade_a
245
+ - condition: "$.input.score >= 80"
246
+ next: grade_b
247
+ - condition: "$.input.score >= 70"
248
+ next: grade_c
249
+ - condition: "$.input.score >= 60"
250
+ next: grade_d
251
+ default: grade_f
252
+
253
+ - id: grade_a
254
+ type: assign
255
+ config:
256
+ assignments:
257
+ grade: "'A'"
258
+ message: "'Excellent!'"
259
+ next: done
260
+
261
+ - id: grade_b
262
+ type: assign
263
+ config:
264
+ assignments:
265
+ grade: "'B'"
266
+ message: "'Good job!'"
267
+ next: done
268
+
269
+ - id: grade_c
270
+ type: assign
271
+ config:
272
+ assignments:
273
+ grade: "'C'"
274
+ message: "'Satisfactory'"
275
+ next: done
276
+
277
+ - id: grade_d
278
+ type: assign
279
+ config:
280
+ assignments:
281
+ grade: "'D'"
282
+ message: "'Needs improvement'"
283
+ next: done
284
+
285
+ - id: grade_f
286
+ type: assign
287
+ config:
288
+ assignments:
289
+ grade: "'F'"
290
+ message: "'Please try again'"
291
+ next: done
292
+
293
+ - id: done
294
+ type: end
295
+ ```
296
+
297
+ ### 5. `examples/routing/multi_branch.yml`
298
+
299
+ ```yaml
300
+ # Complex routing with multiple conditions
301
+ id: multi_branch
302
+ name: Multi Branch Decision
303
+ version: "1.0"
304
+
305
+ input_schema:
306
+ type: object
307
+ properties:
308
+ user_type:
309
+ type: string
310
+ subscription:
311
+ type: string
312
+ amount:
313
+ type: number
314
+
315
+ steps:
316
+ - id: start
317
+ type: start
318
+ next: check_user
319
+
320
+ - id: check_user
321
+ type: router
322
+ config:
323
+ routes:
324
+ - condition: "$.input.user_type == 'admin'"
325
+ next: admin_flow
326
+ - condition: "$.input.user_type == 'premium'"
327
+ next: premium_flow
328
+ default: standard_flow
329
+
330
+ - id: admin_flow
331
+ type: assign
332
+ config:
333
+ assignments:
334
+ discount: 1.0
335
+ access_level: "'full'"
336
+ next: calculate_final
337
+
338
+ - id: premium_flow
339
+ type: router
340
+ config:
341
+ routes:
342
+ - condition: "$.input.subscription == 'annual'"
343
+ next: premium_annual
344
+ - condition: "$.input.subscription == 'monthly'"
345
+ next: premium_monthly
346
+ default: premium_basic
347
+
348
+ - id: premium_annual
349
+ type: assign
350
+ config:
351
+ assignments:
352
+ discount: 0.3
353
+ access_level: "'premium'"
354
+ next: calculate_final
355
+
356
+ - id: premium_monthly
357
+ type: assign
358
+ config:
359
+ assignments:
360
+ discount: 0.15
361
+ access_level: "'premium'"
362
+ next: calculate_final
363
+
364
+ - id: premium_basic
365
+ type: assign
366
+ config:
367
+ assignments:
368
+ discount: 0.1
369
+ access_level: "'premium'"
370
+ next: calculate_final
371
+
372
+ - id: standard_flow
373
+ type: assign
374
+ config:
375
+ assignments:
376
+ discount: 0
377
+ access_level: "'basic'"
378
+ next: calculate_final
379
+
380
+ - id: calculate_final
381
+ type: assign
382
+ config:
383
+ assignments:
384
+ final_amount: "$.input.amount * (1 - $.ctx.discount)"
385
+ next: done
386
+
387
+ - id: done
388
+ type: end
389
+ ```
390
+
391
+ ### 6. `examples/loops/process_items.yml`
392
+
393
+ ```yaml
394
+ # Process a collection of items
395
+ id: process_items
396
+ name: Process Items
397
+ version: "1.0"
398
+
399
+ input_schema:
400
+ type: object
401
+ properties:
402
+ items:
403
+ type: array
404
+ items:
405
+ type: object
406
+ properties:
407
+ name:
408
+ type: string
409
+ quantity:
410
+ type: integer
411
+ price:
412
+ type: number
413
+
414
+ steps:
415
+ - id: start
416
+ type: start
417
+ next: init
418
+
419
+ - id: init
420
+ type: assign
421
+ config:
422
+ assignments:
423
+ total: 0
424
+ processed_items: "[]"
425
+ next: loop_items
426
+
427
+ - id: loop_items
428
+ type: loop
429
+ config:
430
+ collection: "$.input.items"
431
+ item_var: item
432
+ body:
433
+ - id: calculate_item
434
+ type: assign
435
+ config:
436
+ assignments:
437
+ item_total: "$.ctx.item.quantity * $.ctx.item.price"
438
+ total: "$.ctx.total + $.ctx.item_total"
439
+ processed_items: "$.ctx.processed_items.concat([{ name: $.ctx.item.name, subtotal: $.ctx.item_total }])"
440
+ next: summarize
441
+
442
+ - id: summarize
443
+ type: assign
444
+ config:
445
+ assignments:
446
+ summary:
447
+ item_count: "$.ctx.processed_items.length"
448
+ total: "$.ctx.total"
449
+ items: "$.ctx.processed_items"
450
+ next: done
451
+
452
+ - id: done
453
+ type: end
454
+ ```
455
+
456
+ ### 7. `examples/loops/aggregation.yml`
457
+
458
+ ```yaml
459
+ # Aggregate data from multiple sources
460
+ id: aggregation
461
+ name: Data Aggregation
462
+ version: "1.0"
463
+
464
+ input_schema:
465
+ type: object
466
+ properties:
467
+ numbers:
468
+ type: array
469
+ items:
470
+ type: number
471
+
472
+ steps:
473
+ - id: start
474
+ type: start
475
+ next: init_stats
476
+
477
+ - id: init_stats
478
+ type: assign
479
+ config:
480
+ assignments:
481
+ sum: 0
482
+ min: "$.input.numbers[0]"
483
+ max: "$.input.numbers[0]"
484
+ count: 0
485
+ next: aggregate
486
+
487
+ - id: aggregate
488
+ type: loop
489
+ config:
490
+ collection: "$.input.numbers"
491
+ item_var: num
492
+ body:
493
+ - id: update_stats
494
+ type: assign
495
+ config:
496
+ assignments:
497
+ sum: "$.ctx.sum + $.ctx.num"
498
+ count: "$.ctx.count + 1"
499
+ min: "$.ctx.num < $.ctx.min ? $.ctx.num : $.ctx.min"
500
+ max: "$.ctx.num > $.ctx.max ? $.ctx.num : $.ctx.max"
501
+ next: calculate_average
502
+
503
+ - id: calculate_average
504
+ type: assign
505
+ config:
506
+ assignments:
507
+ average: "$.ctx.sum / $.ctx.count"
508
+ statistics:
509
+ sum: "$.ctx.sum"
510
+ min: "$.ctx.min"
511
+ max: "$.ctx.max"
512
+ count: "$.ctx.count"
513
+ average: "$.ctx.average"
514
+ next: done
515
+
516
+ - id: done
517
+ type: end
518
+ ```
519
+
520
+ ### 8. `examples/halts/human_input.yml`
521
+
522
+ ```yaml
523
+ # Workflow that pauses for human input
524
+ id: human_input
525
+ name: Human Input Required
526
+ version: "1.0"
527
+
528
+ input_schema:
529
+ type: object
530
+ properties:
531
+ document_id:
532
+ type: string
533
+
534
+ steps:
535
+ - id: start
536
+ type: start
537
+ next: fetch_document
538
+
539
+ - id: fetch_document
540
+ type: assign
541
+ config:
542
+ assignments:
543
+ document:
544
+ id: "$.input.document_id"
545
+ title: "'Sample Document'"
546
+ content: "'This is the document content...'"
547
+ next: request_review
548
+
549
+ - id: request_review
550
+ type: halt
551
+ config:
552
+ data:
553
+ message: "'Please review the document'"
554
+ document: "$.ctx.document"
555
+ next: process_review
556
+
557
+ - id: process_review
558
+ type: assign
559
+ config:
560
+ assignments:
561
+ review_notes: "$.ctx._response.notes"
562
+ reviewer: "$.ctx._response.reviewer"
563
+ next: check_approval
564
+
565
+ - id: check_approval
566
+ type: router
567
+ config:
568
+ routes:
569
+ - condition: "$.ctx._response.approved == true"
570
+ next: approved
571
+ default: rejected
572
+
573
+ - id: approved
574
+ type: assign
575
+ config:
576
+ assignments:
577
+ status: "'approved'"
578
+ result:
579
+ status: "'approved'"
580
+ document: "$.ctx.document"
581
+ reviewer: "$.ctx.reviewer"
582
+ notes: "$.ctx.review_notes"
583
+ next: done
584
+
585
+ - id: rejected
586
+ type: assign
587
+ config:
588
+ assignments:
589
+ status: "'rejected'"
590
+ result:
591
+ status: "'rejected'"
592
+ document: "$.ctx.document"
593
+ reviewer: "$.ctx.reviewer"
594
+ notes: "$.ctx.review_notes"
595
+ next: done
596
+
597
+ - id: done
598
+ type: end
599
+ ```
600
+
601
+ ### 9. `examples/halts/approval_flow.yml`
602
+
603
+ ```yaml
604
+ # Multi-level approval workflow
605
+ id: approval_flow
606
+ name: Multi-Level Approval
607
+ version: "1.0"
608
+
609
+ input_schema:
610
+ type: object
611
+ properties:
612
+ request_type:
613
+ type: string
614
+ amount:
615
+ type: number
616
+ requester:
617
+ type: string
618
+
619
+ steps:
620
+ - id: start
621
+ type: start
622
+ next: check_amount
623
+
624
+ - id: check_amount
625
+ type: router
626
+ config:
627
+ routes:
628
+ - condition: "$.input.amount > 10000"
629
+ next: executive_approval
630
+ - condition: "$.input.amount > 1000"
631
+ next: manager_approval
632
+ default: auto_approve
633
+
634
+ - id: manager_approval
635
+ type: approval
636
+ config:
637
+ prompt: "'Manager approval required for $' + $.input.amount"
638
+ data:
639
+ request_type: "$.input.request_type"
640
+ amount: "$.input.amount"
641
+ requester: "$.input.requester"
642
+ level: "'manager'"
643
+ approved_next: approved
644
+ rejected_next: rejected
645
+
646
+ - id: executive_approval
647
+ type: approval
648
+ config:
649
+ prompt: "'Executive approval required for $' + $.input.amount"
650
+ data:
651
+ request_type: "$.input.request_type"
652
+ amount: "$.input.amount"
653
+ requester: "$.input.requester"
654
+ level: "'executive'"
655
+ approved_next: approved
656
+ rejected_next: rejected
657
+
658
+ - id: auto_approve
659
+ type: assign
660
+ config:
661
+ assignments:
662
+ approval_level: "'auto'"
663
+ approved_by: "'system'"
664
+ next: approved
665
+
666
+ - id: approved
667
+ type: assign
668
+ config:
669
+ assignments:
670
+ result:
671
+ status: "'approved'"
672
+ amount: "$.input.amount"
673
+ approved_by: "$.ctx.approved_by || 'approver'"
674
+ timestamp: "new Date().toISOString()"
675
+ next: done
676
+
677
+ - id: rejected
678
+ type: assign
679
+ config:
680
+ assignments:
681
+ result:
682
+ status: "'rejected'"
683
+ amount: "$.input.amount"
684
+ rejected_by: "'approver'"
685
+ reason: "$.ctx._response.reason || 'No reason provided'"
686
+ next: done
687
+
688
+ - id: done
689
+ type: end
690
+ ```
691
+
692
+ ### 10. `examples/halts/run_halts.rb`
693
+
694
+ ```ruby
695
+ #!/usr/bin/env ruby
696
+ # frozen_string_literal: true
697
+
698
+ require "bundler/setup"
699
+ require "durable_workflow"
700
+ require "durable_workflow/storage/redis"
701
+
702
+ DurableWorkflow.configure do |c|
703
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
704
+ end
705
+
706
+ # Human Input Example
707
+ puts "=== Human Input Workflow ==="
708
+ workflow = DurableWorkflow.load("examples/halts/human_input.yml")
709
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
710
+
711
+ # Start workflow - will halt for review
712
+ result = runner.run(document_id: "DOC-123")
713
+ puts "Status: #{result.status}"
714
+ puts "Halt data: #{result.halt.data}"
715
+
716
+ # Simulate human providing review
717
+ result = runner.resume(result.execution_id, response: {
718
+ approved: true,
719
+ reviewer: "John Smith",
720
+ notes: "Looks good!"
721
+ })
722
+
723
+ puts "Final status: #{result.status}"
724
+ puts "Result: #{result.output[:result]}"
725
+
726
+ # Approval Flow Example
727
+ puts "\n=== Approval Flow ==="
728
+ approval_wf = DurableWorkflow.load("examples/halts/approval_flow.yml")
729
+ runner = DurableWorkflow::Runners::Sync.new(approval_wf)
730
+
731
+ # Use run_until_complete with block to handle approvals
732
+ result = runner.run_until_complete(
733
+ request_type: "expense",
734
+ amount: 5000,
735
+ requester: "Alice"
736
+ ) do |halt|
737
+ puts "Approval needed: #{halt.prompt}"
738
+ puts "Data: #{halt.data}"
739
+
740
+ # Simulate approval
741
+ { approved: true }
742
+ end
743
+
744
+ puts "Final result: #{result.output[:result]}"
745
+ ```
746
+
747
+ ### 11. `examples/parallel/concurrent_tasks.yml`
748
+
749
+ ```yaml
750
+ # Execute multiple tasks in parallel
751
+ id: concurrent_tasks
752
+ name: Concurrent Tasks
753
+ version: "1.0"
754
+
755
+ input_schema:
756
+ type: object
757
+ properties:
758
+ user_id:
759
+ type: string
760
+
761
+ steps:
762
+ - id: start
763
+ type: start
764
+ next: fetch_data
765
+
766
+ - id: fetch_data
767
+ type: parallel
768
+ config:
769
+ branches:
770
+ profile:
771
+ - id: get_profile
772
+ type: call
773
+ config:
774
+ service: user_service
775
+ method: get_profile
776
+ args:
777
+ user_id: "$.input.user_id"
778
+ result_key: profile
779
+
780
+ orders:
781
+ - id: get_orders
782
+ type: call
783
+ config:
784
+ service: order_service
785
+ method: get_recent
786
+ args:
787
+ user_id: "$.input.user_id"
788
+ limit: 10
789
+ result_key: orders
790
+
791
+ notifications:
792
+ - id: get_notifications
793
+ type: call
794
+ config:
795
+ service: notification_service
796
+ method: unread
797
+ args:
798
+ user_id: "$.input.user_id"
799
+ result_key: notifications
800
+
801
+ merge_strategy: all
802
+ next: combine
803
+
804
+ - id: combine
805
+ type: assign
806
+ config:
807
+ assignments:
808
+ dashboard:
809
+ user: "$.ctx.profile"
810
+ recent_orders: "$.ctx.orders"
811
+ unread_notifications: "$.ctx.notifications"
812
+ next: done
813
+
814
+ - id: done
815
+ type: end
816
+ ```
817
+
818
+ ### 12. `examples/parallel/fan_out_fan_in.yml`
819
+
820
+ ```yaml
821
+ # Fan-out processing then fan-in results
822
+ id: fan_out_fan_in
823
+ name: Fan Out Fan In
824
+ version: "1.0"
825
+
826
+ input_schema:
827
+ type: object
828
+ properties:
829
+ regions:
830
+ type: array
831
+ items:
832
+ type: string
833
+
834
+ steps:
835
+ - id: start
836
+ type: start
837
+ next: process_regions
838
+
839
+ - id: process_regions
840
+ type: parallel
841
+ config:
842
+ branches:
843
+ us:
844
+ - id: us_data
845
+ type: assign
846
+ config:
847
+ assignments:
848
+ us_result:
849
+ region: "'US'"
850
+ sales: 1000000
851
+ growth: 0.15
852
+
853
+ eu:
854
+ - id: eu_data
855
+ type: assign
856
+ config:
857
+ assignments:
858
+ eu_result:
859
+ region: "'EU'"
860
+ sales: 850000
861
+ growth: 0.12
862
+
863
+ apac:
864
+ - id: apac_data
865
+ type: assign
866
+ config:
867
+ assignments:
868
+ apac_result:
869
+ region: "'APAC'"
870
+ sales: 750000
871
+ growth: 0.22
872
+
873
+ merge_strategy: all
874
+ next: aggregate_results
875
+
876
+ - id: aggregate_results
877
+ type: assign
878
+ config:
879
+ assignments:
880
+ total_sales: "$.ctx.us_result.sales + $.ctx.eu_result.sales + $.ctx.apac_result.sales"
881
+ regions: "[$.ctx.us_result, $.ctx.eu_result, $.ctx.apac_result]"
882
+ average_growth: "($.ctx.us_result.growth + $.ctx.eu_result.growth + $.ctx.apac_result.growth) / 3"
883
+ next: done
884
+
885
+ - id: done
886
+ type: end
887
+ ```
888
+
889
+ ### 13. `examples/services/services.rb`
890
+
891
+ ```ruby
892
+ # frozen_string_literal: true
893
+
894
+ # Example service implementations
895
+
896
+ module ExampleServices
897
+ class UserService
898
+ def get_profile(user_id:)
899
+ {
900
+ id: user_id,
901
+ name: "User #{user_id}",
902
+ email: "user#{user_id}@example.com",
903
+ created_at: Time.now.iso8601
904
+ }
905
+ end
906
+
907
+ def update_profile(user_id:, updates:)
908
+ { updated: true, user_id: user_id, changes: updates }
909
+ end
910
+ end
911
+
912
+ class OrderService
913
+ def get_recent(user_id:, limit: 10)
914
+ limit.times.map do |i|
915
+ {
916
+ id: "ORD-#{user_id}-#{i}",
917
+ amount: rand(10..500),
918
+ status: %w[pending shipped delivered].sample,
919
+ created_at: (Time.now - rand(1..30) * 86400).iso8601
920
+ }
921
+ end
922
+ end
923
+
924
+ def create(user_id:, items:)
925
+ {
926
+ id: "ORD-#{SecureRandom.hex(4)}",
927
+ user_id: user_id,
928
+ items: items,
929
+ total: items.sum { |i| i[:price] * i[:quantity] },
930
+ status: "pending",
931
+ created_at: Time.now.iso8601
932
+ }
933
+ end
934
+ end
935
+
936
+ class NotificationService
937
+ def unread(user_id:)
938
+ rand(0..10).times.map do |i|
939
+ {
940
+ id: "NOTIF-#{i}",
941
+ type: %w[order_update promotion reminder].sample,
942
+ message: "Notification #{i} for user #{user_id}",
943
+ created_at: (Time.now - rand(1..7) * 86400).iso8601
944
+ }
945
+ end
946
+ end
947
+
948
+ def send(user_id:, type:, message:)
949
+ { sent: true, notification_id: "NOTIF-#{SecureRandom.hex(4)}" }
950
+ end
951
+ end
952
+
953
+ class PaymentService
954
+ def process(amount:, currency: "USD", method: "card")
955
+ sleep(0.1) # Simulate processing time
956
+ {
957
+ transaction_id: "TXN-#{SecureRandom.hex(6)}",
958
+ amount: amount,
959
+ currency: currency,
960
+ method: method,
961
+ status: "completed",
962
+ processed_at: Time.now.iso8601
963
+ }
964
+ end
965
+
966
+ def refund(transaction_id:, amount:)
967
+ {
968
+ refund_id: "REF-#{SecureRandom.hex(6)}",
969
+ original_transaction: transaction_id,
970
+ amount: amount,
971
+ status: "completed"
972
+ }
973
+ end
974
+ end
975
+
976
+ class InventoryService
977
+ def check(product_id:)
978
+ {
979
+ product_id: product_id,
980
+ available: rand(0..100),
981
+ reserved: rand(0..20),
982
+ warehouse: %w[WEST EAST CENTRAL].sample
983
+ }
984
+ end
985
+
986
+ def reserve(product_id:, quantity:)
987
+ {
988
+ reservation_id: "RES-#{SecureRandom.hex(4)}",
989
+ product_id: product_id,
990
+ quantity: quantity,
991
+ expires_at: (Time.now + 3600).iso8601
992
+ }
993
+ end
994
+ end
995
+ end
996
+ ```
997
+
998
+ ### 14. `examples/services/external_api.yml`
999
+
1000
+ ```yaml
1001
+ # Call external API service
1002
+ id: external_api
1003
+ name: External API Call
1004
+ version: "1.0"
1005
+
1006
+ input_schema:
1007
+ type: object
1008
+ properties:
1009
+ product_id:
1010
+ type: string
1011
+ quantity:
1012
+ type: integer
1013
+
1014
+ steps:
1015
+ - id: start
1016
+ type: start
1017
+ next: check_inventory
1018
+
1019
+ - id: check_inventory
1020
+ type: call
1021
+ config:
1022
+ service: inventory_service
1023
+ method: check
1024
+ args:
1025
+ product_id: "$.input.product_id"
1026
+ result_key: inventory
1027
+ next: evaluate_stock
1028
+
1029
+ - id: evaluate_stock
1030
+ type: router
1031
+ config:
1032
+ routes:
1033
+ - condition: "$.ctx.inventory.available >= $.input.quantity"
1034
+ next: reserve_stock
1035
+ default: out_of_stock
1036
+
1037
+ - id: reserve_stock
1038
+ type: call
1039
+ config:
1040
+ service: inventory_service
1041
+ method: reserve
1042
+ args:
1043
+ product_id: "$.input.product_id"
1044
+ quantity: "$.input.quantity"
1045
+ result_key: reservation
1046
+ next: success
1047
+
1048
+ - id: out_of_stock
1049
+ type: assign
1050
+ config:
1051
+ assignments:
1052
+ error: "'Insufficient inventory'"
1053
+ available: "$.ctx.inventory.available"
1054
+ requested: "$.input.quantity"
1055
+ next: done
1056
+
1057
+ - id: success
1058
+ type: assign
1059
+ config:
1060
+ assignments:
1061
+ result:
1062
+ status: "'reserved'"
1063
+ reservation: "$.ctx.reservation"
1064
+ product_id: "$.input.product_id"
1065
+ quantity: "$.input.quantity"
1066
+ next: done
1067
+
1068
+ - id: done
1069
+ type: end
1070
+ ```
1071
+
1072
+ ### 15. `examples/services/run_services.rb`
1073
+
1074
+ ```ruby
1075
+ #!/usr/bin/env ruby
1076
+ # frozen_string_literal: true
1077
+
1078
+ require "bundler/setup"
1079
+ require "durable_workflow"
1080
+ require "durable_workflow/storage/redis"
1081
+ require_relative "services"
1082
+
1083
+ DurableWorkflow.configure do |c|
1084
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
1085
+ end
1086
+
1087
+ # Register services
1088
+ DurableWorkflow.register_service(:user_service, ExampleServices::UserService.new)
1089
+ DurableWorkflow.register_service(:order_service, ExampleServices::OrderService.new)
1090
+ DurableWorkflow.register_service(:notification_service, ExampleServices::NotificationService.new)
1091
+ DurableWorkflow.register_service(:payment_service, ExampleServices::PaymentService.new)
1092
+ DurableWorkflow.register_service(:inventory_service, ExampleServices::InventoryService.new)
1093
+
1094
+ # External API Example
1095
+ puts "=== External API Workflow ==="
1096
+ workflow = DurableWorkflow.load("examples/services/external_api.yml")
1097
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
1098
+
1099
+ result = runner.run(product_id: "PROD-001", quantity: 5)
1100
+ puts "Result: #{result.output}"
1101
+
1102
+ # Concurrent Tasks Example
1103
+ puts "\n=== Concurrent Tasks Workflow ==="
1104
+ concurrent = DurableWorkflow.load("examples/parallel/concurrent_tasks.yml")
1105
+ runner = DurableWorkflow::Runners::Sync.new(concurrent)
1106
+
1107
+ result = runner.run(user_id: "USER-123")
1108
+ puts "Dashboard data:"
1109
+ puts " Profile: #{result.output[:dashboard][:user][:name]}"
1110
+ puts " Orders: #{result.output[:dashboard][:recent_orders].size} recent orders"
1111
+ puts " Notifications: #{result.output[:dashboard][:unread_notifications].size} unread"
1112
+ ```
1113
+
1114
+ ### 16. `examples/subworkflows/parent.yml`
1115
+
1116
+ ```yaml
1117
+ # Parent workflow that calls child workflows
1118
+ id: parent_workflow
1119
+ name: Parent Workflow
1120
+ version: "1.0"
1121
+
1122
+ input_schema:
1123
+ type: object
1124
+ properties:
1125
+ orders:
1126
+ type: array
1127
+ items:
1128
+ type: object
1129
+
1130
+ steps:
1131
+ - id: start
1132
+ type: start
1133
+ next: init
1134
+
1135
+ - id: init
1136
+ type: assign
1137
+ config:
1138
+ assignments:
1139
+ processed_orders: "[]"
1140
+ total_revenue: 0
1141
+ next: process_orders
1142
+
1143
+ - id: process_orders
1144
+ type: loop
1145
+ config:
1146
+ collection: "$.input.orders"
1147
+ item_var: order
1148
+ body:
1149
+ - id: process_single_order
1150
+ type: workflow
1151
+ config:
1152
+ workflow_id: child_workflow
1153
+ input:
1154
+ order_id: "$.ctx.order.id"
1155
+ items: "$.ctx.order.items"
1156
+ customer_id: "$.ctx.order.customer_id"
1157
+ result_key: order_result
1158
+
1159
+ - id: accumulate
1160
+ type: assign
1161
+ config:
1162
+ assignments:
1163
+ processed_orders: "$.ctx.processed_orders.concat([$.ctx.order_result])"
1164
+ total_revenue: "$.ctx.total_revenue + $.ctx.order_result.total"
1165
+ next: summarize
1166
+
1167
+ - id: summarize
1168
+ type: assign
1169
+ config:
1170
+ assignments:
1171
+ summary:
1172
+ orders_processed: "$.ctx.processed_orders.length"
1173
+ total_revenue: "$.ctx.total_revenue"
1174
+ orders: "$.ctx.processed_orders"
1175
+ next: done
1176
+
1177
+ - id: done
1178
+ type: end
1179
+ ```
1180
+
1181
+ ### 17. `examples/subworkflows/child.yml`
1182
+
1183
+ ```yaml
1184
+ # Child workflow for processing a single order
1185
+ id: child_workflow
1186
+ name: Process Single Order
1187
+ version: "1.0"
1188
+
1189
+ input_schema:
1190
+ type: object
1191
+ properties:
1192
+ order_id:
1193
+ type: string
1194
+ items:
1195
+ type: array
1196
+ customer_id:
1197
+ type: string
1198
+
1199
+ steps:
1200
+ - id: start
1201
+ type: start
1202
+ next: calculate_total
1203
+
1204
+ - id: calculate_total
1205
+ type: assign
1206
+ config:
1207
+ assignments:
1208
+ subtotal: 0
1209
+ next: sum_items
1210
+
1211
+ - id: sum_items
1212
+ type: loop
1213
+ config:
1214
+ collection: "$.input.items"
1215
+ item_var: item
1216
+ body:
1217
+ - id: add_item
1218
+ type: assign
1219
+ config:
1220
+ assignments:
1221
+ subtotal: "$.ctx.subtotal + ($.ctx.item.price * $.ctx.item.quantity)"
1222
+ next: apply_tax
1223
+
1224
+ - id: apply_tax
1225
+ type: assign
1226
+ config:
1227
+ assignments:
1228
+ tax: "$.ctx.subtotal * 0.08"
1229
+ total: "$.ctx.subtotal * 1.08"
1230
+ next: create_result
1231
+
1232
+ - id: create_result
1233
+ type: assign
1234
+ config:
1235
+ assignments:
1236
+ result:
1237
+ order_id: "$.input.order_id"
1238
+ customer_id: "$.input.customer_id"
1239
+ subtotal: "$.ctx.subtotal"
1240
+ tax: "$.ctx.tax"
1241
+ total: "$.ctx.total"
1242
+ status: "'processed'"
1243
+ next: done
1244
+
1245
+ - id: done
1246
+ type: end
1247
+ ```
1248
+
1249
+ ### 18. `examples/subworkflows/run_subworkflows.rb`
1250
+
1251
+ ```ruby
1252
+ #!/usr/bin/env ruby
1253
+ # frozen_string_literal: true
1254
+
1255
+ require "bundler/setup"
1256
+ require "durable_workflow"
1257
+ require "durable_workflow/storage/redis"
1258
+
1259
+ DurableWorkflow.configure do |c|
1260
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
1261
+ end
1262
+
1263
+ # Load and register both workflows
1264
+ child = DurableWorkflow.load("examples/subworkflows/child.yml")
1265
+ parent = DurableWorkflow.load("examples/subworkflows/parent.yml")
1266
+
1267
+ DurableWorkflow.register(child)
1268
+ DurableWorkflow.register(parent)
1269
+
1270
+ # Run parent workflow
1271
+ runner = DurableWorkflow::Runners::Sync.new(parent)
1272
+
1273
+ result = runner.run(orders: [
1274
+ {
1275
+ id: "ORD-001",
1276
+ customer_id: "CUST-001",
1277
+ items: [
1278
+ { name: "Widget", price: 10, quantity: 3 },
1279
+ { name: "Gadget", price: 25, quantity: 2 }
1280
+ ]
1281
+ },
1282
+ {
1283
+ id: "ORD-002",
1284
+ customer_id: "CUST-002",
1285
+ items: [
1286
+ { name: "Widget", price: 10, quantity: 5 },
1287
+ { name: "Gizmo", price: 15, quantity: 1 }
1288
+ ]
1289
+ }
1290
+ ])
1291
+
1292
+ puts "=== Order Processing Complete ==="
1293
+ puts "Orders processed: #{result.output[:summary][:orders_processed]}"
1294
+ puts "Total revenue: $#{result.output[:summary][:total_revenue].round(2)}"
1295
+ puts "\nOrder details:"
1296
+ result.output[:summary][:orders].each do |order|
1297
+ puts " #{order[:order_id]}: $#{order[:total].round(2)}"
1298
+ end
1299
+ ```
1300
+
1301
+ ### 19. `examples/streaming/run_streaming.rb`
1302
+
1303
+ ```ruby
1304
+ #!/usr/bin/env ruby
1305
+ # frozen_string_literal: true
1306
+
1307
+ require "bundler/setup"
1308
+ require "durable_workflow"
1309
+ require "durable_workflow/storage/redis"
1310
+
1311
+ DurableWorkflow.configure do |c|
1312
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
1313
+ end
1314
+
1315
+ # Load workflow
1316
+ workflow = DurableWorkflow.load("examples/basic/calculator.yml")
1317
+ runner = DurableWorkflow::Runners::Stream.new(workflow)
1318
+
1319
+ # Subscribe to all events
1320
+ puts "=== Streaming Events ==="
1321
+ runner.subscribe do |event|
1322
+ puts "[#{event.timestamp.strftime('%H:%M:%S.%L')}] #{event.type}"
1323
+ puts " Data: #{event.data}"
1324
+ end
1325
+
1326
+ # Also show SSE format
1327
+ runner.subscribe(events: ["workflow.completed"]) do |event|
1328
+ puts "\n=== SSE Format ==="
1329
+ puts event.to_sse
1330
+ end
1331
+
1332
+ # Run workflow
1333
+ result = runner.run(operation: "multiply", a: 7, b: 6)
1334
+ puts "\nFinal result: #{result.output[:result]}"
1335
+ ```
1336
+
1337
+ ### 20. `examples/complete/order_processing.yml`
1338
+
1339
+ ```yaml
1340
+ # Complete order processing workflow
1341
+ id: order_processing
1342
+ name: Complete Order Processing
1343
+ version: "1.0"
1344
+
1345
+ input_schema:
1346
+ type: object
1347
+ properties:
1348
+ customer_id:
1349
+ type: string
1350
+ items:
1351
+ type: array
1352
+ items:
1353
+ type: object
1354
+ properties:
1355
+ product_id:
1356
+ type: string
1357
+ quantity:
1358
+ type: integer
1359
+ price:
1360
+ type: number
1361
+ shipping_address:
1362
+ type: object
1363
+ payment_method:
1364
+ type: string
1365
+ required:
1366
+ - customer_id
1367
+ - items
1368
+ - shipping_address
1369
+ - payment_method
1370
+
1371
+ steps:
1372
+ - id: start
1373
+ type: start
1374
+ next: validate_order
1375
+
1376
+ - id: validate_order
1377
+ type: assign
1378
+ config:
1379
+ assignments:
1380
+ order_id: "'ORD-' + Date.now().toString(36)"
1381
+ item_count: "$.input.items.length"
1382
+ next: check_items
1383
+
1384
+ - id: check_items
1385
+ type: router
1386
+ config:
1387
+ routes:
1388
+ - condition: "$.ctx.item_count == 0"
1389
+ next: empty_cart_error
1390
+ default: calculate_totals
1391
+
1392
+ - id: empty_cart_error
1393
+ type: assign
1394
+ config:
1395
+ assignments:
1396
+ error: "'Cannot process empty order'"
1397
+ next: failed
1398
+
1399
+ - id: calculate_totals
1400
+ type: assign
1401
+ config:
1402
+ assignments:
1403
+ subtotal: 0
1404
+ processing_items: "[]"
1405
+ next: process_items
1406
+
1407
+ - id: process_items
1408
+ type: loop
1409
+ config:
1410
+ collection: "$.input.items"
1411
+ item_var: item
1412
+ body:
1413
+ - id: check_inventory
1414
+ type: call
1415
+ config:
1416
+ service: inventory_service
1417
+ method: check
1418
+ args:
1419
+ product_id: "$.ctx.item.product_id"
1420
+ result_key: stock
1421
+
1422
+ - id: validate_stock
1423
+ type: router
1424
+ config:
1425
+ routes:
1426
+ - condition: "$.ctx.stock.available >= $.ctx.item.quantity"
1427
+ next: add_to_order
1428
+ default: insufficient_stock
1429
+
1430
+ - id: add_to_order
1431
+ type: assign
1432
+ config:
1433
+ assignments:
1434
+ line_total: "$.ctx.item.quantity * $.ctx.item.price"
1435
+ subtotal: "$.ctx.subtotal + $.ctx.line_total"
1436
+ processing_items: "$.ctx.processing_items.concat([{ ...$.ctx.item, available: true, line_total: $.ctx.line_total }])"
1437
+
1438
+ - id: insufficient_stock
1439
+ type: assign
1440
+ config:
1441
+ assignments:
1442
+ processing_items: "$.ctx.processing_items.concat([{ ...$.ctx.item, available: false, error: 'Insufficient stock' }])"
1443
+ next: check_availability
1444
+
1445
+ - id: check_availability
1446
+ type: router
1447
+ config:
1448
+ routes:
1449
+ - condition: "$.ctx.processing_items.every(i => i.available)"
1450
+ next: apply_pricing
1451
+ default: partial_availability
1452
+
1453
+ - id: partial_availability
1454
+ type: halt
1455
+ config:
1456
+ data:
1457
+ message: "'Some items are unavailable'"
1458
+ available_items: "$.ctx.processing_items.filter(i => i.available)"
1459
+ unavailable_items: "$.ctx.processing_items.filter(i => !i.available)"
1460
+ subtotal: "$.ctx.subtotal"
1461
+ next: handle_partial_response
1462
+
1463
+ - id: handle_partial_response
1464
+ type: router
1465
+ config:
1466
+ routes:
1467
+ - condition: "$.ctx._response.proceed == true"
1468
+ next: apply_pricing
1469
+ default: cancelled
1470
+
1471
+ - id: apply_pricing
1472
+ type: assign
1473
+ config:
1474
+ assignments:
1475
+ tax_rate: 0.08
1476
+ shipping_cost: "$.ctx.subtotal > 100 ? 0 : 9.99"
1477
+ tax: "$.ctx.subtotal * $.ctx.tax_rate"
1478
+ total: "$.ctx.subtotal + $.ctx.tax + $.ctx.shipping_cost"
1479
+ next: check_high_value
1480
+
1481
+ - id: check_high_value
1482
+ type: router
1483
+ config:
1484
+ routes:
1485
+ - condition: "$.ctx.total > 500"
1486
+ next: require_approval
1487
+ default: process_payment
1488
+
1489
+ - id: require_approval
1490
+ type: approval
1491
+ config:
1492
+ prompt: "'High value order requires approval: $' + $.ctx.total.toFixed(2)"
1493
+ data:
1494
+ order_id: "$.ctx.order_id"
1495
+ customer_id: "$.input.customer_id"
1496
+ total: "$.ctx.total"
1497
+ items: "$.ctx.processing_items"
1498
+ approved_next: process_payment
1499
+ rejected_next: rejected
1500
+
1501
+ - id: process_payment
1502
+ type: call
1503
+ config:
1504
+ service: payment_service
1505
+ method: process
1506
+ args:
1507
+ amount: "$.ctx.total"
1508
+ currency: "'USD'"
1509
+ method: "$.input.payment_method"
1510
+ result_key: payment
1511
+ next: reserve_inventory
1512
+
1513
+ - id: reserve_inventory
1514
+ type: loop
1515
+ config:
1516
+ collection: "$.ctx.processing_items.filter(i => i.available)"
1517
+ item_var: item
1518
+ body:
1519
+ - id: reserve_item
1520
+ type: call
1521
+ config:
1522
+ service: inventory_service
1523
+ method: reserve
1524
+ args:
1525
+ product_id: "$.ctx.item.product_id"
1526
+ quantity: "$.ctx.item.quantity"
1527
+ next: create_order_record
1528
+
1529
+ - id: create_order_record
1530
+ type: assign
1531
+ config:
1532
+ assignments:
1533
+ order:
1534
+ id: "$.ctx.order_id"
1535
+ customer_id: "$.input.customer_id"
1536
+ items: "$.ctx.processing_items.filter(i => i.available)"
1537
+ subtotal: "$.ctx.subtotal"
1538
+ tax: "$.ctx.tax"
1539
+ shipping: "$.ctx.shipping_cost"
1540
+ total: "$.ctx.total"
1541
+ payment: "$.ctx.payment"
1542
+ shipping_address: "$.input.shipping_address"
1543
+ status: "'confirmed'"
1544
+ created_at: "new Date().toISOString()"
1545
+ next: send_confirmation
1546
+
1547
+ - id: send_confirmation
1548
+ type: call
1549
+ config:
1550
+ service: notification_service
1551
+ method: send
1552
+ args:
1553
+ user_id: "$.input.customer_id"
1554
+ type: "'order_confirmation'"
1555
+ message: "'Your order ' + $.ctx.order_id + ' has been confirmed!'"
1556
+ next: completed
1557
+
1558
+ - id: completed
1559
+ type: assign
1560
+ config:
1561
+ assignments:
1562
+ result:
1563
+ status: "'success'"
1564
+ order: "$.ctx.order"
1565
+ next: done
1566
+
1567
+ - id: cancelled
1568
+ type: assign
1569
+ config:
1570
+ assignments:
1571
+ result:
1572
+ status: "'cancelled'"
1573
+ reason: "'Customer cancelled due to unavailable items'"
1574
+ next: done
1575
+
1576
+ - id: rejected
1577
+ type: assign
1578
+ config:
1579
+ assignments:
1580
+ result:
1581
+ status: "'rejected'"
1582
+ reason: "'Order rejected during approval'"
1583
+ next: done
1584
+
1585
+ - id: failed
1586
+ type: assign
1587
+ config:
1588
+ assignments:
1589
+ result:
1590
+ status: "'failed'"
1591
+ error: "$.ctx.error"
1592
+ next: done
1593
+
1594
+ - id: done
1595
+ type: end
1596
+ ```
1597
+
1598
+ ### 21. `examples/complete/run_complete.rb`
1599
+
1600
+ ```ruby
1601
+ #!/usr/bin/env ruby
1602
+ # frozen_string_literal: true
1603
+
1604
+ require "bundler/setup"
1605
+ require "durable_workflow"
1606
+ require "durable_workflow/storage/redis"
1607
+ require_relative "../services/services"
1608
+
1609
+ DurableWorkflow.configure do |c|
1610
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
1611
+ end
1612
+
1613
+ # Register services
1614
+ DurableWorkflow.register_service(:inventory_service, ExampleServices::InventoryService.new)
1615
+ DurableWorkflow.register_service(:payment_service, ExampleServices::PaymentService.new)
1616
+ DurableWorkflow.register_service(:notification_service, ExampleServices::NotificationService.new)
1617
+
1618
+ # Load workflow
1619
+ workflow = DurableWorkflow.load("examples/complete/order_processing.yml")
1620
+ runner = DurableWorkflow::Runners::Stream.new(workflow)
1621
+
1622
+ # Subscribe to events
1623
+ runner.subscribe do |event|
1624
+ case event.type
1625
+ when "step.started"
1626
+ puts " -> #{event.data[:step_id]}"
1627
+ when "workflow.halted"
1628
+ puts " HALTED: #{event.data[:halt][:message]}"
1629
+ when "workflow.completed"
1630
+ puts " COMPLETED"
1631
+ end
1632
+ end
1633
+
1634
+ puts "=== Order Processing Workflow ==="
1635
+ puts
1636
+
1637
+ # Process an order
1638
+ result = runner.run_until_complete(
1639
+ customer_id: "CUST-001",
1640
+ items: [
1641
+ { product_id: "PROD-001", quantity: 2, price: 29.99 },
1642
+ { product_id: "PROD-002", quantity: 1, price: 149.99 },
1643
+ { product_id: "PROD-003", quantity: 3, price: 9.99 }
1644
+ ],
1645
+ shipping_address: {
1646
+ street: "123 Main St",
1647
+ city: "Anytown",
1648
+ state: "CA",
1649
+ zip: "12345"
1650
+ },
1651
+ payment_method: "card"
1652
+ ) do |halt|
1653
+ puts "\nHalt received: #{halt.prompt || halt.data[:message]}"
1654
+
1655
+ if halt.data[:unavailable_items]
1656
+ puts "Unavailable items: #{halt.data[:unavailable_items].size}"
1657
+ puts "Proceed anyway? (simulating yes)"
1658
+ { proceed: true }
1659
+ else
1660
+ # Approval request
1661
+ puts "Approving high-value order..."
1662
+ true
1663
+ end
1664
+ end
1665
+
1666
+ puts "\n=== Result ==="
1667
+ puts "Status: #{result.output[:result][:status]}"
1668
+ if result.output[:result][:order]
1669
+ order = result.output[:result][:order]
1670
+ puts "Order ID: #{order[:id]}"
1671
+ puts "Total: $#{order[:total].round(2)}"
1672
+ puts "Items: #{order[:items].size}"
1673
+ end
1674
+ ```
1675
+
1676
+ ### 22. `examples/ai/chatbot.yml` (requires AI extension)
1677
+
1678
+ ```yaml
1679
+ # Simple chatbot workflow using AI extension
1680
+ id: chatbot
1681
+ name: AI Chatbot
1682
+ version: "1.0"
1683
+
1684
+ # AI extension data
1685
+ agents:
1686
+ assistant:
1687
+ model: claude-sonnet
1688
+ system_prompt: |
1689
+ You are a helpful customer service assistant. Be friendly,
1690
+ professional, and concise. If you don't know something, say so.
1691
+ tools:
1692
+ - get_order_status
1693
+ - get_product_info
1694
+
1695
+ tools:
1696
+ get_order_status:
1697
+ description: Get the status of a customer order
1698
+ parameters:
1699
+ order_id:
1700
+ type: string
1701
+ description: The order ID to look up
1702
+ handler: order_service.get_status
1703
+
1704
+ get_product_info:
1705
+ description: Get information about a product
1706
+ parameters:
1707
+ product_id:
1708
+ type: string
1709
+ description: The product ID to look up
1710
+ handler: product_service.get_info
1711
+
1712
+ input_schema:
1713
+ type: object
1714
+ properties:
1715
+ message:
1716
+ type: string
1717
+ conversation_history:
1718
+ type: array
1719
+
1720
+ steps:
1721
+ - id: start
1722
+ type: start
1723
+ next: chat
1724
+
1725
+ - id: chat
1726
+ type: agent
1727
+ config:
1728
+ agent: assistant
1729
+ input: "$.input.message"
1730
+ context:
1731
+ history: "$.input.conversation_history"
1732
+ next: done
1733
+
1734
+ - id: done
1735
+ type: end
1736
+ ```
1737
+
1738
+ ### 23. `examples/ai/run_ai.rb`
1739
+
1740
+ ```ruby
1741
+ #!/usr/bin/env ruby
1742
+ # frozen_string_literal: true
1743
+
1744
+ require "bundler/setup"
1745
+ require "durable_workflow"
1746
+ require "durable_workflow/storage/redis"
1747
+ require "durable_workflow/extensions/ai" # Load AI extension
1748
+
1749
+ DurableWorkflow.configure do |c|
1750
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
1751
+ c.ai_api_key = ENV["ANTHROPIC_API_KEY"]
1752
+ end
1753
+
1754
+ # Mock services for tool calls
1755
+ class OrderService
1756
+ def get_status(order_id:)
1757
+ { order_id: order_id, status: "shipped", eta: "2024-01-15" }
1758
+ end
1759
+ end
1760
+
1761
+ class ProductService
1762
+ def get_info(product_id:)
1763
+ { product_id: product_id, name: "Widget Pro", price: 49.99, in_stock: true }
1764
+ end
1765
+ end
1766
+
1767
+ DurableWorkflow.register_service(:order_service, OrderService.new)
1768
+ DurableWorkflow.register_service(:product_service, ProductService.new)
1769
+
1770
+ # Load AI workflow
1771
+ workflow = DurableWorkflow.load("examples/ai/chatbot.yml")
1772
+ runner = DurableWorkflow::Runners::Stream.new(workflow)
1773
+
1774
+ # Subscribe to AI events
1775
+ runner.subscribe(events: ["agent.thinking", "agent.tool_use", "agent.response"]) do |event|
1776
+ case event.type
1777
+ when "agent.thinking"
1778
+ print "."
1779
+ when "agent.tool_use"
1780
+ puts "\n[Tool: #{event.data[:tool]}]"
1781
+ when "agent.response"
1782
+ puts "\nAssistant: #{event.data[:content]}"
1783
+ end
1784
+ end
1785
+
1786
+ # Chat loop
1787
+ history = []
1788
+ puts "=== AI Chatbot ==="
1789
+ puts "Type 'quit' to exit\n\n"
1790
+
1791
+ loop do
1792
+ print "You: "
1793
+ input = gets.chomp
1794
+ break if input.downcase == "quit"
1795
+
1796
+ result = runner.run(message: input, conversation_history: history)
1797
+
1798
+ history << { role: "user", content: input }
1799
+ history << { role: "assistant", content: result.output[:response] }
1800
+ end
1801
+ ```
1802
+
1803
+ ## Usage Instructions
1804
+
1805
+ ### Quick Start
1806
+
1807
+ ```bash
1808
+ # Install dependencies
1809
+ bundle install
1810
+
1811
+ # Start Redis
1812
+ redis-server
1813
+
1814
+ # Run basic examples
1815
+ ruby examples/basic/run_basic.rb
1816
+
1817
+ # Run with streaming
1818
+ ruby examples/streaming/run_streaming.rb
1819
+
1820
+ # Run complete order processing
1821
+ ruby examples/complete/run_complete.rb
1822
+ ```
1823
+
1824
+ ### Running Individual Examples
1825
+
1826
+ ```bash
1827
+ # Basic workflows
1828
+ ruby examples/basic/run_basic.rb
1829
+
1830
+ # Routing examples
1831
+ ruby examples/routing/run_routing.rb
1832
+
1833
+ # Loop examples
1834
+ ruby examples/loops/run_loops.rb
1835
+
1836
+ # Halt/approval examples
1837
+ ruby examples/halts/run_halts.rb
1838
+
1839
+ # Service integration
1840
+ ruby examples/services/run_services.rb
1841
+
1842
+ # Sub-workflows
1843
+ ruby examples/subworkflows/run_subworkflows.rb
1844
+
1845
+ # AI chatbot (requires API key)
1846
+ ANTHROPIC_API_KEY=your-key ruby examples/ai/run_ai.rb
1847
+ ```
1848
+
1849
+ ## Acceptance Criteria
1850
+
1851
+ 1. All example workflows are syntactically valid
1852
+ 2. Each category has working runner script
1853
+ 3. Services demonstrate integration patterns
1854
+ 4. Streaming example shows SSE format
1855
+ 5. Complete example combines multiple features
1856
+ 6. AI example demonstrates extension usage
1857
+ 7. All examples include comments explaining key concepts