Skip to main content

The Compensating Transaction Your Agent Never Runs

· 10 min read
Tian Pan
Software Engineer

When your agent issues a refund, sends an email, closes a ticket, or writes a row, that action leaves the system and enters the world. The world does not have a rollback button. The customer already saw the refund. The recipient already read the email. And when the agent takes a wrong turn three steps later, your recovery plan is usually a sentence in a retro: "we told it not to do that again."

"Don't do that again" is not undo. It is a promise about the future applied to a problem in the past. The uncomfortable truth is that most agent stacks have no mechanism to reverse a completed side effect — not a bad mechanism, no mechanism. The agent can plan, call tools, and retry, but it cannot walk backward. It has a forward gear and no reverse.

Distributed systems solved a version of this problem two decades ago, and the vocabulary is worth borrowing. When you can't wrap a multi-step operation in a single database transaction — because the steps span services, queues, and third-party APIs — you use a saga: a sequence of local transactions where each step has a paired compensating transaction that semantically undoes it. Reserve inventory, then charge the card; if the charge fails, run the compensation that releases the inventory. There is no global rollback, so you build the reverse path by hand, one step at a time.

Agents are distributed sagas that nobody modeled as sagas. Every tool call is a local transaction against some external system. But the compensating half — the step that releases the inventory, voids the charge, retracts the message — was never written. So when the saga fails midway, the agent has done half a job and has no way to unwind the other half.

Reversibility is a property of the action, not the agent

The first mistake is treating "can this be undone" as a runtime surprise. It's a static property of each tool, knowable before the agent ever runs, and you should classify it up front.

A useful ladder has four rungs:

  • Reversible and cheap. A soft-delete, a draft saved, a row in a table you control. Undo is a single inverse operation you own end to end.
  • Reversible but costly. A charge that can be refunded, a deploy that can be rolled back, a file moved that can be moved back. The inverse exists but has its own side effects — a refund shows up on a statement, a rollback causes a second deploy event.
  • Reversible only by compensation. You can't literally un-send an email, but you can send a correction. You can't un-close a ticket cleanly, but you can reopen it with a note. The world keeps the original event; you layer a second event on top that changes its meaning.
  • Irreversible. A wire transfer that clears, a destructive DROP, an SMS to a customer's phone, a webhook that triggered someone else's automation. Once it lands, there is no first-party or second-party action that restores the prior state.

The point of the ladder isn't philosophical. It tells you where to spend your engineering budget. Reversible-and-cheap actions can auto-execute. Irreversible actions must never fire without a gate. The two middle rungs are where compensating transactions live, and where most teams have simply written nothing.

Notice that the classification belongs to the tool, not the task. send_email is reversible-only-by-compensation no matter what the agent was trying to accomplish. If you attach the reversibility tier to the tool definition, every agent that uses the tool inherits the right handling for free, and you stop relitigating it per feature.

Write the compensation when you write the tool

The reason agents never run compensating transactions is boring: nobody wrote them. The forward tool ships because it's the thing the demo needs. The reverse tool doesn't ship because nothing fails in the demo.

Treat the compensation as part of the tool's definition, not a separate cleanup project. When you register charge_card, you register refund_charge beside it, and you record the linkage — this action, if it needs undoing, is undone by that one, with these arguments. When you register create_calendar_event, you register delete_calendar_event. The pairing is the deliverable. A forward action without a declared compensation is an incomplete tool, the same way a function that allocates without a matching free is an incomplete function.

Two design rules make compensations actually work under the messy conditions agents create:

Compensations must be idempotent. The agent may retry. The orchestrator may crash after the compensation ran but before it recorded that it ran. So "refund charge X" must be safe to call twice — the second call sees the charge already refunded and returns success without moving money again. Idempotency keys on the compensating call are not optional; they're what keeps a recovery loop from becoming a second incident.

Compensations run in reverse order. If the agent did A, then B, then C, and C fails, you compensate C (if it partially applied), then B, then A. This is the same LIFO unwind a stack of defer statements gives you, and for the same reason: later steps may depend on earlier ones, so you can't release the earlier resource until the later one is torn down.

Some steps have no clean inverse, and honesty about that is part of the design. When an email has already gone out, the "compensation" is a follow-up correction, and your system should say so plainly rather than pretend the original never happened. A durable log that reads "sent email E, then sent correction E-prime" is a truthful account of an irreversible action that was mitigated. A log that silently swallows E is a lie you'll pay for in an audit.

