LangGraph’s Interrupt Pattern
The Closest Thing to Production HITL
LangGraph’s interrupt_before mechanism handles the pause and resume. It does not handle the design of the decision itself.
The thing nobody tells you about human-in-the-loop for agents is that the hardest part is not the human. It is the loop. Pausing an agent mid-execution, collecting input from somewhere outside the graph, resuming with that input injected into the right state slot, and continuing without corrupting the execution context or skipping a validation step. That is harder than it sounds. Most agent frameworks handle none of it. LangGraph handles most of it, which is why it has become the default choice for any team building agents that need a human approval step.
LangGraph’s interrupt mechanism comes in two forms. The older form is interrupt_before, which you declare at compile time. You pass a list of node names to the interrupt_before parameter of graph.compile(), and the graph pauses execution before each of those nodes runs. The newer form, introduced in the 1.1.x line and stabilized by 1.2.x, is the interrupt() function, which you call from inside a node at runtime. The difference matters. interrupt_before is a static gate. You decide at compile time which nodes are pausable. interrupt() is a dynamic gate. The node itself decides, based on the current state, whether to stop and wait for input. You cannot use the static form to pause conditionally, and you cannot use the dynamic form without understanding what happens to the execution context when the graph pauses.
The static form is simpler and more predictable. You declare the pausable nodes in your graph definition, and every time the graph reaches one, it writes a checkpoint, stores the current state, and returns control to the caller. The caller inspects the state, decides what to do, and calls graph.resume() or graph.update_state() to inject the human’s decision back into the execution. The graph restores the checkpoint, applies the update, and continues from the paused node as if nothing happened.
The dynamic form is more flexible and more dangerous. You call interrupt() inside a node with a payload that describes what you need from the human. The graph writes a checkpoint at that exact point, returns the payload to the caller, and waits. When the caller resumes, the node continues from the line after interrupt() with the human’s input available as the return value. The advantage is that you can pause conditionally. You can check the current state, decide whether pausing is necessary, and only interrupt when the criteria are met. The disadvantage is that any code path that calls interrupt() inside a node creates a state dependency that the rest of the graph must handle. If the node has side effects between the interrupt() call and the node’s return, those effects must be idempotent. The graph cannot rewind those effects if the human denies the request.
The checkpointing layer that makes both forms work is LangGraph’s persistent store, backed by a checkpointer that the user provides at compile time. LangGraph supports SQLite, Postgres, and in-memory checkpointers. The Postgres checkpointer is the one that matters for production. It stores every state snapshot at every interrupt point, which means the entire execution history is recoverable. This is the feature that separates LangGraph from frameworks that treat pauses as transient events. A paused graph in LangGraph is a stored execution that survives process restarts. If the process crashes while the graph is waiting for human input, the graph resumes from the same checkpoint when the process restarts and the caller calls resume() with the same thread ID.
This durability is the real value proposition. It is also the source of the scaling problem. Each paused graph holds a checkpoint in the database. Each checkpoint holds the full state of the graph at the moment of interruption. For a graph with a large context window, a long conversation history, or multiple tool outputs accumulated before the interrupt point, that checkpoint can be substantial. Multiply by hundreds of concurrent agents waiting for human approval, and the storage cost becomes meaningful. LangGraph does not have built-in checkpoint retention policies. If you are running at scale, you need to build your own TTL mechanism for abandoned interrupts, or the database fills up with graphs that nobody ever resumes.
The honest assessment is that LangGraph’s interrupt mechanism is the best option available in open source, and it is still not fully enterprise-grade. It handles the mechanics of pause and resume correctly. It provides durable checkpointing that survives crashes. It supports both static and dynamic interrupt patterns. What it does not provide is the surrounding infrastructure that makes HITL safe at scale. There is no built-in timeout mechanism that escalates an unanswered interrupt to a backup approver. There is no built-in audit log that records what context the human saw when they made the decision. There is no built-in mechanism for batch-approving or batch-denying a set of interrupts from the same class of request. There is a community-built dashboard called LangGraph Studio that lets you view and respond to interrupts, but it is a development tool, not a production approval console.
For a team that needs to deploy HITL today, the right approach is LangGraph’s dynamic interrupt() pattern combined with your own escrow layer. The escrow layer sits between the graph and the human and handles everything that LangGraph does not: timeouts, escalation paths, audit logging, approval UIs, and conditional routing based on the type of request. The graph calls interrupt() with a structured payload that includes a request ID, the action being proposed, the context that supports it, and a required approval tier. The escrow layer captures the interrupt, routes it to the right human or the right automated rule, and calls resume() with the decision. The graph never knows whether the decision came from a human, a timeout-based denial, or an automated rule that matches the request’s characteristics.
The pattern that works in production looks like this. Define the interrupt payload as a typed schema, not a freeform dictionary. The schema includes a request type field that determines the routing logic, a confidence score if the graph can estimate one, a list of the specific tool calls or state mutations being proposed, and the minimum approval tier required. The escrow layer checks the request type against its routing table. If the request matches an automated rule, the escrow layer decides without human involvement. If it matches a human approval tier, the escrow layer creates a notification, starts a timer, and waits. If the timer expires, the escrow layer checks the escalation chain, promotes the notification to the backup approver, and restarts the timer. Only when the escrow layer receives a decision, either from human input or from a timeout rule, does it call graph.resume() with the decision embedded in the state update.
The gap that remains is audit. LangGraph’s checkpointer stores every state transition, which means the raw data for an audit trail exists. Nobody has built the layer that extracts that data into an auditor-readable format. If a regulator asks why a particular interrupt was approved, the answer requires querying the checkpointer for the state at the interrupt point, reconstructing the context the human saw, and correlating it with the external decision log. It is possible. It is not easy. The team that builds a production-grade audit layer on top of LangGraph’s checkpoint store will have solved one of the hardest remaining problems in enterprise agent governance.
If you are building agents with human-in-the-loop right now, LangGraph’s interrupt pattern is the right starting point. Use the dynamic interrupt() form, not the static interrupt_before compile-time gate, because conditional pauses are the only pattern that scales. Build the escrow layer around it that handles timeouts, escalation, and audit. Do not assume that because LangGraph handles the pause and resume, the HITL problem is solved. It handles the loop. You have to design everything else that makes the loop safe.
If this was useful, forward it to one engineer who needs less noise in their feed.


