DEV Community

Cover image for How to Pause and Resume Ruby Workflows Mid-Flight
Artur Pañach
Artur Pañach

Posted on

How to Pause and Resume Ruby Workflows Mid-Flight

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
Enter fullscreen mode Exit fullscreen mode

How It Works

1. Start the reactor:

execution = FraudCheckReactor.run(order_id: 42)
execution.status  # => :paused
Enter fullscreen mode Exit fullscreen mode

The reactor runs reserve_inventorycharge_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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
gem 'ruby_reactor', '~> 0.4'
Enter fullscreen mode Exit fullscreen mode

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. 👇

Top comments (0)