Human-in-the-loop with LangGraph: when (and how) to interrupt your agent
TL;DR
interrupt()is the primitive that makes human-in-the-loop LangGraph possible: it pauses a graph node, serializes state, and waits for an external signal before continuing.- A persistent checkpointer (SqliteSaver for local work, PostgresSaver for production) is a hard prerequisite (without one,
interrupt()raises an error). - Place a human gate only on nodes with irreversible side effects or compliance requirements, not as a general safety net.
- Resumption uses
Command(resume=value), not a graph replay from scratch. - State TTL and thread abandonment must be handled by your own logic; LangGraph does not expire frozen threads automatically as of 2026.
Interrupting an agent mid-execution is not the same as adding a retry loop or lowering a confidence threshold. Human-in-the-loop LangGraph works at a different layer: it pauses the graph at a specific node, persists the full state to a database, and waits indefinitely for an external signal before continuing.
The cost you are weighing is concrete. A 30-second manual approval step on a $500 Stripe charge costs almost nothing compared to a disputed transaction, a refund workflow, and the engineering time to audit what the agent did. This article covers when that trade-off favors interruption and exactly how to wire it.
What human-in-the-loop LangGraph actually means
In LangGraph, a graph is a directed structure made of nodes (Python functions) connected by edges (conditional or unconditional transitions). Each node reads and writes a typed state object. When you call interrupt() inside a node, execution halts immediately, the current state is serialized to a checkpointer, and the graph returns control to the caller without advancing further.
This is the core of the human-in-the-loop LangGraph model: a graph-level pause primitive, not a webhook and not a retry mechanism.
Three requirements apply:
- A checkpointer must be attached to the graph. Without one,
interrupt()fails because there is nowhere to persist state. - The caller must supply a thread_id in the config so the runtime can locate the frozen state later.
- Resumption happens via
Command(resume=value), which injects the human's input and advances execution from the interrupted node.
LangGraph ships three checkpointer implementations. MemorySaver stores state in a Python dict and is lost when the process exits (useful only for unit tests). SqliteSaver (from langgraph-checkpoint-sqlite) persists to a local SQLite file, survives process restarts, and adds negligible latency for single-machine development. PostgresSaver (from langgraph-checkpoint-postgres) persists to a PostgreSQL database, costs one network round-trip per checkpoint (typically 5 to 20 ms depending on host proximity), and is the correct choice for any multi-process or production deployment.
The choice matters for human oversight workflows. If your approval loop spans minutes or hours, only SQLite or PostgreSQL guarantee the frozen state will still be there when a reviewer responds.
Full API reference is in the LangGraph documentation.
When to add a human checkpoint: a decision framework
Not every node benefits from a human gate. Adding supervised execution everywhere increases latency without reducing risk. The decision rests on three factors.
Side effect reversibility. If the node is about to send an email, charge a card, write to a production database, or delete a record, a review workflow is worth the wait. These actions cannot be undone cheaply.
Model confidence. If the agent selected a tool or generated a plan from ambiguous input and the downstream action is expensive or visible to an end user, an agent checkpoint adds value. If the model call is low-stakes and the output can be retried trivially, it probably does not.
Compliance requirements. Financial services workflows, GDPR deletion requests, and healthcare data pipelines often require a documented approval before any mutation. Here the human gate is a legal requirement, not an engineering preference.
| Trigger | Reversible? | Add human gate? |
|---|---|---|
| Stripe charge over threshold | No | Yes |
| Email to external recipient | No | Yes |
| SQL DELETE or UPDATE | Rarely | Yes |
| Read-only API call | Yes | No |
| Draft text generation | Yes | No |
| Low-confidence classification | Depends | Maybe |
The asymmetry drives the decision. A short wait for an agent checkpoint on a high-stakes transaction is negligible compared to the cost of correcting the mistake afterward.
Wiring interrupt() in LangGraph: step-by-step
The current API (LangGraph 0.2 and later) calls interrupt() directly inside a node. The older NodeInterrupt exception pattern was retired in mid-2024 and should not appear in new code.
from langgraph.types import interrupt, Command
from langgraph.graph import StateGraph, END
from langgraph_checkpoint_sqlite import SqliteSaver
def review_node(state):
human_input = interrupt({"action": state["pending_action"], "amount": state["amount"]})
return {"approved": human_input["approved"]}
builder = StateGraph(dict)
builder.add_node("review", review_node)
builder.set_entry_point("review")
builder.add_edge("review", END)
with SqliteSaver.from_conn_string("checkpoints.db") as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "order-1234"}}
graph.invoke({"pending_action": "charge_card", "amount": 500}, config)
graph.invoke(Command(resume={"approved": True}), config)
When interrupt() is called, the runtime serializes state and suspends. The first invoke returns a result indicating the graph is paused. The second call, passing Command(resume=...), restores state from the checkpointer and continues from the interrupted node.
Checkpointer setup
For local development, SqliteSaver.from_conn_string("checkpoints.db") is sufficient. The file persists between runs, so you can pause a graph, restart your script, and resume correctly.
For production:
from langgraph_checkpoint_postgres import PostgresSaver
with PostgresSaver.from_conn_string(os.environ["POSTGRES_URL"]) as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
The PostgreSQL checkpointer supports concurrent threads, which is necessary when multiple approval workflows run in parallel across different orders or users.
Resuming graph execution after human input
Command(resume=value) is the only supported way to resume a suspended graph. The value is whatever your node expects from the reviewer: a boolean, a corrected string, or a structured dict. The runtime injects it as the return value of interrupt() and continues execution normally.
If a single thread contains multiple interrupt() calls, each Command(resume=...) advances to the next one in sequence.
HITL in production: state TTL, latency, and LangGraph Platform
Three operational concerns come up when supervised execution moves to production.
A human may never respond. The checkpointer holds frozen state indefinitely unless you implement a cleanup policy. For most deployments, a background job identifies threads older than a defined TTL (24 or 48 hours, typically), marks them as abandoned, and emits an alert. LangGraph does not enforce TTL natively as of early 2026, so this logic belongs in your application.
Multi-turn approval flows, where initial review is followed by a manager sign-off, are handled by placing multiple interrupt() calls in the graph. The thread_id ties them together, and state accumulates across all resume calls, so later nodes see the full history of human inputs. Honestly not sure why more teams don't use this pattern (it's pretty clean once you see it work).
For teams that want to avoid running their own PostgreSQL checkpointer, LangGraph Platform provides a managed backend. The developer tier is available at no cost for self-hosted open-source deployments. A managed cloud tier introduces per-execution billing, which becomes relevant as your checkpoint volume scales. LangSmith integrates with the platform to trace interrupted threads, inspect state snapshots at each node, and debug cases where a graph resumed with an unexpected value.
LangGraph HITL patterns in 2026: what changed and what to watch
The interrupt() API reached its current stable form with LangGraph 0.2 in mid-2024, replacing the NodeInterrupt exception. In late 2024, the team added streaming support for interrupt events: a client application can now receive a structured event the moment a graph pauses, rather than polling for state changes.
As of early 2026, two areas are actively evolving in the human-in-the-loop LangGraph ecosystem. Structured approval schemas are moving toward typed Pydantic payloads as the standard for the value passed to interrupt(). This lets the reviewer UI validate input before passing it back via Command(resume=...), reducing bugs where a graph resumes with a malformed value.
Multi-agent HITL routing is the other development. In architectures where a supervisor agent delegates to sub-agents, human oversight can be routed at the supervisor level rather than embedded deep in each sub-agent. The LangChain team documented this pattern in multi-agent tutorials updated in Q1 2025.
A January 2026 post on the Elastic engineering blog described a flight operations system using LangGraph's interrupt primitive for flight plan approval, with a PostgreSQL checkpointer and a 4-hour state TTL. That reference is worth reading as a named production case outside a tutorial context. The source code and release notes for all checkpoint packages are tracked in the langchain-ai/langgraph GitHub repository.
Key takeaways
Human-in-the-loop LangGraph comes down to three decisions. First, place interrupt() only at nodes with irreversible side effects or compliance requirements, not as a generic safety net. Second, match your checkpointer to your deployment: SqliteSaver for prototypes, PostgresSaver for any multi-process production environment. Third, design your state TTL strategy before launch, since LangGraph does not expire frozen threads automatically.
The 2026 direction is toward typed approval schemas and supervisor-level HITL routing in multi-agent architectures 🚀
Human gates belong only on irreversible actions, not as a safety net for every agent node. The production CLAUDE.md template in the kit covers agent rules that keep side effects intentional and auditable, so you know exactly where interruption matters.