npx skills add ...
npx skills add langchain-ai/skills-benchmarks --skill langgraph-human-in-the-loop
npx skills add langchain-ai/skills-benchmarks --skill langgraph-human-in-the-loop
INVOKE THIS SKILL when implementing human-in-the-loop patterns, pausing for approval, or handling errors in LangGraph. Covers interrupt(), Command(resume=...), approval/validation workflows, and the 4-tier error handling strategy.
interrupt(value) — pauses execution, surfaces a value to the callerCommand(resume=value) — resumes execution, providing the value back to interrupt()Three things are required for interrupts to work:
checkpointer=InMemorySaver() (dev) or PostgresSaver (prod){"configurable": {"thread_id": "..."}} to every invoke/stream callinterrupt() must be JSON-serializableinterrupt(value) pauses the graph. The value surfaces in the result under __interrupt__. Command(resume=value) resumes — the resume value becomes the return value of interrupt().
Critical: when the graph resumes, the node restarts from the beginning — all code before interrupt() re-runs.
class State(TypedDict): approved: bool
def approval_node(state: State): # Pause and ask for approval approved = interrupt("Do you approve this action?") # When resumed, Command(resume=...) returns that value here return {"approved": approved}
checkpointer = InMemorySaver() graph = ( StateGraph(State) .add_node("approval", approval_node) .add_edge(START, "approval") .add_edge("approval", END) .compile(checkpointer=checkpointer) )
config = {"configurable": {"thread_id": "thread-1"}}
result = graph.invoke({"approved": False}, config) print(result["interrupt"])
result = graph.invoke(Command(resume=True), config) print(result["approved"]) # True
A common pattern: interrupt to show a draft, then route based on the human's decision.
Interrupt for human review, then route to send or end based on the decision. ```python from langgraph.types import interrupt, Command from langgraph.graph import StateGraph, START, END from typing import Literal from typing_extensions import TypedDictclass EmailAgentState(TypedDict): email_content: str draft_response: str classification: dict
def human_review(state: EmailAgentState) -> Command[Literal["send_reply", "end"]]: """Pause for human review using interrupt and route based on decision.""" classification = state.get("classification", {})
Use interrupt() in a loop to validate human input and re-prompt if invalid.
def get_age_node(state): prompt = "What is your age?"
const getAgeNode = (state: typeof State.State) => { let prompt = "What is your age?";
while (true) { const answer = interrupt(prompt);
} };
const State = Annotation.Root({ vals: Annotation<string[]>({ reducer: (left, right) => left.concat(Array.isArray(right) ? right : [right]), default: () => [], }), });
function nodeA(_state: typeof State.State) {
const answer = interrupt("question_a") as string;
return { vals: [a:${answer}] };
}
function nodeB(_state: typeof State.State) {
const answer = interrupt("question_b") as string;
return { vals: [b:${answer}] };
}
const graph = new StateGraph(State) .addNode("a", nodeA) .addNode("b", nodeB) .addEdge(START, "a") .addEdge(START, "b") .addEdge("a", END) .addEdge("b", END) .compile({ checkpointer: new MemorySaver() });
const config = { configurable: { thread_id: "1" } };
const interruptedResult = await graph.invoke({ vals: [] }, config);
// Resume all pending interrupts at once
const resumeMap: Record<string, string> = {};
if (isInterrupted(interruptedResult)) {
for (const i of interruptedResult[INTERRUPT]) {
if (i.id != null) {
resumeMap[i.id] = answer for ${i.value};
}
}
}
const result = await graph.invoke(new Command({ resume: resumeMap }), config);
// result.vals = ["a:answer for question_a", "b:answer for question_b"]
// GOOD: Side effect AFTER interrupt — only runs once const nodeA = async (state: typeof State.State) => { const approved = interrupt("Approve this change?"); if (approved) { await db.createAuditLog({ userId: state.userId, action: "approved" }); } return { approved }; };
// BAD: Insert creates duplicates on each resume! const nodeA = async (state: typeof State.State) => { await db.createAuditLog({ // Runs again on resume! userId: state.userId, action: "pending_approval", }); const approved = interrupt("Approve this change?"); return { approved }; };
async function nodeInSubgraph(state: State) { someOtherCode(); // <-- Also re-executes on resume const result = interrupt("What's your name?"); // ... }
// CORRECT const graph = builder.compile({ checkpointer: new MemorySaver() });
// CORRECT await graph.invoke(new Command({ resume: "approve" }), config);