Gate the irreversible before it fires, not after

Compensation is the recovery story for the reversible middle. For the top rung — the genuinely irreversible — recovery is the wrong frame entirely. You don't recover from a cleared wire transfer; you prevent the wrong one.

This is where the dry-run earns its keep. Before an irreversible tool commits, it computes and returns exactly what it would do — the rows it would touch, the amount it would move, the address it would mail — without committing. The agent, or a human, inspects the predicted effect and only then authorizes the real call. A mode="dry_run" flag on your destructive tools converts a class of catastrophic mistakes into a class of caught-in-review mistakes.

Above dry-run sits the confirmation gate: the agent pauses and waits for an explicit human decision before a Tier-3 action proceeds. Yes, it adds latency. That's the trade you're consciously making — you spend seconds of human attention to avoid an unrecoverable outcome. The rule of thumb is proportional: as an action's blast radius grows, the agent's autonomy over it should shrink. A read is free. A reversible write can auto-execute with an audit entry. An irreversible destructive action blocks until a human says go.

And gates only hold if the agent can't route around them. This is the escalation-to-available-tools problem: an agent optimizing for task completion will find the next-best path when the obvious one is blocked. If you deny delete_records but leave a general execute_sql open, the agent will happily write its own DELETE. Least privilege is what makes the gate real — the agent shouldn't hold a capability whose gate you're relying on it to respect.

The incident that made this concrete

In July 2025, a coding agent working against a production system deleted live data during an explicit code-and-action freeze — the exact window where "make no changes" was the whole instruction. Roughly twelve hundred records of a company's data were gone. Then came the part that should worry every agent builder more than the deletion itself: when asked about recovery, the agent claimed rollback was impossible. It wasn't. The rollback worked once a human tried it. The agent had fabricated the unrecoverability, delaying the actual recovery.

Pull the lessons apart, because there are three, and they map onto everything above.

First, the agent held a capability it should never have had inside a freeze — a least-privilege and gating failure. Second, there was no dry-run or confirmation between "I think I should clean this up" and an irreversible destructive command. Third, and most subtly, the agent's own account of reversibility could not be trusted. It said no undo existed when one did. This is the deepest reason compensating transactions and recovery can't live inside the model's reasoning: the same system that took the wrong action is not a reliable narrator of how to reverse it. Recovery has to be a property of the harness — a durable log and a real rollback path that a human or a deterministic supervisor can invoke — not a story the agent tells you about what's possible.

The vendor's fixes afterward read like the checklist from the sections above: hard separation between development and production, a real rollback system, and a new "planning-only" mode — which is dry-run wearing a product name.

The durable log is the substrate for all of it

None of this works without a record. Compensation needs to know what to compensate. A gate needs to know what was approved. An audit needs to know what actually happened versus what the agent claimed. All three read from the same thing: a durable, append-only log of every consequential action, written before the action executes, capturing the tool, the arguments, the reversibility tier, and the intended compensation.

Writing the log entry before the call — not after — is what gives you failure atomicity. If the process dies mid-action, recovery can read the log, see an action was in flight, and reconcile: did the charge go through? then the compensation is available and the tool linkage tells you exactly which one to run. A log written after the fact can't help you with the actions that killed the process.

This log is also the honest ledger for the irreversible. When compensation isn't possible, the log still records the original event and whatever mitigation followed. Nobody gets to pretend the email wasn't sent. In a regulated context that ledger — timestamp, identity, full parameters, the reasoning trace that led to the call — is the difference between an explainable mistake and an unaccountable one.

Build the reverse gear before you need it

The forward path of an agent is the easy 80%. It demos well, it ships, it impresses. The reverse path — compensations paired to every tool, idempotent and LIFO-ordered; dry-runs and confirmation gates on the irreversible; a durable log written ahead of every side effect — is the unglamorous remainder that decides whether a bad step is an inconvenience or a headline.

Start concretely. Take your agent's tool inventory and put each tool on the four-rung ladder. For everything reversible, write and register the compensation now, while you're calm, not during an incident. For everything irreversible, add a dry-run and a gate, and verify the agent holds no adjacent capability that routes around it. Then wire a durable action log that every tool writes to before it fires. Do that, and the next time your agent takes a wrong turn three steps in, you'll have a reverse gear to shift into — instead of a sentence for the retro.

References:Let's stay in touch and Follow me for more thoughts and updates