agent_signal
← All templates
LangGraph · 1.2.15 · Python ✓ Tested

Tested on LangGraph (langgraph 1.2.15, pinned in requirements.txt)

Multiple interrupts in one node

One node calls interrupt() three times and is answered one resume at a time (matched by position), plus two parallel nodes that interrupt in the same step and are resumed together with a dict keyed by Interrupt.id.

What it shows

  • Part 1: sequential interrupts: Each invoke(Command(resume=v)) answers the next pending question. The node re-executes from the top each time (4 executions for 3 interrupts); earlier answers are replayed by position.
  • Part 2: parallel interrupts: Two parallel nodes interrupt in the same step. out["__interrupt__"] lists both with ids, and one Command(resume={interrupt.id: value, ...}) answers both, matched by id.
Mocked vs real. No LLM and nothing is mocked. All LangGraph code is real (langgraph==1.2.15, langgraph-checkpoint==4.3.0, InMemorySaver).

Code

multi_interrupt.pysrc
"""Multiple interrupts in ONE node, plus parallel interrupts resumed by interrupt id. No LLM; all LangGraph real."""
import operator
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command

class S(TypedDict, total=False):
    answers: Annotated[list, operator.add]

calls = {"n": 0}
def review(s):
    calls["n"] += 1
    print(f"  [review] execution #{calls['n']}")
    a = interrupt({"step": 1, "q": "Approve deleting 1200 rows?"})
    b = interrupt({"step": 2, "q": "Also notify the customer?"})
    c = interrupt({"step": 3, "q": "Reason for audit log?"})
    return {"answers": [a, b, c]}

def part1():
    print("== Part 1: three sequential interrupt() calls in one node ==")
    b = StateGraph(S); b.add_node("review", review); b.add_edge(START, "review"); b.add_edge("review", END)
    g = b.compile(checkpointer=InMemorySaver()); cfg = {"configurable": {"thread_id": "seq"}}
    out = g.invoke({"answers": []}, cfg)
    for resume in [True, False, "cleanup ticket #881"]:
        print("  paused on:", out["__interrupt__"][0].value, "-> resume", repr(resume))
        out = g.invoke(Command(resume=resume), cfg)
    print("  final:", out)

def part2():
    print("== Part 2: two parallel nodes each interrupt; resume both at once keyed by interrupt id ==")
    def mk(name):
        def node(s):
            return {"answers": [(name, interrupt({"node": name, "q": f"approve {name}?"}))]}
        return node
    b = StateGraph(S)
    for n in ("tool_a", "tool_b"):
        b.add_node(n, mk(n)); b.add_edge(START, n); b.add_edge(n, END)
    g = b.compile(checkpointer=InMemorySaver()); cfg = {"configurable": {"thread_id": "par"}}
    out = g.invoke({"answers": []}, cfg)
    intrs = out["__interrupt__"]
    print("  pending:", [(i.id[:8], i.value["node"]) for i in intrs])
    decisions = {i.id: (i.value["node"] == "tool_a") for i in intrs}  # approve a, reject b
    out = g.invoke(Command(resume=decisions), cfg)
    print("  final:", sorted(out["answers"]))

part1(); calls["n"] = 0; part2()
requirements.txtsrc
langgraph==1.2.15
langgraph-checkpoint==4.3.0
run_output.txtsrc
== Part 1: three sequential interrupt() calls in one node ==
  [review] execution #1
  paused on: {'step': 1, 'q': 'Approve deleting 1200 rows?'} -> resume True
  [review] execution #2
  paused on: {'step': 2, 'q': 'Also notify the customer?'} -> resume False
  [review] execution #3
  paused on: {'step': 3, 'q': 'Reason for audit log?'} -> resume 'cleanup ticket #881'
  [review] execution #4
  final: {'answers': [True, False, 'cleanup ticket #881']}
== Part 2: two parallel nodes each interrupt; resume both at once keyed by interrupt id ==
  pending: [('0965f4b8', 'tool_a'), ('1c911b00', 'tool_b')]
  final: [('tool_a', True), ('tool_b', False)]
LICENSEsrc
MIT License

Copyright (c) 2026 Agent Signal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Upgrade gotchas

  • Within one node, resume values match interrupt() calls by order. Keep the sequence deterministic: no interrupts inside conditionals that can change between executions, no loops over unordered or random data.
  • Code between interrupts runs again on every resume, so keep side effects out of it or make them idempotent (see the idempotent approval template).
  • When several interrupts are pending at once, resume with a dict keyed by Interrupt.id; a bare value would be ambiguous.
  • To ask several things in one go, a single interrupt() carrying a list keyed by your own ids is often simpler.

Free starter pack

The approval-flow starter pack

Tested human-in-the-loop approval starters for LangGraph, OpenAI Agents SDK, Vercel AI SDK and Google ADK, plus the weekly Agent Signal newsletter. Free.

FAQ

How are multiple resume values matched in one node?

By order of the interrupt() calls within the node, which is why the call sequence must be deterministic.

How do I resume two parallel interrupts at once?

Pass Command(resume={interrupt_id: value, ...}) using the ids listed in out["__interrupt__"].

Which versions was it tested on?

langgraph 1.2.15 and langgraph-checkpoint 4.3.0 on Python 3.13, Oct 15, 2026. MIT-licensed.