In LangGraph, what does interrupt() do inside a node and how do you resume?
answer
- Pause is persistence, not blocking
- Payload surfaces to the caller
- Same thread_id must resume
- Command carries the human's answer
- Node restarts, not line-after
basics
~20 sinterrupt() pauses the graph in the middle of a node, persists a checkpoint, and surfaces a payload to the caller under the interrupt key. You resume by invoking the same thread_id with Command(resume=value); the interrupt() call then returns that value.
solid answer
~40 s`interrupt()` (from `langgraph.types`) is LangGraph's dynamic human-in-the-loop primitive. Called inside a node, it raises a special `GraphInterrupt` that the runtime catches: the state written so far is checkpointed, and the payload you passed to `interrupt()` comes back to the caller as an `Interrupt` object under the `__interrupt__` key of the invoke/stream result. Execution of that thread stops — nothing is blocked, no thread is held; the process can exit. To resume, you call the graph again with the **same** `thread_id` in the config and pass `Command(resume=<human's answer>)`. LangGraph loads the checkpoint, re-executes the interrupted node from its first line, and this time the `interrupt()` call returns the resume value instead of pausing. A checkpointer is mandatory — without one there is no saved state to come back to.
code
python · 27 linesfrom typing_extensions import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
class State(TypedDict):
draft: str
approved: bool
def review(state: State) -> dict:
decision = interrupt({"draft": state["draft"], "question": "send it?"})
return {"approved": decision == "yes"}
builder = StateGraph(State)
builder.add_node("review", review)
builder.add_edge(START, "review")
builder.add_edge("review", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "t-1"}}
paused = graph.invoke({"draft": "hello", "approved": False}, config)
print(paused["__interrupt__"][0].value)
print(graph.invoke(Command(resume="yes"), config))go deeper
Know that LangGraph can stop mid-run to ask a person something, and that the pause is saved rather than held open. Be able to name interrupt() as the call that pauses and Command(resume=...) as the way the answer gets back in.
Be ready to write the whole loop: compile with a checkpointer, call interrupt() with a payload, read the interrupt key from the result, and resume on the same thread_id. Explain that the node replays from the top rather than continuing mid-function.
Show that you treat the pause as durable state: thread ids are your handles into an approval queue, the payload is a UI contract, and a resume may arrive from a different process days later. Be ready to discuss what the payload should and should not expose.
Own the surface around it: which actions are worth interrupting for, how approval latency interacts with the rest of the system, and how a durable pause changes capacity planning versus an architecture that holds a request open while waiting for a human.
## The problem it solves An agent that can spend money, send email, delete rows or file tickets needs a person in the loop before the irreversible step. A plain function call cannot do that: the process would have to sit and block for however long the human takes, which might be minutes or days. LangGraph's answer is to make the pause a *persistence* event rather than a *blocking* event. ## What interrupt() actually does `interrupt` is imported from `langgraph.types`. You call it inside a node with any JSON-serializable payload: When the runtime executes that node and hits the call, `interrupt` raises an internal `GraphInterrupt` exception. The Pregel loop catches it and does three things. First, it commits a checkpoint for the current thread containing the state as of the end of the previous superstep, plus a record of the pending task and its interrupt. Second, it stops the run — no further nodes are scheduled. Third, it returns to the caller. The result of `graph.invoke(...)` is the current state dict with an extra `__interrupt__` key holding a tuple of `Interrupt` objects; each carries the `value` you passed. If you are streaming, the same information arrives as an `__interrupt__` update. Crucially, the pause costs nothing while it lasts. No OS thread, no coroutine, no open connection. The run exists only as rows in whatever checkpointer you configured. A web server can return a 200 with the interrupt payload, the human can answer three days later, and a completely different process can resume the run. ## Resuming Resumption is another invocation of the same graph, with a config whose `thread_id` matches the paused run, and `Command(resume=...)` as the input: `graph.invoke(Command(resume="yes"), config)` `Command` also comes from `langgraph.types`. LangGraph restores the checkpoint, finds the task that was interrupted, and re-runs that node. On this pass, the `interrupt()` call does not raise: it returns `"yes"`, and the node continues normally to its `return`. Everything downstream then proceeds as if the pause had never happened. Two details follow from that mechanic and both are frequently probed. The node restarts **from its first statement**, not from the line after `interrupt()` — LangGraph does not snapshot the Python stack, it replays the function. And if a single node calls `interrupt()` more than once, the resume values are matched positionally within that node's task: the first call consumes the first stored resume value, the second consumes the second, and only the call with no stored value pauses again. ## The checkpointer requirement `interrupt()` is meaningless without persistence, so the graph must be compiled with a checkpointer: `builder.compile(checkpointer=InMemorySaver())` for a demo, a durable saver for anything real. Calling `interrupt()` on a graph compiled without one is an error, and calling `invoke` without a `thread_id` in `configurable` is an error too, because there is no key under which to store or find the paused run. ## Payload design The payload is the entire contract with your UI, so put in it everything the reviewer needs and nothing secret they should not see: the proposed action, its arguments, a short rationale, and enough identifying context to render a decision screen. It must be serializable by the checkpointer. Keep it stable — it is effectively an API between the graph and the approval front end. ## Where this sits in a real system A typical shape is: HTTP request starts or resumes a thread; the graph runs until it interrupts; the service persists nothing extra (the checkpointer already did) and returns the interrupt payload plus the `thread_id` to the client; the client renders an approval screen; the human's decision comes back on a second endpoint that calls `invoke(Command(resume=decision), config)`. Because the thread id is the only handle, treat it as a first-class identifier — log it, index it, and make it addressable from the approval queue. ## What candidates get wrong The two classic mistakes are resuming by passing the human's answer as ordinary graph input (which starts a *new* superstep with new input instead of delivering a resume value) and assuming execution continues after the `interrupt()` line. A third is imagining `interrupt()` is a blocking read — it is not; it is an exception plus a checkpoint, and that is exactly why the pause can outlive the process.
- What happens if you compile the graph without a checkpointer and a node calls interrupt()?It fails. `interrupt()` works by checkpointing the paused task and returning control to the caller; with no checkpointer there is nothing to write and nothing to resume from, so LangGraph raises rather than silently discarding the pause. The same applies if you invoke without a `thread_id` in `configurable` — there is no key under which the paused run could be found again.
- How does LangGraph match resume values when one node calls interrupt() twice?Positionally, within that node's task. On resume the node replays from its first line; the first `interrupt()` call returns the first stored resume value, the second returns the second, and the first call that has no stored value pauses the graph again. That also means reordering or adding `interrupt()` calls in the node's code while a thread is paused will mismatch the stored answers.
- How do you find out what a paused thread is waiting on without resuming it?Call `graph.get_state(config)` with the thread's config. The returned `StateSnapshot` exposes `values` (the checkpointed state), `next` (the node names that are about to run), and `tasks`, whose entries carry the pending interrupts and their payloads. That is how an approval queue renders pending items without touching execution.
saying these in an interview costs you the question
- Thinks interrupt() works without a checkpointer configured
- Believes execution resumes on the line after interrupt()
- Passes the human's answer as ordinary graph input instead of Command(resume=...)
- Assumes the pause blocks a server thread or coroutine
- Resumes with a fresh thread_id and expects the old state