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,96 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Item Processor - Loop through collection and aggregate
5
+ #
6
+ # Demonstrates: Loop step, service calls for computation
7
+ #
8
+ # Run: ruby examples/item_processor.rb
9
+ # Requires: Redis running on localhost:6379
10
+
11
+ require "bundler/setup"
12
+ require "durable_workflow"
13
+ require "durable_workflow/storage/redis"
14
+
15
+ # Service for item processing (must be globally accessible)
16
+ module ItemProcessor
17
+ def self.process(items:)
18
+ total = 0
19
+ processed = []
20
+
21
+ items.each do |item|
22
+ line_total = item[:quantity] * item[:price]
23
+ total += line_total
24
+ processed << { name: item[:name], subtotal: line_total }
25
+ end
26
+
27
+ {
28
+ count: items.size,
29
+ total: total,
30
+ average: items.empty? ? 0 : total.to_f / items.size,
31
+ items: processed
32
+ }
33
+ end
34
+ end
35
+
36
+ DurableWorkflow.configure do |c|
37
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
38
+ end
39
+
40
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
41
+ id: item_processor
42
+ name: Item Processor
43
+ version: "1.0"
44
+
45
+ inputs:
46
+ items:
47
+ type: array
48
+ required: true
49
+
50
+ steps:
51
+ - id: start
52
+ type: start
53
+ next: process
54
+
55
+ - id: process
56
+ type: call
57
+ service: ItemProcessor
58
+ method: process
59
+ input:
60
+ items: "$input.items"
61
+ output: result
62
+ next: end
63
+
64
+ - id: end
65
+ type: end
66
+ result:
67
+ item_count: "$result.count"
68
+ total: "$result.total"
69
+ average: "$result.average"
70
+ items: "$result.items"
71
+ YAML
72
+
73
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
74
+
75
+ result = runner.run(input: {
76
+ items: [
77
+ { name: "Widget", quantity: 3, price: 10.00 },
78
+ { name: "Gadget", quantity: 2, price: 25.00 },
79
+ { name: "Gizmo", quantity: 5, price: 5.00 }
80
+ ]
81
+ })
82
+
83
+ puts "Processed #{result.output[:item_count]} items"
84
+ puts "Total: $#{result.output[:total]}"
85
+ puts "Average: $#{result.output[:average].round(2)}"
86
+ puts "Breakdown:"
87
+ result.output[:items].each do |item|
88
+ puts " #{item[:name]}: $#{item[:subtotal]}"
89
+ end
90
+ # => Processed 3 items
91
+ # => Total: $105.0
92
+ # => Average: $35.0
93
+ # => Breakdown:
94
+ # => Widget: $30.0
95
+ # => Gadget: $50.0
96
+ # => Gizmo: $25.0
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "durable_workflow", path: "../.."
6
+ gem "redis"
@@ -0,0 +1,84 @@
1
+ # Order Fulfillment
2
+
3
+ E-commerce order processing workflow demonstrating complex state transitions,
4
+ service integration, parallel execution, and approval workflows.
5
+
6
+ ## Features
7
+
8
+ - Multi-step order processing pipeline
9
+ - Inventory checking and reservation
10
+ - Payment processing with retry logic
11
+ - Parallel shipping calculations
12
+ - Approval workflow for high-value orders
13
+ - Event streaming for order tracking
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ cd examples/order_fulfillment
19
+ bundle install
20
+ redis-server
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ ruby run.rb
27
+ ```
28
+
29
+ ## Architecture
30
+
31
+ ```
32
+ ┌─────────────────┐
33
+ │ Validate Order │
34
+ └────────┬────────┘
35
+
36
+ ┌─────────────────┐
37
+ │ Check Inventory │
38
+ └────────┬────────┘
39
+ ↓ (parallel)
40
+ ┌─────────┴─────────┐
41
+ │ Reserve Stock │
42
+ │ Calculate Ship │
43
+ └─────────┬─────────┘
44
+
45
+ ┌─────────────────┐ ┌──────────────┐
46
+ │ Check Amount │────→│ Approval │
47
+ └────────┬────────┘ └──────┬───────┘
48
+ ↓ ↓
49
+ ┌─────────────────┐
50
+ │ Process Payment │
51
+ └────────┬────────┘
52
+
53
+ ┌─────────────────┐
54
+ │ Create Shipment │
55
+ └────────┬────────┘
56
+
57
+ ┌─────────────────┐
58
+ │ Complete │
59
+ └─────────────────┘
60
+ ```
61
+
62
+ ## Order States
63
+
64
+ - `pending` - Initial state
65
+ - `validated` - Order validated
66
+ - `inventory_reserved` - Stock reserved
67
+ - `payment_pending` - Waiting for payment
68
+ - `payment_approved` - High-value order approved
69
+ - `paid` - Payment processed
70
+ - `shipped` - Order shipped
71
+ - `completed` - Order complete
72
+ - `cancelled` - Order cancelled
73
+ - `failed` - Processing failed
74
+
75
+ ## Event Types
76
+
77
+ Subscribe to real-time order events:
78
+
79
+ - `order.validated`
80
+ - `order.inventory_reserved`
81
+ - `order.payment_processed`
82
+ - `order.shipped`
83
+ - `order.completed`
84
+ - `order.failed`
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "dotenv/load" if File.exist?(File.expand_path("../../.env", __dir__))
6
+ require "securerandom"
7
+ require "durable_workflow"
8
+ require "durable_workflow/storage/redis"
9
+ require_relative "services"
10
+
11
+ DurableWorkflow.configure do |c|
12
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
13
+ end
14
+
15
+ # Services are auto-resolved via Object.const_get
16
+
17
+ workflow = DurableWorkflow.load(File.join(__dir__, "workflow.yml"))
18
+ runner = DurableWorkflow::Runners::Stream.new(workflow)
19
+
20
+ # Progress tracking
21
+ runner.subscribe do |event|
22
+ case event.type
23
+ when "step.started"
24
+ puts " [#{Time.now.strftime('%H:%M:%S')}] Starting: #{event.data[:step_id]}"
25
+ when "step.completed"
26
+ puts " [#{Time.now.strftime('%H:%M:%S')}] Completed: #{event.data[:step_id]}"
27
+ when "workflow.completed"
28
+ puts "\n[DONE] Order processing completed"
29
+ when "workflow.failed"
30
+ puts "\n[FAILED] #{event.data[:error]}"
31
+ end
32
+ end
33
+
34
+ puts "=" * 60
35
+ puts "Order Fulfillment Demo"
36
+ puts "=" * 60
37
+
38
+ # Sample order
39
+ order = {
40
+ order_id: "ORD-#{SecureRandom.hex(4).upcase}",
41
+ customer: {
42
+ id: "CUST-001",
43
+ email: "alice@example.com",
44
+ name: "Alice Smith",
45
+ address: {
46
+ street: "123 Main St",
47
+ city: "San Francisco",
48
+ state: "CA",
49
+ zip: "94105",
50
+ country: "US"
51
+ }
52
+ },
53
+ items: [
54
+ { product_id: "PROD-001", quantity: 2, price: 29.99 },
55
+ { product_id: "PROD-002", quantity: 1, price: 49.99 }
56
+ ],
57
+ payment: {
58
+ method: "credit_card",
59
+ token: "tok_visa_4242"
60
+ },
61
+ options: {
62
+ expedited: false,
63
+ gift_wrap: false
64
+ }
65
+ }
66
+
67
+ puts "\nProcessing order: #{order[:order_id]}"
68
+ puts "Customer: #{order[:customer][:name]}"
69
+ puts "Items: #{order[:items].size}"
70
+ puts "-" * 40
71
+
72
+ result = runner.run(input: order)
73
+
74
+ puts "\n" + "=" * 60
75
+ puts "Order Result"
76
+ puts "=" * 60
77
+ puts "Order ID: #{result.output[:order_id]}"
78
+ puts "Status: #{result.output[:status]}"
79
+ puts "Total: $#{result.output.dig(:totals, :total)}" if result.output[:totals]
80
+ puts "Payment ID: #{result.output[:payment_id]}" if result.output[:payment_id]
81
+ puts "Tracking: #{result.output[:tracking_number]}" if result.output[:tracking_number]
82
+
83
+ if result.output[:error]
84
+ puts "\nError: #{result.output[:error]}"
85
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ # Order validation and calculation service
6
+ module OrderService
7
+ class << self
8
+ def validate_order(order_id:, customer:, items:)
9
+ errors = []
10
+
11
+ errors << "Order ID required" if order_id.nil? || order_id.to_s.empty?
12
+ errors << "Customer required" if customer.nil?
13
+ errors << "Customer email required" if customer && customer[:email].to_s.empty?
14
+ errors << "Items required" if items.nil? || items.empty?
15
+
16
+ items&.each_with_index do |item, i|
17
+ errors << "Item #{i + 1}: product_id required" if item[:product_id].to_s.empty?
18
+ errors << "Item #{i + 1}: quantity must be positive" if item[:quantity].to_i <= 0
19
+ errors << "Item #{i + 1}: price required" if item[:price].to_f <= 0
20
+ end
21
+
22
+ {
23
+ valid: errors.empty?,
24
+ errors: errors.empty? ? nil : errors,
25
+ validated_at: Time.now.iso8601
26
+ }
27
+ end
28
+
29
+ def calculate_totals(items:, options:)
30
+ subtotal = items.sum { |item| item[:quantity] * item[:price] }
31
+ tax = subtotal * 0.08
32
+ shipping = (options && options[:expedited]) ? 14.99 : 5.99
33
+ total = subtotal + tax + shipping
34
+
35
+ {
36
+ subtotal: subtotal.round(2),
37
+ tax: tax.round(2),
38
+ shipping: shipping,
39
+ total: total.round(2)
40
+ }
41
+ end
42
+ end
43
+ end
44
+
45
+ # Inventory management service
46
+ module InventoryService
47
+ STOCK = {
48
+ "PROD-001" => { name: "Widget Pro", quantity: 100, price: 29.99 },
49
+ "PROD-002" => { name: "Gadget Plus", quantity: 50, price: 49.99 },
50
+ "PROD-003" => { name: "Gizmo Max", quantity: 0, price: 99.99 },
51
+ "PROD-004" => { name: "Device Ultra", quantity: 25, price: 199.99 }
52
+ }
53
+
54
+ RESERVATIONS = {}
55
+
56
+ class << self
57
+ def check_availability(items:)
58
+ available = []
59
+ unavailable = []
60
+
61
+ items.each do |item|
62
+ stock = STOCK[item[:product_id]]
63
+ if stock && stock[:quantity] >= item[:quantity]
64
+ available << item[:product_id]
65
+ else
66
+ unavailable << {
67
+ product_id: item[:product_id],
68
+ requested: item[:quantity],
69
+ available: stock&.dig(:quantity) || 0
70
+ }
71
+ end
72
+ end
73
+
74
+ {
75
+ all_available: unavailable.empty?,
76
+ available: available,
77
+ unavailable: unavailable,
78
+ checked_at: Time.now.iso8601
79
+ }
80
+ end
81
+
82
+ def reserve(items:, order_id:)
83
+ reservation_id = "RES-#{SecureRandom.hex(6).upcase}"
84
+
85
+ items.each do |item|
86
+ stock = STOCK[item[:product_id]]
87
+ stock[:quantity] -= item[:quantity] if stock
88
+ end
89
+
90
+ RESERVATIONS[reservation_id] = {
91
+ order_id: order_id,
92
+ items: items,
93
+ reserved_at: Time.now.iso8601
94
+ }
95
+
96
+ {
97
+ id: reservation_id,
98
+ order_id: order_id,
99
+ items: items.size,
100
+ reserved_at: Time.now.iso8601
101
+ }
102
+ end
103
+ end
104
+ end
105
+
106
+ # Shipping service
107
+ module ShippingService
108
+ class << self
109
+ def create_shipment(order_id:, customer:, items:)
110
+ tracking = "TRK#{SecureRandom.hex(8).upcase}"
111
+ address = customer[:address] || {}
112
+
113
+ {
114
+ shipment_id: "SHIP-#{SecureRandom.hex(6).upcase}",
115
+ order_id: order_id,
116
+ tracking_number: tracking,
117
+ carrier: "FastShip",
118
+ destination: "#{address[:city]}, #{address[:state]}",
119
+ estimated_delivery: (Date.today + 5).iso8601,
120
+ created_at: Time.now.iso8601
121
+ }
122
+ end
123
+ end
124
+ end
125
+
126
+ # Payment processing service
127
+ module PaymentService
128
+ class << self
129
+ def charge(payment:, amount:)
130
+ token = payment[:token]
131
+
132
+ # Simulate declined cards
133
+ if token == "declined"
134
+ raise "Card declined"
135
+ end
136
+
137
+ {
138
+ success: true,
139
+ payment_id: "PAY-#{SecureRandom.hex(8).upcase}",
140
+ amount: amount,
141
+ payment_method: payment[:method],
142
+ processed_at: Time.now.iso8601
143
+ }
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,188 @@
1
+ id: order_fulfillment
2
+ name: Order Fulfillment
3
+ version: "1.0"
4
+ description: E-commerce order processing pipeline
5
+
6
+ inputs:
7
+ order_id:
8
+ type: string
9
+ required: true
10
+ description: Unique order identifier
11
+ customer:
12
+ type: object
13
+ required: true
14
+ description: Customer information
15
+ items:
16
+ type: array
17
+ required: true
18
+ description: Order line items
19
+ payment:
20
+ type: object
21
+ required: true
22
+ description: Payment information
23
+ options:
24
+ type: object
25
+ required: false
26
+ description: Order options
27
+
28
+ steps:
29
+ - id: start
30
+ type: start
31
+ next: validate
32
+
33
+ - id: validate
34
+ type: call
35
+ service: OrderService
36
+ method: validate_order
37
+ input:
38
+ order_id: "$input.order_id"
39
+ customer: "$input.customer"
40
+ items: "$input.items"
41
+ output: validation
42
+ on_error: failed
43
+ next: check_validation
44
+
45
+ - id: check_validation
46
+ type: router
47
+ routes:
48
+ - when:
49
+ field: validation.valid
50
+ op: eq
51
+ value: true
52
+ then: check_inventory
53
+ default: failed
54
+
55
+ - id: check_inventory
56
+ type: call
57
+ service: InventoryService
58
+ method: check_availability
59
+ input:
60
+ items: "$input.items"
61
+ output: inventory
62
+ next: inventory_decision
63
+
64
+ - id: inventory_decision
65
+ type: router
66
+ routes:
67
+ - when:
68
+ field: inventory.all_available
69
+ op: eq
70
+ value: true
71
+ then: calculate_totals
72
+ default: out_of_stock
73
+
74
+ - id: out_of_stock
75
+ type: assign
76
+ set:
77
+ status: out_of_stock
78
+ error: "Items not available"
79
+ totals: null
80
+ payment_result: null
81
+ shipment: null
82
+ next: end
83
+
84
+ - id: calculate_totals
85
+ type: call
86
+ service: OrderService
87
+ method: calculate_totals
88
+ input:
89
+ items: "$input.items"
90
+ options: "$input.options"
91
+ output: totals
92
+ next: check_approval
93
+
94
+ - id: check_approval
95
+ type: router
96
+ routes:
97
+ - when:
98
+ field: totals.total
99
+ op: gt
100
+ value: 1000
101
+ then: require_approval
102
+ default: process_payment
103
+
104
+ - id: require_approval
105
+ type: approval
106
+ prompt: "High-value order requires approval"
107
+ context:
108
+ order_id: "$input.order_id"
109
+ total: "$totals.total"
110
+ on_reject: rejected
111
+ next: process_payment
112
+
113
+ - id: rejected
114
+ type: assign
115
+ set:
116
+ status: rejected
117
+ error: "Order rejected by manager"
118
+ payment_result: null
119
+ shipment: null
120
+ next: end
121
+
122
+ - id: process_payment
123
+ type: call
124
+ service: PaymentService
125
+ method: charge
126
+ input:
127
+ payment: "$input.payment"
128
+ amount: "$totals.total"
129
+ output: payment_result
130
+ on_error: payment_failed
131
+ next: reserve_inventory
132
+
133
+ - id: payment_failed
134
+ type: assign
135
+ set:
136
+ status: payment_failed
137
+ error: "Payment processing failed"
138
+ payment_result: null
139
+ shipment: null
140
+ next: end
141
+
142
+ - id: reserve_inventory
143
+ type: call
144
+ service: InventoryService
145
+ method: reserve
146
+ input:
147
+ items: "$input.items"
148
+ order_id: "$input.order_id"
149
+ output: reservation
150
+ next: create_shipment
151
+
152
+ - id: create_shipment
153
+ type: call
154
+ service: ShippingService
155
+ method: create_shipment
156
+ input:
157
+ order_id: "$input.order_id"
158
+ customer: "$input.customer"
159
+ items: "$input.items"
160
+ output: shipment
161
+ next: completed
162
+
163
+ - id: completed
164
+ type: assign
165
+ set:
166
+ status: completed
167
+ error: null
168
+ next: end
169
+
170
+ - id: failed
171
+ type: assign
172
+ set:
173
+ status: failed
174
+ error: "Order validation failed"
175
+ totals: null
176
+ payment_result: null
177
+ shipment: null
178
+ next: end
179
+
180
+ - id: end
181
+ type: end
182
+ result:
183
+ order_id: "$input.order_id"
184
+ status: "$status"
185
+ totals: "$totals"
186
+ payment_id: "$payment_result.payment_id"
187
+ tracking_number: "$shipment.tracking_number"
188
+ error: "$error"
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Parallel Fetch - Execute multiple operations concurrently
5
+ #
6
+ # Demonstrates: Parallel step, concurrent execution
7
+ #
8
+ # Run: ruby examples/parallel_fetch.rb
9
+ # Requires: Redis running on localhost:6379, async gem
10
+
11
+ require "bundler/setup"
12
+ require "durable_workflow"
13
+ require "durable_workflow/storage/redis"
14
+
15
+ DurableWorkflow.configure do |c|
16
+ c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
17
+ end
18
+
19
+ # Mock services (must be globally accessible constants)
20
+ module UserService
21
+ def self.get_profile(user_id:)
22
+ sleep(0.1) # Simulate latency
23
+ { id: user_id, name: "User #{user_id}", email: "user#{user_id}@example.com" }
24
+ end
25
+ end
26
+
27
+ module OrderService
28
+ def self.get_recent(user_id:, limit:)
29
+ sleep(0.1)
30
+ limit.times.map { |i| { id: "ORD-#{i}", amount: rand(10..100) } }
31
+ end
32
+ end
33
+
34
+ module NotificationService
35
+ def self.get_unread(user_id:)
36
+ sleep(0.1)
37
+ rand(0..5).times.map { |i| { id: "NOTIF-#{i}", message: "Notification #{i}" } }
38
+ end
39
+ end
40
+
41
+ workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
42
+ id: dashboard_data
43
+ name: Dashboard Data Fetch
44
+ version: "1.0"
45
+
46
+ inputs:
47
+ user_id:
48
+ type: string
49
+ required: true
50
+
51
+ steps:
52
+ - id: start
53
+ type: start
54
+ next: fetch_all
55
+
56
+ - id: fetch_all
57
+ type: parallel
58
+ branches:
59
+ - id: get_profile
60
+ type: call
61
+ service: UserService
62
+ method: get_profile
63
+ input:
64
+ user_id: "$input.user_id"
65
+ output: profile
66
+
67
+ - id: get_orders
68
+ type: call
69
+ service: OrderService
70
+ method: get_recent
71
+ input:
72
+ user_id: "$input.user_id"
73
+ limit: 5
74
+ output: orders
75
+
76
+ - id: get_notifications
77
+ type: call
78
+ service: NotificationService
79
+ method: get_unread
80
+ input:
81
+ user_id: "$input.user_id"
82
+ output: notifications
83
+ next: end
84
+
85
+ - id: end
86
+ type: end
87
+ result:
88
+ user: "$profile"
89
+ recent_orders: "$orders"
90
+ notifications: "$notifications"
91
+ YAML
92
+
93
+ runner = DurableWorkflow::Runners::Sync.new(workflow)
94
+
95
+ start_time = Time.now
96
+ result = runner.run(input: { user_id: "USER-123" })
97
+ elapsed = Time.now - start_time
98
+
99
+ puts "Fetched dashboard data in #{elapsed.round(2)}s (parallel, not sequential 0.3s)"
100
+ puts "User: #{result.output[:user][:name]}"
101
+ puts "Orders: #{result.output[:recent_orders].size}"
102
+ puts "Notifications: #{result.output[:notifications].size}"