Skip to content
Blog

The Saga Pattern for Multi-Agent Transactions: Compensating Actions

Agent workflows touch payment, inventory, email, and CRM systems — and they fail halfway. Sagas give you long-running transactions with compensating actions instead of pretending rollback exists.

Published on • October 14, 2026

AI Assistant

An agent books a flight, reserves a hotel, then charges a card — and the hotel reservation fails. Now you have a confirmed flight, a charging card, and no guest. Two-phase commit cannot save you: the participants are external services with their own databases, and a distributed lock across a payment provider and a hotel API is not something you get to hold for eleven seconds while a model thinks.

The answer is old — the saga pattern, from Garcia-Molina and Salem’s work on long-running distributed transactions — and it maps onto multi-agent workflows almost too neatly. A workflow is a sequence of local operations, each with a paired operation that undoes it. When a step fails, you don’t roll back; you compensate, in reverse order.

Recent research has made the connection explicit: SagaLLM wraps multi-agent LLM planning in exactly this structure, with compensatory rollback restoring global consistency when a step fails, demonstrated on scenarios like a failed flight reservation automatically triggering reversal of related hotel and train bookings.

The core idea

A saga is a sequence of local transactions T₁, T₂ … Tₙ where each step has a paired compensating operation C₁, C₂ … Cₙ. Each Tᵢ commits on its own system — there is no global lock. If Tₖ fails, the coordinator executes Cₖ₋₁ … C₁ in reverse order, returning every system to a consistent state.

Two rules make this work in practice:

1. Log the compensating action before executing the forward action. You must know how to undo a step before you take it. If the process crashes between the log write and the forward action, recovery finds a logged-but-not-executed step and takes no compensation. If you reversed that order — act first, decide how to undo later — a crash at the wrong moment leaves an operation you cannot reverse.

2. Compensations must be idempotent. The compensation itself can fail and be retried, and after a crash the recovery process may re-run it. cancelReservation(id) must be safe to call twice; “refund unless already refunded” needs the check inside the operation, not in the caller.

Note that a compensation is not an inverse operation in the mathematical sense. You cannot un-send an email. You compensate with a follow-up email; you cannot un-charge a card, you refund. Sometimes the correct compensation is a human task queued for review — and that is a legitimate saga outcome, not a failure of the pattern.

Orchestration or choreography

Where does compensation authority live?

  • Orchestrated saga — a central coordinator knows the full workflow, tracks state, and initiates compensation when a step fails. This is the pattern agent frameworks already give you: a supervisor graph or a state machine with a checkpointer.
  • Choreographed saga — each agent reacts to events; a failure publishes an event that the previously-completed participants observe and respond to. No single point knows the whole plan.

For agent systems, orchestration is usually right. An LLM-driven plan is exactly the “coordinator knows the workflow” shape, and it makes the compensation log a single durable artifact. Choreography scales better across organizational boundaries — and the A2A ecosystem has a proposal (A2A-SAGA) to standardize execute → verify → compensate across agents from different vendors — but within one application it tends to bury the rollback path in implicit event handlers.

A concrete example

An outreach agent: look up a candidate, generate a personalized email, send it, log the activity in the CRM, schedule a follow-up.

StepForward action TᵢCompensation Cᵢ
1Load candidate record(no-op — read-only)
2Generate draft emailDiscard draft
3Send emailSend correction / mark thread voided
4Create CRM activityDelete the activity record
5Schedule follow-upCancel the scheduled task

Step 3 succeeds and step 4 fails. The coordinator walks backward: delete nothing (4 never happened), retract the sent email (3), discard the draft (2). Candidate state is consistent again, and the retraction is queued as a human-readable task because “un-send” does not exist.

The important part is that each step’s compensation was declared before the run started — a table like the one above, encoded as data, not improvised inside an except block.

Implementing it on a graph-based runtime

In LangGraph terms (the cited toolkit for this pattern), a saga is a state machine with a checkpointer and an explicit compensation branch:

from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver

class SagaState(TypedDict):
    completed: list[str]          # step ids that committed
    compensation_log: list[dict]  # durable record of how to undo each step
    error: str | None

async def run_step(state: SagaState, step):
    # 1. Log compensation BEFORE the forward action
    state["compensation_log"].append({"step": step.id, "undo": step.compensation})
    # 2. Execute the forward action
    result = await step.execute()
    state["completed"].append(step.id)
    return state

async def compensate(state: SagaState):
    # Reverse order, idempotent, each retried on its own
    for entry in reversed(state["compensation_log"]):
        if entry["step"] in state["completed"]:
            await safe_retry_idempotent(entry["undo"])
    return state

Wire it with StateGraph, register SqliteSaver (or Postgres) as the checkpointer, and add an interrupt before any step that deserves human sign-off. The checkpointer is what makes the compensation log durable — without persisted state, a crash mid-saga loses the only record of what needs undoing.

Design choices that matter:

  • Mark completion only after the forward action commits. Compensation runs for steps in completed only; a step logged but never finished is left alone.
  • Make the compensation log append-only. It is your audit trail as well as your rollback plan.
  • Retry compensation independently of the original error. A failed cancel-due-to-timeout is a different problem from a failed cancel-due-to-404; the second means it never existed.
  • Escalate to a human when a compensation fails twice. Partial compensation with a visible queue is strictly better than silent inconsistency.

Designing for the failure point

The engineering happens before the first tool call:

  • Enumerate failure points up front. For each step ask: what state does this leave behind if the next step fails? That answer is the compensation.
  • Prefer steps that are naturally reversible. Draft-then-send beats send-then-apologize: put irreversible actions as late in the saga as possible, and gate them with an approval interrupt.
  • Idempotency keys everywhere. Every forward action should carry a key so a retried step does not double-charge; every compensation keys off the same identifier.
  • Timeouts are failures. A step that hangs past its deadline counts as failed and triggers compensation — do not let an agent loop retry a dead endpoint forever.
  • Verification beats assumption. After a step, confirm the effect actually happened (query the system, don’t trust the 200). Sagas with verify-then-advance catch silent failures that would otherwise be compensated too late.

Testing a saga

Because the pattern is explicit, it is unusually testable:

  1. Fault injection per step. Force step k to fail for every k, and assert the system ends in the pre-saga state.
  2. Crash injection between log and action. Kill the process after the compensation log write, restart, and verify recovery does the right thing (nothing to undo for an unexecuted step).
  3. Double-compensation. Run compensation twice and assert idempotency holds.
  4. Compensation failure. Make C₂ fail twice and assert the escalation path fires with enough context for a human.

Property to assert on every run: after the saga terminates (success, compensated, or escalated), no system is in a half-updated state that nobody knows about.

When not to use a saga

Sagas trade isolation for availability. If you need a genuinely atomic all-or-nothing operation — a ledger transfer where intermediate states must never be visible — you want a real transactional store, not compensation. And for a two-step workflow with a single reversible side effect, a plain try/catch with one undo is not a saga; don’t import the machinery for it.

The pattern earns its keep exactly where agents live: many steps, multiple external systems, irreversible-ish actions, and a model in the middle that will occasionally do something you did not anticipate.

Checklist

  • Pair every forward step with a declared compensation before the run starts.
  • Log compensation durably before executing the action; make it append-only.
  • Make compensations idempotent and retry them independently.
  • Put irreversible actions as late as possible, gated by human approval.
  • Persist saga state with a checkpointer; verify effects after each step.
  • Escalate failed compensation to a human instead of looping.
  • Fault-inject every step, crash mid-run, and run compensation twice in tests.

Sources: LangGraph documentation, SagaLLM: Transaction Guarantees for Multi-Agent LLM Planning (arXiv:2503.11951), AWS Prescriptive Guidance — Prompt chaining saga patterns, A2A-SAGA proposal (a2aproject/A2A discussions).