AI agents

A Reversal Plan per Write Type: Undoing an Approved Action

Antonio Evans9 min read

A committed write card stamped APPROVED sits left, three labeled paths lead to a soft delete card, an inverse write card with before and after values, and a reconciliation job card.
Key points

Key points

  1. 1An idempotency key stops a repeat of the same request, but the first committed write stays in place until something else reverses it.
  2. 2Pick one reversal mechanism per write type: a soft delete flag for rows you can mark, an inverse write for state you can snapshot, a reconciliation job for state you cannot see end to end.
  3. 3Every reversal record needs the original key, the before and after values, and the actor and reason, so the reversal can be found and audited later.
  4. 4Test each reversal path the same way you test the write path, including what happens when the reversal itself fails halfway.

Why an idempotency key is not an undo

The most common incident in an agent stack is not a wrong answer. It is a duplicate: a refund issued twice, a ticket opened three times, an email sent on every retry of a step that timed out after delivery (Rajat Pandit). Idempotency keys exist for exactly that failure. A request with the same key produces the same result no matter how many times it arrives, so a retry after a timeout cannot create a second effect (Google Cloud).

That protection is real, and it stops at a specific boundary. The key guarantees that repeated attempts converge on one business operation. It says nothing about whether that one operation was the right thing to do. Once the tool call commits, the record exists, the money moved, or the email left the building. Asking the key to undo it is asking a lock to become a time machine.

The finance pattern guide draws the same line: idempotency makes repeat requests converge on one operation, while compensation applies a controlled business action when completed work must be corrected (bitbibit). These are different tools with different jobs.

So the design question splits in two. First: did the write happen once, despite retries? That is the idempotency key's job. Second: was the write correct, and if not, what takes it back? That is the reversal plan, and it has to be chosen per write type, before the agent runs.

A useful way to see the gap is a simple timeline. An agent calls a refund tool with key refund:run-41:step-7:order-882. The provider accepts it. The HTTP response is lost. The platform retries with the same key, the provider returns the stored success, and no second refund happens. The key did its job perfectly. But if the reviewer later realizes order 882 was the wrong order, the key is finished. Nothing in the idempotency record can take back money that already moved. Only a new action can.

The decision rule for choosing a reversal mechanism

Before the table, the rule in one sentence: choose the reversal by what the system can observe and what the storage engine lets you do, not by how the write was triggered.

Three mechanisms cover almost every case.

Soft delete flag. You can mark the row and filter it in every read path. Cheap, fast, reversible, and it preserves history. It only works if every reader honors the flag. One query that forgets the filter turns a deleted record back into a live one.

Inverse write. You record the before and after state, then apply an opposite operation: a credit for a debit, a status set back to its previous value, a compensating event appended to a ledger. This is what the saga pattern calls a compensating transaction: each completed local step is undone by an explicit action, not by an automatic rollback (microservices.io). Compensation can fail too, so it needs its own operation ID, authorization checks and escalation path.

Reconciliation job. You compare independent evidence, such as your internal ledger against a provider's settlement file, and resolve differences. This is the only option when part of the state lives in a system you cannot inspect or control at write time. Reconciliation is not automatic undo. It closes gaps by detecting them and routing exceptions to a person (bitbibit).

Write typeDefault mechanismKey caveats
Insert, internal recordSoft delete flagEvery read path must filter; add a partial index or view so it is hard to forget
Insert, record with dependent child rowsSoft delete flag plus cascade reviewDecide whether children are hidden with the parent or left visible with a parent reference
Update, single fieldInverse writeSnapshot the old value in the same transaction as the update
Update, many fields or computed valuesInverse writeConfirm the inverse is exact; recomputing derived totals needs the original inputs
Soft delete already in placeInverse write (unset the flag)Keep the delete and restore events, not just the flag state
Hard deleteReinsert from a tombstone record, or switch to soft deleteIf you cannot reinsert exactly, the write type is the problem, not the reversal
External side effect, reversible (void a hold, cancel a draft)Inverse write through the same provider APIUse a new idempotency key for the reversal so the provider deduplicates it too
External side effect, irreversible (sent email, filed document)Reconciliation job plus human follow-upThe record is the acknowledgment; track it as a reversal case, not a rollback
Multi-step transaction across servicesSaga compensation, then reconciliationEach step needs its own compensation and its own idempotency key
Batch or bulk writeReconciliation jobA single inverse on ten thousand rows is itself a wide-reach action; gate it

