
Your Agent Shouldn't Act Without Asking: Human-in-the-Loop Approval With LangGraph interrupt()
Chris Harper
3 min read
Jul 10, 2026 · 04:03 UTC
TL;DR: Call interrupt() inside any LangGraph node to freeze execution, snapshot state to a checkpointer, and wait for a human to approve or reject — then resume from exactly that line with Command(resume=...).
What you'll be able to do after this:
- Pause any agent before a risky action (file delete, email send, external API call) and gate it on human approval
- Persist the paused state so you can resume from the exact checkpoint — even across server restarts — using a database-backed checkpointer
- Accept, reject, or redirect an agent mid-run by passing any value through
Command(resume=...)
Human-in-the-loop (HITL) is a critical pattern for production agents: let the model plan and propose, but require a human sign-off before anything irreversible happens. LangGraph's interrupt() function is the cleanest way to wire this in.
How it works
interrupt() is a function you call inside a node. When the graph hits it, LangGraph raises a special exception that the runtime catches — it snapshots the full graph state to the checkpointer and returns control to the caller with the interrupt payload. No polling, no background thread: the graph simply pauses.
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState
import os
def risky_action_node(state: MessagesState):
# Pass a payload describing what we want to do
approved = interrupt({
"action": "delete_file",
"path": state["target"]
})
if not approved:
return {"messages": [{"role": "assistant", "content": "Action cancelled."}]}
os.remove(state["target"])
return {"messages": [{"role": "assistant", "content": f"Deleted {state['target']}."}]}
builder = StateGraph(MessagesState)
builder.add_node("risky", risky_action_node)
builder.set_entry_point("risky")
# A checkpointer is REQUIRED — it stores the paused state
graph = builder.compile(checkpointer=InMemorySaver())
InMemorySaver is fine for development. Use PostgresSaver or SqliteSaver for production — the state survives process restarts.
Running and resuming
config = {"configurable": {"thread_id": "session-1"}}
# First call — runs until interrupt()
result = graph.invoke({"target": "/tmp/important.log"}, config)
# result contains the interrupt payload: {'action': 'delete_file', 'path': '/tmp/important.log'}
# Human reviews and decides — resume with True (approve) or False (cancel)
final = graph.invoke(Command(resume=True), config)
The value you pass to Command(resume=...) becomes the return value of interrupt() inside the node. So approved = interrupt(...) will be True after the first example above.
Compile-time breakpoints (for debugging)
If you want the graph to pause before a node runs (without changing the node code), use interrupt_before at compile time:
graph = builder.compile(
interrupt_before=["risky"],
checkpointer=InMemorySaver()
)
# Resume by passing None
graph.invoke(None, config)
The one gotcha
When a node resumes after interrupt(), LangGraph re-runs the entire node from the top. Any code before the interrupt() call executes again — so keep pre-interrupt side effects idempotent (reads are fine; writes need guards).
Sources: LangGraph interrupt docs · LangChain blog: Making it easier to build human-in-the-loop agents with interrupt · YouTube walkthrough: LangGraph interrupt