Once a graph asks more than one question, resume gets subtle: which Command(resume=...) answers which interrupt(), and why does the node run four times for three questions? Here is the behavior, run on langgraph 1.2.15 / langgraph-checkpoint 4.3.0 (Python), Oct 15, 2026.
The two rules
- Several interrupts in one node are matched by position. The docs: LangGraph "keeps a list of resume values specific to the task executing the node... Matching is strictly index-based, so the order of interrupt calls within the node is important" (LangGraph interrupts).
- Interrupts pending at the same time (parallel nodes) are matched by id. "When resuming multiple interrupts with a single invocation, map each interrupt ID to its resume value."
Tested example
import operator
from typing import Annotated, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import START, END, StateGraph
from langgraph.types import Command, interrupt
# Part 1: three interrupts in one node, answered one resume at a time
RUNS = {"n": 0}
class Form(TypedDict, total=False):
name: str; age: int; city: str
def ask(state: Form):
RUNS["n"] += 1
name = interrupt("name?"); age = interrupt("age?"); city = interrupt("city?")
return {"name": name, "age": age, "city": city}
b = StateGraph(Form); b.add_node("ask", ask); b.add_edge(START, "ask"); b.add_edge("ask", END)
g = b.compile(checkpointer=InMemorySaver()); cfg = {"configurable": {"thread_id": "form-1"}}
out = g.invoke({}, cfg); print("pending:", out["__interrupt__"][0].value)
for v in ["Ada", 36, "London"]:
out = g.invoke(Command(resume=v), cfg)
print("resumed", repr(v), "->", out["__interrupt__"][0].value if "__interrupt__" in out else out)
print("node executions:", RUNS["n"])
# Part 2: two parallel nodes interrupt in the same step
class S(TypedDict):
vals: Annotated[list[str], operator.add]
def a(s): return {"vals": [f"a:{interrupt('approve refund?')}"]}
def bb(s): return {"vals": [f"b:{interrupt('approve email?')}"]}
b = StateGraph(S); b.add_node("a", a); b.add_node("b", bb)
b.add_edge(START, "a"); b.add_edge(START, "b"); b.add_edge("a", END); b.add_edge("b", END)
g = b.compile(checkpointer=InMemorySaver()); cfg = {"configurable": {"thread_id": "par-1"}}
out = g.invoke({"vals": []}, cfg)
pending = out["__interrupt__"]; print("pending:", [i.value for i in pending])
out = g.invoke(Command(resume={i.id: ("yes" if "refund" in i.value else "no") for i in pending}), cfg)
print("final:", out)
Real output:
pending: name?
resumed 'Ada' -> age?
resumed 36 -> city?
resumed 'London' -> {'name': 'Ada', 'age': 36, 'city': 'London'}
node executions: 4
pending: ['approve email?', 'approve refund?']
final: {'vals': ['a:yes', 'b:no']}
Part 1 takes 4 node executions for 3 interrupts: the node re-runs from the top on every resume and earlier answers are replayed by position. Part 2 answers both parallel interrupts with one Command(resume={interrupt.id: value}).
Mistakes that break it
- Conditional or non-deterministic interrupts. Skipping an
interrupt()behind anif, or looping over data that changes between runs, shifts the index and hands answers to the wrong question. while Truevalidation loops. The docs warn each resume "replay[s] all previous iterations." Callinterrupt()once per node and loop back with a conditional edge instead.- Side effects between interrupts. They re-run on every resume. Keep them in a later node or make them idempotent. See why a LangGraph interrupt resumes twice.
- Too many questions? One
interrupt()that carries a list keyed by your own ids is often simpler than several calls.
Tested starters
- Free: Multiple interrupts in one node, sequential and parallel resume with real run output.
- UI: Minimal web UI to approve paused runs.
- Done for you: the $79 Agent Approval-Flow Fix. We repair approval and resume in your LangGraph, OpenAI Agents SDK, Google ADK or Vercel AI SDK agent and deliver a tested patch within 48h, or a full refund. More tested code in Signal Pro.