The rule of thumb underneath the table: if you can mark the row, mark it. If you can compute an exact opposite, write it. If you can only compare sources and find the gap, reconcile.

What a reversal record must contain

A reversal that cannot be linked to its original write is not an audit trail. It is a second mystery. The original key is the join column, and the reversal should carry it explicitly.

The referenced sources converge on a similar field set. A useful idempotency record holds the stable command key, the operation type, the business subject, the fields that must not change under the key, the current state, the provider correlation, the attempt history and a retention window (bitbibit). Approval evidence for agent actions adds the immutable state snapshot, the field-level diff between baseline and proposed state, and the model's rationale (AgentDraft). A reversal record is the same idea, pointed backwards.

A record like this is enough to answer "what did we undo, and why" months later:

{
  "reversal_id": "rev-run-58-step-7-order-882",
  "original_idempotency_key": "refund:run-41:step-7:order-882",
  "action": "order.refund",
  "target": { "type": "order", "id": "882" },
  "mechanism": "inverse_write",
  "reversal_key": "rev:run-58:step-7:order-882",
  "before": { "refunded_total_cents": 0, "status": "paid" },
  "after":  { "refunded_total_cents": 15000, "status": "refunded" },
  "reason": "wrong order selected by agent",
  "requested_by": "agent:run-58",
  "approved_by": "[email protected]",
  "approved_at": "2026-02-14T10:12:03Z",
  "state": "completed",
  "provider_reference": "re_1Nxyz",
  "linked_evidence": "approval:evt-9912"
}

The reversal_key is not optional. A reversal that is itself retried without a key can apply twice, which lands you back in the duplicate problem you started with. Derive the reversal key the same way you derive the original: run, step, action, target, but with a distinct namespace so the two never collide (Rajat Pandit). If your platform distinguishes at-least-once from at-most-once execution, remember that at-most-once per retry still does not guarantee the step runs once across the whole workflow; you need a no-retry strategy as well (AWS).

Two more fields earn their place in practice. A mechanism field lets you route the record to the right reconciliation queue. A state field (pending, completed, failed, escalated) tells a support engineer whether the undo actually happened or is still an intention.

Soft delete, inverse write or reconciliation, by write type

Insert of an internal record. Mark it. A deleted_at timestamp and a deleted_by column are the whole mechanism. The hard part is discipline: every read path must filter. Use a database view or a repository layer that excludes deleted rows by default so a new query cannot accidentally see them. Record the soft delete as a separate event, not just a column change, so the reversal history shows a delete and any later restore.

Update of a single field. Inverse write. The write layer should snapshot the old value in the same transaction. If the agent set status from paid to refunded, the inverse sets it back to paid, but only if it is still refunded. A conditional update (WHERE status = 'refunded') prevents the reversal from overwriting a later legitimate change.

Update of computed or derived values. Inverse write is riskier here. If the update changed a running total, the exact inverse depends on whether anything else changed the total in between. Snapshot the old value and subtract the delta, then run a reconciliation check against the source of truth.

Hard delete. If rows are gone, an exact inverse needs a tombstone record: a copy of the deleted row kept somewhere durable, with the original key in the payload. If you cannot afford to keep that copy, the write type is wrong and should become a soft delete. This is a design decision, not a runtime one.

External side effect. Split it by reversibility. A void of an authorization or a cancel of a draft is an inverse write through the same API, with its own idempotency key. A sent email or a filed regulatory document cannot be inverted. The reversal is a record that says "this went out and should not have," plus a reconciliation case that a person follows up. The payment guide maps completed actions to possible compensations the same way: a capture can lead to a refund, and a refund can fail, so both sides need state and escalation (bitbibit).

Multi-step transaction. Use a saga. Each local step gets its own compensating transaction, and if a step fails, the completed steps are undone by explicit compensation rather than a distributed rollback (microservices.io). Then run reconciliation on top, because compensation is not guaranteed to succeed.

Batch writes. Treat the whole batch as one gated action. A reversal of ten thousand rows is a wide-reach action that deserves the same approval gate as the original, and probably the same blast-radius cap.

Gating the reversal the way you gate the write

A reversal is an action. It has a blast radius. The fact that it undoes a mistake does not make it safe.

The action-tier tables used for approval gates draw the line at reversibility and reach: a restart of one pod can proceed within limits, a rollback on the checkout path needs one named approver, and a data deletion needs two or more approvers plus a restore plan (NOFire). A reversal that touches many rows, changes money, or reaches customers belongs in the higher tiers, even if its purpose is corrective.

Two failure modes follow.

