Interrupts and human-in-the-loop

LangChain · Expert · 14 min

Agents that send emails, spend money or change data must let people approve, edit or reject actions.

LangGraph can pause execution and wait indefinitely for input. Inside a node or tool, call interrupt(payload):

decision = interrupt({"question": "Approve refund?", "amount": 120})

Execution stops, the state is saved by the checkpointer (required), and invoke returns with an __interrupt__ entry containing the payload. Later, possibly from another process or days later, resume the same thread with:

graph.invoke(Command(resume={"approved": True}), config)

The node re-runs from its start and this time interrupt returns the resume value. Therefore code before interrupt must be idempotent (no side effects such as charging a card before the pause), and multiple interrupts in a node are matched by order.

For agents, HumanInTheLoopMiddleware(interrupt_on={"send_email": True, "read_inbox": False}) pauses before selected tool calls. The reviewer can approve, edit the arguments, or reject with feedback, resuming with Command(resume={"decisions": [{"type": "approve"}]}).

Related tools:

  • interrupt_before=["node"] / interrupt_after at compile time for static breakpoints while debugging.
  • graph.get_state(config).next shows which node is pending; update_state lets a human patch state before resuming.

Design the payload for the UI that will show it: what is proposed, why, and what choices exist.

Example

from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt

class State(TypedDict):
    amount: int
    status: str
def approval(state: State):
    decision = interrupt({"question": "Approve refund?", "amount": state["amount"]})
    return {"status": "refunded" if decision["approved"] else "rejected"}
builder = StateGraph(State)
builder.add_node("approval", approval)
builder.add_edge(START, "approval")
builder.add_edge("approval", END)
graph = builder.compile(checkpointer=InMemorySaver())

cfg = {"configurable": {"thread_id": "refund-1"}}
first = graph.invoke({"amount": 120, "status": "pending"}, cfg)
print(first["__interrupt__"][0].value)     # {'question': 'Approve refund?', 'amount': 120}
print(graph.get_state(cfg).next)           # ('approval',)

final = graph.invoke(Command(resume={"approved": True}), cfg)
print(final["status"])                     # refunded

Remember

  • interrupt(payload) pauses and saves state; a checkpointer is required
  • Resume with Command(resume=value) on the same thread_id; the node re-runs from its start
  • HumanInTheLoopMiddleware adds approve/edit/reject for chosen agent tools

Watch out for

  • Performing side effects before interrupt() in the same node, which then run twice when the node re-executes on resume.

Check yourself

  1. What happens to a node's code that runs before interrupt() when the graph is resumed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.langchain.com/oss/python/langgraph/interrupts

Builds on

Unlocks

Nothing builds on this yet.