SagaForge

Your checkout flow works until the settlement webhook lands early, lands twice, or payment fails after inventory was already reserved. SagaForge is the saga engine for workflows the outside world drives.

SagaForge is a gem over your existing database and ActiveJob backend: two tables, a handful of jobs, no separate server. Every incoming event is persisted before it is processed, early arrivals park until the saga is ready, each step commits atomically, and when a step declares failure the steps that already committed unwind in reverse. Works with any ActiveJob backend on Rails 7.1+.

Gem Version CI License: MIT

The SagaForge dashboard: the saga list with state badges, filter chips, and per-class scoping
The management dashboard, live over your sagas. See the full tour →

Install

# Gemfile
gem "saga_forge"
bundle install
rails g saga_forge:install
rails db:migrate

Want SagaForge's tables in their own database? rails g saga_forge:install --database=saga_forge.

The 30-second tour

A saga is one file that reads top to bottom. States exist only where the next fact comes from outside; everything else chains inline:

class OrderFulfillmentSaga < SagaForge::Base
  correlate_by :order_id

  # Create a PENDING payment intent; settlement arrives later, by webhook.
  start_with :order_placed, compensate: :cancel_payment do |saga, payload|
    saga.context[:items] = payload[:items]
    intent = PaymentGateway.create_intent(payload[:total], idempotency_key: saga.correlation_id)
    saga.context[:intent_id] = intent.id
  end                                          # falls through to :awaiting_settlement

  during :awaiting_settlement, on: :payment_settled, compensate: :release_inventory do |saga, _|
    Warehouse.reserve(saga.context[:items], key: saga.correlation_id)
    Shipping.dispatch(saga.correlation_id)
  end                                          # falls through to :completed

  during :awaiting_settlement, on: :payment_failed do |saga, payload|
    saga.fail! reason: payload[:decline_code]  # unwinds committed steps, in reverse
  end

  finish_with :completed

  compensation(:cancel_payment)    { |saga| # cancel or refund, guarded by context }
  compensation(:release_inventory) { |saga| # put the stock back }
end

# The world talks to it by publishing facts, e.g. from a webhook controller:
SagaForge.publish :payment_settled, event_id: webhook.id, order_id: 42

The webhook can arrive before the saga is ready (it parks and re-delivers itself), twice (the event_id makes the duplicate a no-op), or as a failure (every committed step unwinds, last first, using the context each step saved).

What you get

SagaForge or ChronoForge?

They are siblings. If the workflow mostly waits for the outside world and needs an unwind story, it is a saga. If it is a sequence of steps your own code drives and should survive crashes and retries, it is a ChronoForge workflow. They compose: a saga step that needs resumable sub-steps kicks off a ChronoForge workflow and parks on its completion event.

Documentation