The first is the agent reversing its own mistake without oversight. If the agent can write, and the same agent can reverse, you have given the agent a loop that can churn records while looking like recovery. Reversals requested by an agent should pass an approval gate owned by a person, with the evidence in front of them.

The second is confirmation fatigue. Require approval for every reversal, however small, and reviewers stop reading. After enough dialogs, they click approve to clear the queue (Truto). Reserve reversal approval for the tiers where the cost of being wrong is real, and auto-execute the small, narrow ones inside a cap.

The gate also needs something to approve. Pause and resume flows exist in agent frameworks, and they surface pending approvals as interruptions that can be resumed later (OpenAI Agents SDK). But a pause without evidence is a rubber stamp. The approval item should carry the original key, the before and after state, and the reason.

Where the reversal record lives and how to test it

Put the record where the writer can reach it in the same transaction as the original write. For a soft delete, that is the row itself. For an inverse write, a reversals table in the same database, keyed by reversal_id and indexed by original_idempotency_key. For external side effects, a case queue that the reconciliation job reads and updates.

The reconciliation job is not a one-time script. Schedule it, bounded, and make its output a report rather than a silent fix. A useful first pass compares counts and totals per operation type and reports anything that does not match, then routes each mismatch to the right owner. The finance guide's stage list is a good template: internal command accepted, provider result pending or unknown, provider result confirmed, ledger evidence recorded, settlement received, reconciliation matched, with a time expectation and an owner at each stage (bitbibit).

Test the reversal the way you would test the write, with the same scenarios:

TestWhat it proves
Reversal retried twice with the same reversal keyThe reversal is idempotent
Reversal after another write changed the targetThe conditional check blocks an unsafe inverse
Soft-deleted row queried through each read pathNo path leaks the deleted record
Original write committed, response lost, then reversal requestedThe key and the reversal both resolve correctly
Reversal fails halfway through a multi-step undoCompensation and escalation leave a known state
Reconciliation run against a seeded mismatchThe job reports the gap and does not silently fix it

If you run agents on top of a mixed model stack, keep the reversal logic in your own layer rather than in a prompt. The decision belongs in code that can enforce it, and a single API key across model providers such as Quanta Inference keeps the tool-call surface consistent while the reversal record stays under your control.

One last design habit. When you add a new tool that writes, add its row to the decision table before you ship it. If nobody can name the reversal mechanism for a new write type, that is the signal that the tool is not ready, not that the reversal can wait.

FAQ

FAQ

Does an idempotency key prevent a write from happening twice or undo a write that already happened?

It prevents the duplicate. When the same request arrives again, the server returns the stored result instead of running the logic a second time (Google Cloud). The first committed write is untouched, so undoing it requires a separate action chosen for that write type.

What if the write is a hard delete and there is no copy of the row?

Then you cannot reverse it exactly, and the fix is to change the write type before it runs. Keep a tombstone copy or switch the operation to a soft delete flag. A hard delete with no copy is a one-way door, which is why deletions belong in the highest approval tier with a restore plan (NOFire).

How do I stop a reversal from applying twice when the network retries it?

Give the reversal its own idempotency key, derived from the original key plus a reversal namespace, and store the outcome under it. The same retry problem exists on both sides: a duplicate effect on retry is the failure to design against (Rajat Pandit). Test it by sending the same reversal twice and checking that only one took effect.

When should the agent be allowed to reverse its own action?

Rarely, and only inside a cap. A small, narrow, reversible correction can proceed if the blast radius is bounded. Anything that touches money, customers or many rows should wait for a named person, because a self-reversing agent can churn records while looking like recovery (NOFire).

What does the reconciliation job actually do?

It compares evidence produced by independent systems, such as your ledger against a provider file, and reports matches, pending items and exceptions. It closes the gap between "we think it worked" and "we can see it worked" (bitbibit). It is not an automatic undo, and a mismatch should route to a person, not to a silent fix.

Sources

Sources

  1. Retries, Idempotency and Observability in Agentic Loops, AI ...
  2. What is Idempotency? A guide to API reliability
  3. Idempotency, Consistency, Compensation, and Reconciliation
  4. Pattern: Saga
  5. Implementing Human-in-the-Loop Approval JSON Evidence in ...
  6. Idempotency and retries - AWS Durable Execution SDK ...
  7. How to implement runtime policy enforcement for AI agents
  8. Implementing Human-in-the-Loop Approval Workflows for ...
  9. Human-in-the-loop - OpenAI Agents SDK
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles