What is the situation of supersteps in a graph containing interrupted subgraph after resumption?

In the graph above, node_1 is an interrupt node. According to the actual running results of the code, its superstep is as follows: node_0----[node_1,node_2]----(Resume)----node_3----node4, rather than node_0----[node_1,node_2]----(Resume)----[node_4,node3]----node4. What is the reason for this? Are there any special rules after resumption?

The code is as follows:

from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict
from typing import Annotated
#from langgraph.constants import add
import operator

class State(TypedDict):
    foo : Annotated[str,operator.add]

# Subgraph

def subgraph_node_1(state: State):
    value = interrupt("Provide value:")
    #value = 'no interrupt'
    return {"foo":  value}

subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_edge(START, "subgraph_node_1")

subgraph = subgraph_builder.compile()

# Parent graph

def node_0(state: State):
    return {"foo":  "pass node_0    "}

def node_2(state: State):
    return {"foo":  "pass node_2    "}

def node_3(state: State):
    return {"foo": "pass node_3    "}

def node_4(state: State):
    return {"foo":  "pass node_4    "}

# def node_5(state: State):
#     return {"foo":  "pass node_5    "}

# def node_6(state: State):
#     return {"foo":  "pass node_6    "}
    

builder = StateGraph(State)
builder.add_node("node_0", node_0)
builder.add_node("node_1", subgraph)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_node("node_4", node_4)
# builder.add_node("node_5", node_5)
# builder.add_node("node_6", node_6)
builder.add_edge(START, "node_0")
builder.add_edge("node_0", "node_1")
builder.add_edge("node_0", "node_2")
builder.add_edge("node_2", "node_3")
# builder.add_edge("node_3", "node_4")
# builder.add_edge("node_1", "node_5")
# builder.add_edge(["node_4","node_5"], "node_6")

builder.add_edge(["node_1","node_3"], "node_4")

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "1"}}

graph.invoke({"foo": ""}, config)
parent_state = graph.get_state(config)
parent_state_history = list(graph.get_state_history(config))
parent_state_with_subgraphs = graph.get_state(config, subgraphs=True)
#subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state  
print(parent_state)
print("-"*50)
print(parent_state_history)
print("-"*50)
print(parent_state_with_subgraphs)
print("-"*50)
# resume the subgraph
graph.invoke(Command(resume="bar"), config)

The output content of the code after ‘RESUME’ is as follows:

StateSnapshot(values={'foo': 'pass node_0    pass node_0    barpass node_2    pass node_3    pass node_4    '}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01c0-6f40-8004-50f9f71af12d'}}, metadata={'source': 'loop', 'step': 4, 'parents': {}}, created_at='2025-09-19T03:45:46.437408+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01be-684c-8003-702569046e8f'}}, tasks=(), interrupts=())
--------------------------------------------------
[StateSnapshot(values={'foo': 'pass node_0    pass node_0    barpass node_2    pass node_3    pass node_4    '}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01c0-6f40-8004-50f9f71af12d'}}, metadata={'source': 'loop', 'step': 4, 'parents': {}}, created_at='2025-09-19T03:45:46.437408+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01be-684c-8003-702569046e8f'}}, tasks=(), interrupts=()), StateSnapshot(values={'foo': 'pass node_0    pass node_0    barpass node_2    pass node_3    '}, next=('node_4',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01be-684c-8003-702569046e8f'}}, metadata={'source': 'loop', 'step': 3, 'parents': {}}, created_at='2025-09-19T03:45:46.436410+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01bc-6145-8002-7777a76870fa'}}, tasks=(PregelTask(id='667cd88f-bc51-4845-8911-c2278286b0e3', name='node_4', path=('__pregel_pull', 'node_4'), error=None, interrupts=(), state=None, result={'foo': 'pass node_4    '}),), interrupts=()), StateSnapshot(values={'foo': 'pass node_0    pass node_0    barpass node_2    '}, next=('node_3',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01bc-6145-8002-7777a76870fa'}}, metadata={'source': 'loop', 'step': 2, 'parents': {}}, created_at='2025-09-19T03:45:46.435411+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0198-6426-8001-5b0e00195266'}}, tasks=(PregelTask(id='d21d2c70-220f-f528-6479-c83c90dac7fe', name='node_3', path=('__pregel_pull', 'node_3'), error=None, interrupts=(), state=None, result={'foo': 'pass node_3    '}),), interrupts=()), StateSnapshot(values={'foo': 'pass node_0    '}, next=('node_1', 'node_2'), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0198-6426-8001-5b0e00195266'}}, metadata={'source': 'loop', 'step': 1, 'parents': {}}, created_at='2025-09-19T03:45:46.420739+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0195-6d30-8000-78b97d2ba6a4'}}, tasks=(PregelTask(id='76c7ada8-189b-7413-9368-3eab70bd6b43', name='node_1', path=('__pregel_pull', 'node_1'), error=None, interrupts=(Interrupt(value='Provide value:', id='c9b139dfdee2119f6d824ae8a5f71ca2'),), state={'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_1:76c7ada8-189b-7413-9368-3eab70bd6b43'}}, result={'foo': 'pass node_0    bar'}), PregelTask(id='eeffd82a-bb21-d91b-a7fd-d2ce4acc718a', name='node_2', path=('__pregel_pull', 'node_2'), error=None, interrupts=(), state=None, result={'foo': 'pass node_2    '})), interrupts=(Interrupt(value='Provide value:', id='c9b139dfdee2119f6d824ae8a5f71ca2'),)), StateSnapshot(values={'foo': ''}, next=('node_0',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0195-6d30-8000-78b97d2ba6a4'}}, metadata={'source': 'loop', 'step': 0, 'parents': {}}, created_at='2025-09-19T03:45:46.419742+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0190-6f54-bfff-234bcec06cc4'}}, tasks=(PregelTask(id='b234dbf7-0e6c-a35c-6a57-b7ff1926aa4f', name='node_0', path=('__pregel_pull', 'node_0'), error=None, interrupts=(), state=None, result={'foo': 'pass node_0    '}),), interrupts=()), StateSnapshot(values={'foo': ''}, next=('__start__',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-0190-6f54-bfff-234bcec06cc4'}}, metadata={'source': 'input', 'step': -1, 'parents': {}}, created_at='2025-09-19T03:45:46.417749+00:00', parent_config=None, tasks=(PregelTask(id='cd55534c-8bda-16b0-4032-a43a7158ca4e', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'foo': ''}),), interrupts=())]
--------------------------------------------------
StateSnapshot(values={'foo': 'pass node_0    pass node_0    barpass node_2    pass node_3    pass node_4    '}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01c0-6f40-8004-50f9f71af12d'}}, metadata={'source': 'loop', 'step': 4, 'parents': {}}, created_at='2025-09-19T03:45:46.437408+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f0950b2-01be-684c-8003-702569046e8f'}}, tasks=(), interrupts=())
--------------------------------------------------

Hey @Chad - this is by design. LangGraph uses a superstep architecture:

  • In each superstep, all nodes whose dependencies are satisfied run in parallel.

  • Once every active node in that step finishes, the system synchronizes, then moves to the next superstep.

Helpful doc: https://docs.langchain.com/oss/python/langgraph/use-graph-api#run-graph-nodes-in-parallel

In your specific example, node 1 & 2 belongs to the same super step. node 3 is its own superstep, and node 4 is a separate superstep.

before resumption there is a superstep with 2 nodes, but after resumption there is a superstep with only 1 node. why?If there’s no interrupted node ,it should be a superstep with 2 nodes which is node_3 and node_4.

Thanks for your help. Just as your words, node 1 & 2 belongs to the same super step, and node 4 follows node1, node 3 follows node 2, so next super step should be node 4& 3. Why ONLY node 3 is in next super step?

I have tested. If node_1 is a normal node rather than an interrupt node, its superstep will be node_0----[node_1,node_2]----[node_4,node3]----node4. So what’s the difference between the two situations? What does the resuming node do?

In addition, if node_1 is a regular node and another node is added before node_0, it will be (node_3,). If node_4 uses the defer attribute, it will also be (node_3,). This is confusing. When exactly is it (node_3,) and when is it (node_3,node_4)? What factors determine this?