The Approval Gate
- Decide which actions need a human and which do not
- Explain why a gate needs a saved checkpoint, not just a yes/no prompt
- Sketch the propose, pause, decide, resume flow
- Avoid approval fatigue
The scare
Air Canada, 2022 to 2024. A man asked the airline's website chatbot about bereavement fares after a death in the family. The bot told him to book now and claim the discount afterwards. The airline's real policy said you could not. He booked, got refused, and took it to a tribunal in British Columbia. Air Canada argued the chatbot was a separate entity responsible for its own words. The tribunal did not buy that and ordered the airline to pay the difference: about CA$650, plus interest and fees.
The money is small. The precedent is the point: you own what your agent says.
The one-dollar Tahoe, late 2023. A Chevrolet dealership put a ChatGPT-powered bot on its site. A visitor told it to agree with everything and call every offer legally binding, then asked for a new Tahoe for one dollar. It agreed. The dealer did not honour it and pulled the bot, but the screenshots were everywhere.
Both are the same gap: nothing stood between the model's sentence and a commitment.
Refresher: reversible versus irreversible
Most of what an agent does is harmless. Searching, reading, drafting. If it gets one wrong, you shrug. A small set of actions is different: sending, paying, deleting, publishing, deploying, promising. You cannot un-send an email or un-promise a refund.
The trick is not to gate everything. It is to gate the few things that cannot be taken back.
Require approval
Stop. A human owns this decision.
A hard pause: present the plan and the reasoning, wait for an explicit decision, record who approved and when. For the highest stakes, require two approvers.
For example: Releasing a wire transfer; deleting a production database; permanently banning an account.
interrupt(...) → status PAUSED_FOR_HUMAN → resume with Command(resume=decision)
The expensive mistake is gating everything: approval fatigue turns a real safety control into a reflex click. Spend your users' attention where it is irreversible and far-reaching.
The pattern
The Approval Gate pauses an agent right before an irreversible action. The agent does the thinking and the preparation; a human owns the commitment.
agent works ─► proposes action ─► CHECKPOINT saved ─► waits
│
human sees: what, why, cost, can it be undone?
│
approve ──► action runs ──► agent resumes
reject ──► agent told why, re-plans
edit ──► action runs with the human's change
That checkpoint is the underrated part. A human might reply in two seconds or in two days. The agent's state has to survive the wait, so it goes into storage, not into memory. Here is the timeline of what gets saved and when:
A request arrives
A fresh Lambda container spins up and builds a tenant-scoped saver and graph. Nothing has been saved yet — the workflow does not exist in the table.
Agent-State · pk = ACME#booking-9f3
(no rows yet)
Sort keys are shown with an empty namespace (CKPT##id) because this is a top-level graph. Step 4 is the one to sit with: the container is gone and the workflow is still perfectly alive.
The full LangGraph mechanics are in the Human-in-the-Loop module, and the serverless memory table course builds a durable checkpoint store on DynamoDB.
Where humans are actually required
Sometimes it is just good sense. Sometimes it is the law. Pick a domain:
Payments
The agent wants to
Release a supplier payment, issue a refund, or move money above a threshold.
Why a human decides
Money movement is effectively irreversible once settled, and it is the single most attractive target for fraud and manipulation.
The rule or the risk
Banking has enforced the four-eyes principle — a second authoriser above a threshold — for decades, and segregation of duties is a standard financial control. Card fraud losses alone run into the tens of billions of dollars globally each year.
In practice
A treasury agent drafts the payment run and stops. A human approves the batch (or just the items over a limit) before anything leaves the account.
Notice the shared shape: the agent does the work — gathering, reasoning, drafting — and a human owns the commitment. That handover is exactly what an interrupt plus a durable checkpoint makes possible.
Build it
RISK = {"search_docs": "low", "draft_reply": "low", "send_email": "high", "issue_refund": "high"}
pending = {} # proposal_id -> proposal (use a database in real life)
def gated_call(gateway, role, tool, args, reason):
if RISK.get(tool, "high") == "low": # unknown tools count as high
return gateway.call(role, tool, args)
pid = f"p{len(pending) + 1}"
pending[pid] = {"tool": tool, "args": args, "reason": reason, "role": role}
return {"ok": False, "code": "awaiting_approval", "proposal": pid}
def decide(gateway, pid, approve, edited_args=None):
p = pending.pop(pid)
if not approve:
return {"ok": False, "code": "rejected"}
return gateway.call(p["role"], p["tool"], edited_args or p["args"])
Notice RISK.get(tool, "high"): a tool nobody classified is treated as risky. Deny by default again.
When it goes wrong: approval fatigue
Gate everything and people stop reading. Gate the few things that matter, batch similar ones ("approve these 12 refunds under 20 in one go"), and track how often a human changes or rejects a proposal. A gate that is never rejected is either perfect or ignored, and it is usually ignored.
- You own what your agent says and does. "The bot did it" did not work for Air Canada
- The gate covers actions the agent may do but should not do alone
- It needs a saved checkpoint so the wait can be as long as a human needs
- Reviewers need the concrete action, the reason, and whether it can be undone
- Gate sparingly, or approvals become a habit rather than a decision
- Pip proposes 40 refunds of 5 each. Design the approval so a human can review it in a minute without approving blindly.
- Why must the agent's state be saved to storage before waiting for approval?