Framework integration

LangGraph human-in-the-loop approval that survives a restart

How interrupt and Command resume a LangGraph run for human review, why the checkpointer is the part that matters, and where the approval decision should actually live.

Updated July 2026Implementation guidelanggraph human in the loop
Built for

Engineers adding approval steps to a LangGraph agent that touches production systems.

Decision supported

How to pause a graph for a person without holding a process open and without losing the run.

The control gap

LangGraph's interrupt gives you a clean pause point, and the pause is only as durable as the checkpointer behind it. With an in-memory saver, a deploy or a crash between the interrupt and the human's answer discards the run. The second problem is placement: an approval implemented as a node in the graph is a decision the agent's own code controls, so a changed prompt or a new branch can route around it.

What good looks like

Runs pause durably, resume with the reviewer's decision attached, and the rule about which actions require review lives outside the graph where a prompt change cannot move it.

  • Use a persistent checkpointer in anything but a local demo, so an interrupted run outlives the process.
  • Interrupt before the side effect, not after, and include the concrete arguments in the payload the reviewer sees.
  • Keep the decision about which actions need approval in policy, so adding a tool does not silently add an unreviewed path.
  • Record the reviewer, the exact arguments approved, and the expiry, and re-check that the arguments did not change on resume.

A production workflow

  1. The graph reaches a node that proposes a consequential action and calls interrupt with the full request.
  2. The run is checkpointed under its thread identifier and the process is free to exit.
  3. A reviewer sees the request, with the tool, target, and arguments, and accepts or rejects it.
  4. The run resumes with a Command carrying the decision, and the node executes only if the arguments still match what was approved.

Copy this

The graph code is short. The two lines that decide whether this works in production are the checkpointer and the argument comparison on resume.

from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.postgres import PostgresSaver   # not MemorySaver

def refund_node(state):
    request = {"tool": "issue_refund",
               "order_id": state["order_id"],
               "amount_cents": state["amount_cents"]}

    # Policy decides IF review is needed. The graph does not.
    decision = authorize(**request)
    if decision.effect == "deny":
        return {"result": f"denied: {decision.reason}"}
    if decision.effect == "require_approval":
        answer = interrupt(request)              # run is checkpointed here
        if answer.get("approved") is not True:
            return {"result": "rejected by reviewer"}
        if answer.get("request") != request:     # arguments changed after review
            return {"result": "rejected: request changed after approval"}

    return {"result": issue_refund(state["order_id"], state["amount_cents"])}

# Resume, possibly in a different process, hours later
graph.invoke(Command(resume={"approved": True, "request": request}),
             config={"configurable": {"thread_id": thread_id}})

The argument comparison is not paranoia. Between the interrupt and the resume the state can be rewritten by another node or by a retry, and an approval for a 40 dollar refund should not settle a 4000 dollar one.

Evidence to require

  • Every interrupt raised, with the thread identifier, tool, and arguments presented.
  • The reviewer, their decision, the time taken, and the expiry applied.
  • Resumed runs where the arguments differed from those approved.
  • Runs abandoned at an interrupt, which indicate a routing or notification gap rather than a policy one.

Buyer checklist

  • Which checkpointer is configured in production, and has a restart mid-interrupt been tested?
  • Can a new tool be added to the graph without an approval rule being considered?
  • Does the reviewer see the concrete arguments, or a summary written by the model?
  • What happens to an interrupt nobody answers, and who is paged?

Practical answers

Common implementation questions

Why not just add an approval node to the graph?

Because the graph is the thing being changed. When the rule lives in the graph, a refactor or a new branch can bypass it, and nobody notices until an unreviewed action executes. Keeping the rule in policy means the check is asked for every call by construction.

How long can a run stay interrupted?

As long as your checkpointer retains it, but the approval should expire well before that. A stale approval is standing access with extra steps, so bind it to one request and give it a short life.

Does this apply to other frameworks?

The pattern does. Any framework with a durable pause can carry it. What changes is the resume mechanism, not the requirement that the decision and the evidence live outside the agent.

Continue the evaluation

Related controls