VOOZH about

URL: https://dev.to/artur_paach_4b9aadd3b8ed/how-to-pause-and-resume-ruby-workflows-mid-flight-4m93

⇱ How to Pause and Resume Ruby Workflows Mid-Flight - DEV Community


Ruby Reactor 0.4.0 introduces interrupts β€” the first native saga pause/resume mechanism in the Ruby ecosystem.

You've built a checkout flow. The user submits their order, you reserve inventory, charge their card, and generate a shipping label. Everything's a Sidekiq job. It works.

Then you get the requirement: "After payment, wait for the fraud detection webhook before shipping."

Suddenly your clean Sidekiq pipeline needs to stop, hold state for an indeterminate amount of time, and resume when Stripe calls back. Your options are:

  1. Polling loop (wasteful, fragile)
  2. Split into two jobs (now you have to manage state yourself)
  3. Temporal / AWS Step Functions (massive infrastructure lift)

None of these feel right. This is where Ruby Reactor interrupts come in.

What is an Interrupt?

An interrupt is a special step that pauses reactor execution mid-flight, persists the entire execution state to Redis, and waits for an external signal to resume. While paused, no Sidekiq jobs are running. No polling. No wasted resources.

class FraudCheckReactor < RubyReactor::Reactor
 input :order_id

 step :reserve_inventory do
 argument :order_id, input(:order_id)
 run { |args| Inventory.reserve(args[:order_id]) }
 undo { |_err, args| Inventory.release(args[:order_id]) }
 end

 step :charge_card do
 argument :order_id, input(:order_id)
 run { |args| Payment.charge(args[:order_id]) }
 undo { |_err, args| Payment.refund(args[:order_id]) }
 end

 # πŸ‘‡ Pause here until Stripe calls back
 interrupt :wait_for_fraud_check do
 wait_for :charge_card
 correlation_id { |ctx| "order-#{ctx.input(:order_id)}" }
 timeout 3600, strategy: :active
 validate do
 required(:status).filled(:string, included_in?: %w[passed failed])
 end
 max_attempts 3
 end

 step :ship_order do
 argument :status, result(:wait_for_fraud_check, :status)
 run do |args|
 args[:status] == "passed" ? Shipping.create_label : Failure("Fraud check failed")
 end
 undo { |_err, args| Shipping.cancel(args[:order_id]) }
 end

 returns :ship_order
end

How It Works

1. Start the reactor:

execution = FraudCheckReactor.run(order_id: 42)
execution.status # => :paused

The reactor runs reserve_inventory β†’ charge_card β†’ then pauses at wait_for_fraud_check. Everything before the interrupt is committed. The execution state is serialized to Redis.

2. Resume when the webhook arrives:

# In your Stripe webhook controller
FraudCheckReactor.continue_by_correlation_id(
 correlation_id: "order-42",
 payload: { status: "passed" },
 step_name: :wait_for_fraud_check
)

The reactor wakes up, validates the payload (using the validate schema), feeds the payload as the interrupt's result, and continues to ship_order.

Why This Matters

The interrupt pattern solves a class of problems that previously forced Ruby developers into bad choices:

Without interrupts With interrupts
Polling loops that burn DB/Redis Zero resource usage while waiting
Manual state management across jobs Reactor handles all state persistence
Hard-to-debug split job chains Single reactor definition = single mental model
No timeout handling (orphaned states) timeout with :active or :lazy strategies
No validation on resume payloads validate block validates incoming data

Real-World Use Cases

Webhook-driven workflows:

  • Stripe payment confirmation β†’ interrupt β†’ continue on payment_intent.succeeded
  • External KYC provider β†’ interrupt β†’ continue on verification complete
  • Async report generation β†’ interrupt β†’ continue when report URL is ready

Human-in-the-loop:

  • Manager approval for large transactions
  • Customer confirmation for subscription changes
  • Support agent review for flagged accounts

Long-running external jobs:

  • Video transcoding (submit β†’ interrupt β†’ continue on complete)
  • ML model training (kick off β†’ interrupt β†’ continue on model ready)

Comparison: How Others Handle This

Tool Pause/Resume Mechanism
Ruby Reactor 0.4.0 βœ… Built-in interrupt Correlation ID, Redis state, timeout
dry-transaction ❌ No support β€”
Trailblazer ❌ Manual only Custom state machine required
Temporal (Go/Node) βœ… Native Signal-based, built into engine
AWS Step Functions βœ… Native Task token, built into service

Ruby Reactor is the only Ruby-native library that offers this pattern without pulling in external workflow engines.

Getting Started

gem install ruby_reactor
gem 'ruby_reactor', '~> 0.4'

Full interrupt docs: github.com/arturictus/ruby_reactor


If you're building workflows that need to pause and wait for external events, give Ruby Reactor a ⭐ and try it in your next Rails app.

Discussion questions: What's your current approach to webhook-driven workflows in Ruby? Polling? Split jobs? Let me know in the comments. πŸ‘‡