IntermediateAnalysisAgent Engineering

Human in the loop

Pausing an agent for human approval: how interruption and checkpointing actually work, what persistence does not guarantee, and the evidence that approval gates decay into rubber stamps.

Updated Reviewed

An agent that can send an email, merge a branch or move money needs a point at which a person can say no. The standard implementation is an approval gate: the run pauses before the consequential action, a human is shown what is about to happen, and execution continues once they agree.

This page covers both halves of that, and they pull in opposite directions. The mechanism is well built and works as documented. The control is weaker than almost everyone assumes, and the evidence against it is older than the technology and now includes direct measurement of engineers reviewing agent-written code.

Taking the mechanism seriously and the control’s limits seriously at the same time is the whole skill here.

The mechanism

Pausing a program mid-execution and resuming it later, possibly in a different process, days after it stopped, is not free. It requires the run’s state to exist somewhere durable, and that requirement shapes every implementation.

Interruption and checkpointing

LangGraph’s model is representative. A node calls interrupt() with a value to show the human; the first call raises an exception that halts execution and sends that value to the client. A client resumes by passing Command(resume=...). The documentation is explicit that this only works with persistence enabled: “To use an interrupt, you must enable a checkpointer, as the feature relies on persisting the graph state.”

The checkpointer writes a snapshot of state at each step, keyed by a thread_id, which is the identifier for the run being paused and resumed. Backends range from in-memory, documented for debugging and testing only, through SQLite, to Postgres for production.

# langgraph 1.2.11
from langgraph.types import interrupt, Command

def review_node(state: State):
    decision = interrupt({
        "action": "send_email",
        "to": state["recipient"],
        "body": state["draft"],
    })
    return {"approved": decision == "approve"}

graph = builder.compile(checkpointer=checkpointer)   # required, not optional
config = {"configurable": {"thread_id": "run-42"}}

graph.invoke({"draft": draft}, config)               # halts, surfaces the payload
graph.invoke(Command(resume="approve"), config)      # resumes after the human answers

OpenAI’s Agents SDK expresses the same idea differently: tools are marked needs_approval, paused runs surface ToolApprovalItem entries in RunResult.interruptions, and the run is converted to a RunState which is approved or rejected and then resumed. RunState is explicitly a versioned, serialisable artefact, and the SDK rejects state written by a newer version rather than guessing at forward compatibility.

Both designs treat the paused run as a durable object with a schema. That is the right instinct, and it means an upgrade can invalidate runs that are paused when you deploy.

The behaviour that surprises people

The LangGraph reference states it plainly, and it is the detail most often missed:

The graph resumes from the start of the node, re-executing all logic.

This is not input(). Everything in the node before the interrupt() call runs a second time on resume. If that code charged a card, posted a message or wrote a row, it does so again. The fix is structural: a node containing an interrupt should do nothing before it except assemble the payload for the human.

What persistence does not give you

A checkpointer persists graph state. Reading past what it does into what you might want is where production incidents come from.

Guaranteed Not guaranteed
The run survives a process restart and can resume days later Side effects are idempotent across a resume
Prior steps are durable, so you can replay or inspect them External systems are rolled back if a tool half-completed before the pause
State can be read back for time-travel debugging The resumer is the same person, or a person at all
The run waits indefinitely Anything times out or escalates on its own

The third row deserves emphasis. Command(resume=...) accepts a value. Nothing in the framework establishes that the value came from an authorised human who looked at anything. Authentication, authorisation and audit of the approval are the application’s job, and if you do not build them the gate records a decision without recording who made it.

The fourth matters for anything unattended: a paused run waits, forever, by default. A loop with an escalation point and no timeout is a loop that can stop permanently at three in the morning.

Which actions to gate

There is no published rubric. Anthropic’s guidance is principle-level, that agents “can then pause for human feedback at checkpoints or when encountering blockers”, alongside a recommendation for stopping conditions such as a maximum number of iterations. OpenAI’s documentation supplies the needs_approval flag and leaves the policy to you. Neither vendor enumerates thresholds, and it is better to know that than to assume a standard exists.

What practitioner guidance converges on is two dimensions:

  • Reversibility. Can the action be undone by a compensating action? Issuing a refund is reversible in principle and awkward in practice. Sending an email to a customer is not reversible at all.
  • Blast radius. Does one invocation touch one record or fifty thousand? A tool that takes an identifier and a tool that takes a filter are different risk classes even if the code is shared.

To which the material above adds a third that is easy to miss:

  • Replay safety. Given that resume re-executes the node, is this action safe to perform twice? If not, it needs an idempotency key rather than an approval dialogue.

Anthropic’s own submission on agentic security makes the practical argument against gating everything: if every action requires approval, the efficiency benefit disappears and the person may as well do the task themselves. That constraint is real, and it is exactly what makes the next section a problem, because the pressure to gate less runs directly into the evidence that gating more makes each gate weaker.

Why the gate is weaker than it looks

Everything so far treats the human as a reliable checker. Forty years of human factors research says they are not, and the finding has now been reproduced on agent-generated code specifically.

The literature

Lisanne Bainbridge set out the structural problem in 1983, before any of this existed. Automating a process removes the easy work and leaves the operator the parts nobody could automate, while depriving them of the routine practice that kept them competent to do it. The human is left with the hardest cases and the least preparation for them.

The failure mode has a name and a measured shape. Automation bias produces omission errors, missing an event because the system did not flag it, and commission errors, following the system against other available evidence. The most cited synthesis, Parasuraman and Manzey’s 2010 review, reaches a conclusion that ought to change how these gates are designed:

Automation bias occurs in both naive and expert participants, cannot be prevented by training or instructions, and can affect decision making in individuals as well as in teams.

Expertise does not protect against it. Telling people to be careful does not either. Any design whose safety argument rests on reviewers being experienced and attentive is resting on something the evidence does not support.

The measurement on agent code

A 2026 study looked directly at engineers reviewing pull requests from coding agents. Using a longitudinal within-reviewer design over 400 repeat reviewers and 11,429 reviews across seven months, it compared each reviewer’s early and late episodes:

Measure Change
Approval rate 30.1% to 36.8%
Cumulative gap, first to tenth experience decile +14.5 percentage points
Inline comments per review −22%
Median review latency +3.5×

The controls are what make it interesting. The effect is experience-driven, persisting after controlling for calendar time, so it is not that agents simply got better that year. It is agent-specific: human-authored pull requests moved the other way over the same period. And it is not explained by the work getting easier, since median pull request size stayed flat.

The combination is the finding. Reviewers approved more, commented less, and took longer in queue. The authors read this as “most consistent with reflexive habituation under growing workload rather than rational trust calibration alone”.

Two caveats belong with it. The paper is a five-page workshop paper, not a full study, and it observes correlation. The authors themselves note they cannot rule out that agent output genuinely improved. But the direction matches what the human factors literature predicted decades earlier, which is the reason to take a small study seriously here rather than wait.

Where that leaves the pattern

Madeleine Elish’s term for the endpoint is the moral crumple zone: as a car’s crumple zone absorbs impact to protect the occupant, the human placed inside a highly automated system can end up absorbing the moral and legal consequences when the system fails, protecting the integrity of the system at the expense of the nearest operator.

An approval gate that everyone clicks through has become exactly that. It has not reduced the probability of a bad action. It has established who signed for it.

Designing a gate that does something

The literature is more helpful about mitigations than it first appears, and what recurs is not better training but accountability and required verification: designs where the reviewer has to produce something, not merely consent.

  • Gate by risk class, not per tool. One binary flag on every dangerous tool produces one identical dialogue, and the fiftieth looks like the first. Distinct classes with distinct presentations keep the irreversible ones from being answered by muscle memory.
  • Make approval require an artefact. Typing the number of affected records, or the name of the target, forces the reviewer to obtain a fact from the request. Consent is one click; verification is not.
  • Show the diff, not the intent. An approval dialogue describing what the agent means to do is asking the reviewer to trust the description. Show what will change.
  • Budget the gates. Every gate you add spends the same finite attention. Fifty approvals a day is a rubber stamp with extra steps, and the correct response to too many is usually to reduce the agent’s reach rather than to add reviewers.
  • Instrument the gate itself. Approval rate, time to decision, and comment volume per reviewer over time are measurable, and the study above shows what the decay looks like. If you never measure them you will not notice the control failing, because a failing gate and a working gate produce the same green tick.
  • Never rely on the model to ask. A gate implemented as an instruction telling the agent to check with a human before doing anything risky is evaluated by the same context an attacker may control. The check must sit in the code path that executes the action, which is the argument made in full under agent harnesses.

What this page does not tell you

It does not tell you not to use human approval. For irreversible, high-blast-radius actions the realistic alternative is usually no control at all, and a decaying control beats an absent one.

It does tell you not to count it twice. A system whose safety case is an approval gate has one control whose effectiveness decays with use and is not currently measured by most teams who rely on it. Treat the gate as one layer among several, keep the agent’s reach small enough that few actions need gating at all, and measure whether your reviewers are still reviewing.