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.
| Step | Forward action Tᵢ | Compensation Cᵢ |
|---|---|---|
| 1 | Load candidate record | (no-op — read-only) |
| 2 | Generate draft email | Discard draft |
| 3 | Send email | Send correction / mark thread voided |
| 4 | Create CRM activity | Delete the activity record |
| 5 | Schedule follow-up | Cancel 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
completedonly; 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:
- Fault injection per step. Force step k to fail for every k, and assert the system ends in the pre-saga state.
- 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).
- Double-compensation. Run compensation twice and assert idempotency holds.
- 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).