Human Approval Gates for Agents Without Stalling the Run
An approval gate is a pause-and-resume problem before it is a UX one: persist the run so it can wait, batch the questions so a reviewer reads them, show the diff rather than the intent, expire unanswered requests to deny by default with a visible surface, and store the approval record with the changed artefact.
Before you start
An approval gate is a point where the agent stops, a person decides, and the agent continues. That sentence hides the hard part: the agent has to be able to stop and continue. If your run is a single process holding its plan, its tool results and its partial output in memory, then “wait for a human” means either blocking a process for hours or losing the run when it dies. So the prerequisite is durable run state — the agent’s plan and progress must survive its own process. Which pieces of state that involves is covered in what an agent should persist between steps and between runs; this page assumes you have a place to put a paused run and a way to pick it back up.
You also need to know which operations need a gate at all. Anthropic’s guide to building effective agents describes agents as loops that call tools and take environmental feedback, and recommends checkpoints where a human can review — the point being that most steps do not need one, and the gate belongs at the steps that are expensive to reverse. Deciding which operations fall on which side is a permission-model question, settled before the gate is built.
Steps
-
Make the run resumable, then add the gate.
Do this first, because every later step depends on it. The pattern that works is the one durable-workflow engines use: the run is a function whose progress is recorded as events, so a wait can last minutes or days and the function resumes from where it stopped when a signal arrives. Temporal’s workflow documentation describes exactly this — a workflow blocks on an external signal without holding a process, and its state is reconstructed by replaying its history. You do not have to adopt Temporal to copy the shape: write the pending approval (what is being asked, about which run, at which step) to storage, exit the step, and treat the human’s answer as the event that restarts it. If your agent runs on a developer’s laptop rather than a server, note that the wait outlives the laptop’s lid being closed; where the agent runs decides how much of this you get for free.
The failure mode this prevents is the one you see when the gate is bolted on afterwards: the agent asks in the chat, the tab is closed, and the run is gone. Not stalled — gone, with no record that a question was ever asked.
-
Batch the approvals.
Once the agent can pause, it will be tempting to pause often. Resist that. Asking once about twenty planned operations gets read; asking twenty times gets approved reflexively. A reviewer who has clicked “allow” nineteen times is not reviewing the twentieth, and a gate that trains its reviewers to click through has stopped being a gate.
Practically: let the agent plan a phase — the set of writes, deletions or external calls it intends before the next point where feedback changes the plan — and present that set as one request. The Model Context Protocol’s specification makes the same assumption at the protocol level: tools are model-controlled invocations that a host is expected to put in front of the user for consent, and a host that surfaces each call in isolation with no grouping is technically compliant and practically useless. Group by consequence, not by call.
One caveat: batching only works if the batch is what actually runs. If the agent may re-plan after approval, the approval covered a plan that no longer exists. Either fix the plan at approval time or re-gate when it changes.
-
Show the diff, not the intent.
The request the reviewer sees should be the concrete change: the file diff, the SQL that will execute, the API call with its resolved arguments, the list of the twelve records that will be deleted with their identifiers. Not “I will update the config to enable the new flag” — the reviewer cannot evaluate a description of what the agent means to do without reconstructing what it will actually do, and a description is also where a model’s error hides best. A diff is checked in seconds; a paragraph of intent is trusted or not.
This is another place the resumable-run work pays for itself: to produce the diff, the agent has to compute the change without applying it, hold it across the wait, then apply exactly what was approved. If the change is recomputed after approval it may differ from what was shown, and the reviewer approved something else. Store the artefact under review with the pending request; apply that artefact, not a fresh one.
-
Expire unanswered requests to deny — and surface the expiry.
A request nobody answers has to resolve somehow. Default-allow (“proceed if no objection within an hour”) is convenient and turns every missed notification into an unreviewed change. Default-deny is safer, but only if something surfaces the expired request, or work silently stops and the run sits half-finished with nobody aware it is waiting for a decision that can no longer be given.
So a timeout is two things: the deny itself, and a visible landing place for the denial — a queue, a status the run reports, an alert, something a person looks at. Durable-workflow engines make the mechanism cheap (a timer that fires alongside the wait for the signal), but they do not decide where the expired request goes; that is yours to build. Pick a timeout from how long a reviewer plausibly takes, not from how long the run can wait — the run can wait indefinitely; the reviewer’s attention cannot.
-
Record the approval with the artefact.
Write who approved what, when, and against which version of the diff — and put that record next to the thing that changed. In the pull request, in the migration’s metadata, in the deployment record, in the audit table keyed by the change; not in the chat transcript. The question that arrives later is never “what did the agent and the reviewer discuss”; it is “who approved this specific change”, asked by someone holding the change and not the conversation.
The record needs three identities that are easy to conflate: the agent that proposed the change, the person who approved it, and the run they belonged to. If the agent acts under a person’s credentials, the first two collapse and the record can no longer distinguish “approved by” from “executed as” — which is the argument for giving the agent its own identity. Keep the run identifier too, so the approval can be joined back to a replay of what the agent did around it.
What done looks like
A run reaches a gated phase, writes a pending request containing the concrete diff of everything it intends to do in that phase, and stops without holding a process. A reviewer sees one request per phase, reads a change rather than a plan, and answers it. On approval the stored diff is applied unchanged and the run resumes from its recorded step; on denial or expiry the run resolves as denied and appears in a place someone monitors. The approval — approver, time, diff version, run identifier — is stored on the changed artefact and can be produced later without the transcript. Nothing about the gate depends on the original process still being alive.
Sources
- guide to building effective agentsanthropic.com
- workflow documentationdocs.temporal.io
- specificationmodelcontextprotocol.io
See also
An IDE extension and an MCP server expose the same vendor operations to different callers. Which one is a team decision, and when to run both.
Prevent agent retries from repeating charges, messages, or creates by tying one durable idempotency key to each intended operation.
Agent-provisioned Cloudinary environments lock delivery to one public IP. Uploads succeed, images 404 in the browser. Symptoms, checks and fixes.
How to set deactivation thresholds, isolate control, stop agent work, preserve evidence, and define safe redeployment.