chrono_forge 0.6.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ca8b29d2a6800467fd237f7fb998a243aa1f31d2de99543017e10f03f89c949
4
- data.tar.gz: 60bb959cb97d8f782348bc80b1c7578c526e62e6a4454909308150c33cc06c9e
3
+ metadata.gz: 518b82abfe5b0061b385b4293ae955eb6a188c1e3bf76f4763e5f03e13359771
4
+ data.tar.gz: ca59809649371db8eaa1e6eb8579f7bc34d5471f19f065e908b9585b9fb0e2fb
5
5
  SHA512:
6
- metadata.gz: deb1092227fd41a1cb6ce6ac5784cbd8e571893a18294064e648bbbbaa50ca73046457860ccd93c922d44ad2cec197a6bfbdbc12829a12f209da2f978621d893
7
- data.tar.gz: 3cca8a8acff06779828e648415333d85b8ad9161c0211ee30bc11263c3ee5dc953a637a3f8d1bde56c63d1286af01be2312697559b038776dbbf12366e1d2250
6
+ metadata.gz: ae7de522ddbaa15625fa38109a63e28167b1831e635e78d95f2f9c0e9d6ff8117418a972dfbeda1555f2059c4d33b5faa286bbd1a07132c6a157c71f3194839d
7
+ data.tar.gz: ee9a38551628d68e0efb02ad0b4d6f42d97fec92d776e8a394a64c5b05efcac9608cd9f0bb6897b43c8195b31a17e1cb9fb475e44b67a69cda94e898fc59f7f7
data/README.md CHANGED
@@ -99,7 +99,7 @@ class OrderProcessingWorkflow < ApplicationJob
99
99
  durably_execute :process_order
100
100
 
101
101
  # Final steps
102
- complete_order
102
+ durably_execute :complete_order
103
103
  end
104
104
 
105
105
  private
@@ -146,30 +146,260 @@ OrderProcessingWorkflow.perform_later(
146
146
 
147
147
  #### ⚡ Durable Execution
148
148
 
149
- The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
149
+ The `durably_execute` method ensures operations are executed exactly once with automatic retry logic and fault tolerance:
150
150
 
151
151
  ```ruby
152
- # Execute a method
153
- durably_execute(:process_payment, max_attempts: 3)
152
+ # Basic execution
153
+ durably_execute :send_welcome_email
154
154
 
155
- # Or with a block
156
- durably_execute -> (ctx) {
157
- Payment.process(ctx[:payment_id])
158
- }
155
+ # With custom retry attempts
156
+ durably_execute :critical_payment_processing, max_attempts: 5
157
+
158
+ # With custom name for tracking multiple calls to same method
159
+ durably_execute :upload_file, name: "profile_image_upload"
160
+
161
+ # Complex example with error-prone operation
162
+ class FileProcessingWorkflow < ApplicationJob
163
+ prepend ChronoForge::Executor
164
+
165
+ def perform(file_id:)
166
+ @file_id = file_id
167
+
168
+ # This might fail due to network issues, rate limits, etc.
169
+ durably_execute :upload_to_s3, max_attempts: 5
170
+
171
+ # Process file after successful upload
172
+ durably_execute :generate_thumbnails, max_attempts: 3
173
+ end
174
+
175
+ private
176
+
177
+ def upload_to_s3
178
+ file = File.find(@file_id)
179
+ S3Client.upload(file.path, bucket: 'my-bucket')
180
+ Rails.logger.info "Successfully uploaded file #{@file_id} to S3"
181
+ end
182
+
183
+ def generate_thumbnails
184
+ ThumbnailService.generate(@file_id)
185
+ end
186
+ end
159
187
  ```
160
188
 
189
+ **Key Features:**
190
+ - **Idempotent**: Same operation won't be executed twice during replays
191
+ - **Automatic Retries**: Failed executions retry with exponential backoff (2^attempt seconds, capped at 32s)
192
+ - **Error Tracking**: All failures are logged with detailed error information
193
+ - **Configurable**: Customize retry attempts and step naming
194
+
161
195
  #### ⏱️ Wait States
162
196
 
163
- ChronoForge supports both time-based and condition-based waits:
197
+ ChronoForge supports three types of wait states, each optimized for different use cases:
198
+
199
+ **1. Time-based Waits (`wait`)**
200
+
201
+ For simple delays and scheduled pauses:
202
+
203
+ ```ruby
204
+ # Simple delays
205
+ wait 30.minutes, "cooling_period"
206
+ wait 1.day, "daily_batch_interval"
207
+
208
+ # Complex workflow with multiple waits
209
+ def user_onboarding_flow
210
+ durably_execute :send_welcome_email
211
+ wait 1.hour, "welcome_delay"
212
+
213
+ durably_execute :send_tutorial_email
214
+ wait 2.days, "tutorial_followup"
215
+
216
+ durably_execute :send_feedback_request
217
+ end
218
+ ```
219
+
220
+ **2. Automated Condition Waits (`wait_until`)**
221
+
222
+ For conditions that can be automatically polled at regular intervals:
164
223
 
165
224
  ```ruby
166
- # Wait for a specific duration
167
- wait 1.hour, :cooling_period
225
+ # Wait for external API
226
+ wait_until :external_api_ready?,
227
+ timeout: 30.minutes,
228
+ check_interval: 1.minute
229
+
230
+ # Wait with retry on specific errors
231
+ wait_until :database_migration_complete?,
232
+ timeout: 2.hours,
233
+ check_interval: 30.seconds,
234
+ retry_on: [ActiveRecord::ConnectionNotEstablished, Net::TimeoutError]
235
+
236
+ # Complex condition example
237
+ def third_party_service_ready?
238
+ response = HTTParty.get("https://api.example.com/health")
239
+ response.code == 200 && response.body.include?("healthy")
240
+ end
168
241
 
169
- # Wait until a condition is met
170
- wait_until :payment_processed,
242
+ wait_until :third_party_service_ready?,
171
243
  timeout: 1.hour,
172
- check_interval: 5.minutes
244
+ check_interval: 2.minutes,
245
+ retry_on: [Net::TimeoutError, Net::HTTPClientException]
246
+ ```
247
+
248
+ **3. Event-driven Waits (`continue_if`)**
249
+
250
+ For conditions that depend on external events like webhooks, requiring manual workflow continuation:
251
+
252
+ ```ruby
253
+ # Basic usage - wait for webhook-driven state change
254
+ continue_if :payment_confirmed?
255
+
256
+ # With custom name for better tracking
257
+ continue_if :payment_confirmed?, name: "stripe_webhook"
258
+
259
+ # Wait for manual approval
260
+ continue_if :document_approved?
261
+
262
+ # Wait for external file processing
263
+ continue_if :processing_complete?
264
+
265
+ # Multiple waits with same condition but different contexts
266
+ continue_if :external_system_ready?, name: "payment_gateway"
267
+ # ... other steps ...
268
+ continue_if :external_system_ready?, name: "inventory_system"
269
+
270
+ # Complete workflow example
271
+ class PaymentWorkflow < ApplicationJob
272
+ prepend ChronoForge::Executor
273
+
274
+ def perform(order_id:)
275
+ @order_id = order_id
276
+
277
+ # Initialize payment
278
+ durably_execute :create_payment_request
279
+
280
+ # Wait for external payment confirmation (webhook-driven)
281
+ continue_if :payment_confirmed?, name: "stripe_confirmation"
282
+
283
+ # Complete order after payment
284
+ durably_execute :fulfill_order
285
+ end
286
+
287
+ private
288
+
289
+ def payment_confirmed?
290
+ PaymentService.confirmed?(@order_id)
291
+ end
292
+ end
293
+
294
+ # Later, when webhook arrives:
295
+ PaymentService.mark_confirmed(order_id)
296
+ PaymentWorkflow.perform_later("order-#{order_id}", order_id: order_id)
297
+ ```
298
+
299
+ **When to Use Each Wait Type:**
300
+
301
+ | Wait Type | Use Case | Polling | Resource Usage | Response Time |
302
+ |-----------|----------|---------|----------------|---------------|
303
+ | `wait` | Fixed delays, rate limiting | None | Minimal | Exact timing |
304
+ | `wait_until` | API readiness, data processing | Automatic | Medium | Check interval |
305
+ | `continue_if` | Webhooks, user actions, file uploads | Manual only | Minimal | Immediate |
306
+
307
+ **Key Differences:**
308
+
309
+ - **`wait`**: Time-based, no condition checking, resumes automatically
310
+ - **`wait_until`**: Condition-based with automatic polling, resumes when condition becomes true or timeout
311
+ - **`continue_if`**: Condition-based without polling, requires manual workflow retry when condition might have changed
312
+
313
+ #### 🔄 Periodic Tasks
314
+
315
+ The `durably_repeat` method enables robust periodic task execution within workflows. Tasks are scheduled at regular intervals until a specified condition is met, with automatic catch-up for missed executions and configurable error handling.
316
+
317
+ ```ruby
318
+ class NotificationWorkflow < ApplicationJob
319
+ prepend ChronoForge::Executor
320
+
321
+ def perform(user_id:)
322
+ @user_id = user_id
323
+
324
+ # Send reminders every 3 days until user completes onboarding
325
+ durably_repeat :send_reminder_email,
326
+ every: 3.days,
327
+ till: :user_onboarded?
328
+
329
+ # Critical payment processing every hour - fail workflow if it fails
330
+ durably_repeat :process_pending_payments,
331
+ every: 1.hour,
332
+ till: :all_payments_processed?,
333
+ on_error: :fail_workflow
334
+ end
335
+
336
+ private
337
+
338
+ def send_reminder_email(scheduled_time = nil)
339
+ # Optional parameter receives the scheduled execution time
340
+ if scheduled_time
341
+ lateness = Time.current - scheduled_time
342
+ Rails.logger.info "Reminder scheduled for #{scheduled_time}, running #{lateness.to_i}s late"
343
+ end
344
+
345
+ UserMailer.onboarding_reminder(@user_id).deliver_now
346
+ end
347
+
348
+ def user_onboarded?
349
+ User.find(@user_id).onboarded?
350
+ end
351
+
352
+ def process_pending_payments
353
+ PaymentProcessor.process_pending_for_user(@user_id)
354
+ end
355
+
356
+ def all_payments_processed?
357
+ Payment.where(user_id: @user_id, status: :pending).empty?
358
+ end
359
+ end
360
+ ```
361
+
362
+ **Key Features:**
363
+
364
+ - **Idempotent Execution**: Each repetition gets a unique execution log, preventing duplicates during replays
365
+ - **Automatic Catch-up**: Missed executions due to downtime are automatically skipped using timeout-based fast-forwarding
366
+ - **Flexible Timing**: Support for custom start times and precise interval scheduling
367
+ - **Error Resilience**: Individual execution failures don't break the periodic schedule
368
+ - **Configurable Error Handling**: Choose between continuing despite failures or failing the entire workflow
369
+
370
+ **Advanced Options:**
371
+
372
+ ```ruby
373
+ durably_repeat :generate_daily_report,
374
+ every: 1.day, # Execution interval
375
+ till: :reports_complete?, # Stop condition
376
+ start_at: Date.tomorrow.beginning_of_day, # Custom start time (optional)
377
+ max_attempts: 5, # Retries per execution (default: 3)
378
+ timeout: 2.hours, # Catch-up timeout (default: 1.hour)
379
+ on_error: :fail_workflow, # Error handling (:continue or :fail_workflow)
380
+ name: "daily_reports" # Custom task name (optional)
381
+ ```
382
+
383
+ **Method Parameters:**
384
+
385
+ Your periodic methods can optionally receive the scheduled execution time as their first argument:
386
+
387
+ ```ruby
388
+ # Without scheduled time parameter
389
+ def cleanup_files
390
+ FileCleanupService.perform
391
+ end
392
+
393
+ # With scheduled time parameter
394
+ def cleanup_files(scheduled_time)
395
+ # Use scheduled time for business logic
396
+ cleanup_date = scheduled_time.to_date
397
+ FileCleanupService.perform(date: cleanup_date)
398
+
399
+ # Log timing information
400
+ delay = Time.current - scheduled_time
401
+ Rails.logger.info "Cleanup was #{delay.to_i} seconds late"
402
+ end
173
403
  ```
174
404
 
175
405
  #### 🔄 Workflow Context
@@ -413,3 +643,27 @@ Please include tests for any new features or bug fixes.
413
643
  ## 📜 License
414
644
 
415
645
  This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
646
+
647
+ ## 📚 API Reference
648
+
649
+ ### Core Workflow Methods
650
+
651
+ | Method | Purpose | Key Parameters |
652
+ |--------|---------|----------------|
653
+ | `durably_execute` | Execute method with retry logic | `method`, `max_attempts: 3`, `name: nil` |
654
+ | `wait` | Time-based pause | `duration`, `name` |
655
+ | `wait_until` | Condition-based waiting | `condition`, `timeout: 1.hour`, `check_interval: 15.minutes`, `retry_on: []` |
656
+ | `continue_if` | Manual continuation wait | `condition`, `name: nil` |
657
+ | `durably_repeat` | Periodic task execution | `method`, `every:`, `till:`, `start_at: nil`, `max_attempts: 3`, `timeout: 1.hour`, `on_error: :continue` |
658
+
659
+ ### Context Methods
660
+
661
+ | Method | Purpose | Example |
662
+ |--------|---------|---------|
663
+ | `context[:key] = value` | Set context value | `context[:user_id] = 123` |
664
+ | `context[:key]` | Get context value | `user_id = context[:user_id]` |
665
+ | `context.set(key, value)` | Set context value (alias) | `context.set(:status, "active")` |
666
+ | `context.set_once(key, value)` | Set only if key doesn't exist | `context.set_once(:created_at, Time.current)` |
667
+ | `context.fetch(key, default)` | Get with default value | `context.fetch(:count, 0)` |
668
+ | `context.key?(key)` | Check if key exists | `context.key?(:user_id)` |
669
+
@@ -0,0 +1,134 @@
1
+ # Example: Webhook-driven Payment Processing Workflow
2
+ #
3
+ # This example demonstrates how to use continue_if for workflows that need to wait
4
+ # for external events like webhook notifications without consuming resources on polling.
5
+
6
+ class PaymentProcessingWorkflow < ApplicationJob
7
+ prepend ChronoForge::Executor
8
+
9
+ def perform(order_id:)
10
+ @order_id = order_id
11
+
12
+ # Store order details in durable context
13
+ context[:order_id] = @order_id
14
+ context[:created_at] = Time.current.iso8601
15
+
16
+ # Step 1: Initialize payment request
17
+ durably_execute :initialize_payment
18
+
19
+ # Step 2: Wait for payment confirmation (webhook-driven)
20
+ # This will halt the workflow until manually retried when webhook arrives
21
+ continue_if :payment_confirmed?, name: "stripe_payment_webhook"
22
+
23
+ # Step 3: Process successful payment
24
+ durably_execute :process_payment_success
25
+
26
+ # Step 4: Send confirmation to customer
27
+ durably_execute :send_confirmation
28
+ end
29
+
30
+ private
31
+
32
+ def initialize_payment
33
+ # Create payment request with external payment provider
34
+ payment_request = PaymentService.create_payment_request(
35
+ order_id: @order_id,
36
+ amount: Order.find(@order_id).total_amount
37
+ )
38
+
39
+ context[:payment_request_id] = payment_request.id
40
+ context[:payment_status] = "pending"
41
+
42
+ Rails.logger.info "Payment request created for order #{@order_id}: #{payment_request.id}"
43
+ end
44
+
45
+ def payment_confirmed?
46
+ # Check if payment has been confirmed by webhook
47
+ payment_id = context[:payment_request_id]
48
+ payment = PaymentService.find_payment(payment_id)
49
+
50
+ confirmed = payment&.status == "confirmed"
51
+
52
+ if confirmed
53
+ context[:payment_status] = "confirmed"
54
+ context[:confirmed_at] = Time.current.iso8601
55
+ end
56
+
57
+ Rails.logger.debug "Payment confirmation check for #{payment_id}: #{confirmed}"
58
+ confirmed
59
+ end
60
+
61
+ def process_payment_success
62
+ # Update order status and inventory
63
+ order = Order.find(@order_id)
64
+ order.mark_as_paid!
65
+
66
+ # Update inventory
67
+ InventoryService.reserve_items(order.items)
68
+
69
+ context[:processed_at] = Time.current.iso8601
70
+ Rails.logger.info "Payment processed successfully for order #{@order_id}"
71
+ end
72
+
73
+ def send_confirmation
74
+ # Send confirmation email to customer
75
+ order = Order.find(@order_id)
76
+ OrderMailer.payment_confirmation(order).deliver_now
77
+
78
+ context[:confirmation_sent_at] = Time.current.iso8601
79
+ Rails.logger.info "Confirmation email sent for order #{@order_id}"
80
+ end
81
+ end
82
+
83
+ # Webhook handler that resumes the workflow
84
+ class PaymentWebhookController < ApplicationController
85
+ def receive
86
+ payment_id = params[:payment_id]
87
+ status = params[:status]
88
+
89
+ if status == "confirmed"
90
+ # Update payment status in your system
91
+ PaymentService.update_payment_status(payment_id, "confirmed")
92
+
93
+ # Find and continue the corresponding workflow
94
+ order_id = PaymentService.find_payment(payment_id).order_id
95
+ workflow_key = "payment-#{order_id}"
96
+
97
+ # Continue the workflow from where it left off (continue_if)
98
+ PaymentProcessingWorkflow.perform_later(workflow_key, order_id: order_id)
99
+
100
+ Rails.logger.info "Payment confirmed via webhook, continuing workflow #{workflow_key}"
101
+ end
102
+
103
+ head :ok
104
+ end
105
+ end
106
+
107
+ # Usage example:
108
+ #
109
+ # 1. Start the workflow:
110
+ # PaymentProcessingWorkflow.perform_later("payment-order-123", order_id: "order-123")
111
+ #
112
+ # 2. Workflow will:
113
+ # - Create payment request
114
+ # - Check if payment is confirmed (initially false)
115
+ # - Halt execution and wait in idle state
116
+ #
117
+ # 3. When webhook arrives:
118
+ # - PaymentWebhookController#receive processes webhook
119
+ # - Updates payment status to "confirmed"
120
+ # - Calls PaymentProcessingWorkflow.perform_later("payment-order-123", order_id: "order-123")
121
+ #
122
+ # 4. Workflow resumes:
123
+ # - Re-evaluates payment_confirmed? (now returns true)
124
+ # - Continues with processing payment success
125
+ # - Sends confirmation email
126
+ # - Completes workflow
127
+
128
+ # Key benefits of continue_if vs wait_until:
129
+ #
130
+ # 1. No resource consumption: No background polling jobs
131
+ # 2. Instant response: Resumes immediately when condition changes
132
+ # 3. Webhook-friendly: Perfect for external event-driven workflows
133
+ # 4. Durable: Survives system restarts and deployments
134
+ # 5. Traceable: All state changes are logged in execution logs
@@ -0,0 +1,159 @@
1
+ module ChronoForge
2
+ module Executor
3
+ module Methods
4
+ module ContinueIf
5
+ # Waits until a specified condition becomes true, without any automatic polling or time-based checks.
6
+ #
7
+ # This method provides a durable pause state that can only be resumed by manually retrying
8
+ # the workflow (typically triggered by external events like webhooks). Unlike wait_until,
9
+ # this method does not automatically poll the condition - it simply evaluates the condition
10
+ # once and either proceeds (if true) or halts execution (if false) until manually retried.
11
+ #
12
+ # @param condition [Symbol] The name of the instance method to evaluate as the condition.
13
+ # The method should return a truthy value when the condition is met.
14
+ # @param name [String, nil] Optional custom name for this step. If not provided, uses the condition name.
15
+ # Useful for tracking multiple calls to the same condition or providing more descriptive names.
16
+ #
17
+ # @return [true] When the condition is met
18
+ #
19
+ # @example Basic usage
20
+ # continue_if :payment_confirmed?
21
+ #
22
+ # @example With custom name
23
+ # continue_if :payment_confirmed?, name: "stripe_payment_confirmation"
24
+ #
25
+ # @example Waiting for external webhook
26
+ # continue_if :webhook_received?
27
+ #
28
+ # @example Waiting for manual approval
29
+ # continue_if :approval_granted?
30
+ #
31
+ # @example Multiple continue_if with same condition but different names
32
+ # continue_if :external_system_ready?, name: "payment_system_ready"
33
+ # # ... other workflow steps ...
34
+ # continue_if :external_system_ready?, name: "inventory_system_ready"
35
+ #
36
+ # @example Complete workflow with manual continuation
37
+ # def perform(order_id:)
38
+ # @order_id = order_id
39
+ #
40
+ # # Process initial order
41
+ # durably_execute :initialize_order
42
+ #
43
+ # # Wait for external payment confirmation (webhook-driven)
44
+ # continue_if :payment_confirmed?, name: "stripe_webhook"
45
+ #
46
+ # # Complete order processing
47
+ # durably_execute :complete_order
48
+ # end
49
+ #
50
+ # private
51
+ #
52
+ # def payment_confirmed?
53
+ # PaymentService.confirmed?(@order_id)
54
+ # end
55
+ #
56
+ # # Later, when webhook arrives:
57
+ # # PaymentService.mark_confirmed(order_id)
58
+ # # OrderProcessingWorkflow.perform_later("order-#{order_id}", order_id: order_id)
59
+ #
60
+ # == Behavior
61
+ #
62
+ # === Condition Evaluation
63
+ # The condition method is called once per workflow execution:
64
+ # - If truthy, execution continues immediately
65
+ # - If falsy, workflow execution halts until manually retried
66
+ # - No automatic polling or retry attempts are made
67
+ #
68
+ # === Manual Retry Required
69
+ # Unlike other wait states, continue_if requires external intervention:
70
+ # - Call Workflow.perform_later(key, **kwargs) to continue the workflow
71
+ # - Typically triggered by webhooks, background jobs, or manual intervention
72
+ # - No timeout or automatic resumption
73
+ #
74
+ # === Error Handling
75
+ # - Exceptions during condition evaluation cause workflow failure
76
+ # - No automatic retry on condition evaluation errors
77
+ # - Use try/catch in condition methods for error handling
78
+ #
79
+ # === Persistence and Resumability
80
+ # - Wait state is persisted in execution logs
81
+ # - Workflow can be stopped/restarted without losing wait progress
82
+ # - Condition evaluation state persists across restarts
83
+ # - Safe for system interruptions and deployments
84
+ #
85
+ # === Execution Logs
86
+ # Creates execution log with step name: `continue_if$#{name || condition}`
87
+ # - Tracks attempt count and execution times
88
+ # - Records final result (true for success)
89
+ #
90
+ # === Use Cases
91
+ # Perfect for workflows that depend on:
92
+ # - External webhook notifications
93
+ # - Manual approval processes
94
+ # - File uploads or external processing completion
95
+ # - Third-party system state changes
96
+ # - User actions or form submissions
97
+ #
98
+ def continue_if(condition, name: nil)
99
+ step_name = "continue_if$#{name || condition}"
100
+
101
+ # Find or create execution log
102
+ execution_log = ExecutionLog.create_or_find_by!(
103
+ workflow: @workflow,
104
+ step_name: step_name
105
+ ) do |log|
106
+ log.started_at = Time.current
107
+ log.metadata = {
108
+ condition: condition.to_s,
109
+ name: name
110
+ }
111
+ end
112
+
113
+ # Return if already completed
114
+ if execution_log.completed?
115
+ return execution_log.metadata["result"]
116
+ end
117
+
118
+ # Evaluate condition once
119
+ begin
120
+ execution_log.update!(
121
+ attempts: execution_log.attempts + 1,
122
+ last_executed_at: Time.current
123
+ )
124
+
125
+ condition_met = send(condition)
126
+ rescue HaltExecutionFlow
127
+ raise
128
+ rescue => e
129
+ # Log the error and fail the execution
130
+ Rails.logger.error { "Error evaluating condition #{condition}: #{e.message}" }
131
+ self.class::ExecutionTracker.track_error(workflow, e)
132
+
133
+ execution_log.update!(
134
+ state: :failed,
135
+ error_message: e.message,
136
+ error_class: e.class.name
137
+ )
138
+ raise ExecutionFailedError, "#{step_name} failed with an error: #{e.message}"
139
+ end
140
+
141
+ # Handle condition met
142
+ if condition_met
143
+ execution_log.update!(
144
+ state: :completed,
145
+ completed_at: Time.current,
146
+ metadata: execution_log.metadata.merge("result" => true)
147
+ )
148
+ return true
149
+ end
150
+
151
+ # Condition not met - halt execution without scheduling any retry
152
+ # Workflow will remain in idle state until manually retried
153
+ Rails.logger.debug { "Condition not met for #{step_name}, workflow will wait for manual retry" }
154
+ halt_execution!
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end