Graph broken when used with defer-ed nodes + Command + conditional edges #741

Closed
opened 2026-02-20 17:41:32 -05:00 by yindo · 1 comment
Owner

Originally created by @sundaraa-deshaw on GitHub (Jun 24, 2025).

Originally assigned to: @casparb on GitHub.

Checked other resources

  • This is a bug, not a usage question. For questions, please use GitHub Discussions.
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

import operator
from typing import Annotated, Any, Literal

from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.types import Command

class State(TypedDict):
    # The operator.add reducer fn makes this append-only
    aggregate: Annotated[list, operator.add]


def a(state: State):
    print(f'Adding "A" to {state["aggregate"]}')
    return {"aggregate": ["A"]}


def b(state: State):
    print(f'Adding "B" to {state["aggregate"]}')
    return {"aggregate": ["B"]}


def b_2(state: State):
    print(f'Adding "B_2" to {state["aggregate"]}')
    return {"aggregate": ["B_2"]}


def c(state: State):
    print(f'Adding "C" to {state["aggregate"]}')
    return {"aggregate": ["C"]}


def d(state: State):
    print(f'Adding "D" to {state["aggregate"]}')
    return {"aggregate": ["D"]}

def e(state: State) -> Command[Literal["d", "e2"]]:
    return Command(
        # state update
        update={"foo": "bar"},
        # control flow
        goto="e2"
    )

def conditional_edge(state) -> Literal["d", "e3"]:
    pass

def e2(state: State):
    return {}

def e3(state: State):
    return {}

builder = StateGraph(State)
builder.add_node(a)
builder.add_node(b)
builder.add_node(b_2)
builder.add_node(c)
builder.add_node(
    d,
    defer=True
)
builder.add_node(e)
builder.add_node(e2)
builder.add_node(e3)
builder.add_edge(START, "a")
builder.add_edge("a", "b")
builder.add_edge("a", "c")
builder.add_edge("b", "b_2")
builder.add_edge("b_2", "d")
builder.add_edge("c", "d")
builder.add_edge("d", "e")
builder.add_conditional_edges("e2", conditional_edge)
builder.add_edge("e3", END)
graph = builder.compile()

display(Image(graph.get_graph().draw_png()))

Error Message and Stack Trace (if applicable)


Description

The given graph used parallel branches + deferred execution + Command + conditional edge.

The graph looks incorrect with defer=True.
Image

Expected: the graph should look the same with or without defer.
Atleast the END not should not be displaced.

without defer=True:

Image

System Info

System Information

OS: Linux
OS Version: #1 SMP Fri Apr 25 00:55:37 EDT 2025
Python Version: 3.11.8 (main, Sep 17 2024, 14:28:20) [GCC 10.3.1 20210422 (Red Hat 10.3.1-1)]

Package Information

langchain_core: 0.3.55
langchain: 0.3.14
langchain_community: 0.3.14
langsmith: 0.2.10
langchain_anthropic: 0.2.4
langchain_cli: 0.0.31
langchain_experimental: 0.3.4
langchain_google_vertexai: 2.0.21
langchain_openai: 0.2.14
langchain_text_splitters: 0.3.5
langchainplus_sdk: 0.0.21
langgraph_gen: 0.0.6
langgraph_reflection: 0.0.1
langgraph_sdk: 0.1.60
langgraph_supervisor: 0.0.20
langgraph_swarm: 0.0.11

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.8.4
anthropic: 0.30.0
anthropic[vertexai]: Installed. No version info available.
async-timeout: 4.0.3
bottleneck: 1.3.7
dataclasses-json: 0.6.3
defusedxml: 0.7.1
gitpython: 3.1.40
google-cloud-aiplatform: 1.89.0
google-cloud-storage: 2.13.0
gritql: Installed. No version info available.
httpx: 0.27.0
httpx-sse: 0.4.0
jinja2>=3.1.5: Installed. No version info available.
jsonpatch<2.0,>=1.33: Installed. No version info available.
langchain-core<0.4.0,>=0.3.40: Installed. No version info available.
langchain-mistralai: Installed. No version info available.
langchain>=0.1.0: Installed. No version info available.
langgraph: 0.4.1
langgraph-prebuilt<0.2.0,>=0.1.7: Installed. No version info available.
langgraph>=0.2.74: Installed. No version info available.
langgraph>=0.3.5: Installed. No version info available.
langserve[all]: Installed. No version info available.
langsmith-pyo3: Installed. No version info available.
langsmith<0.4,>=0.1.125: Installed. No version info available.
mypy>=1.8.0: Installed. No version info available.
numexpr: 2.8.7
numpy: 1.26.4+deshaw5
openai: 1.59.7
orjson: 3.10.16+deshaw2
packaging<25,>=23.2: Installed. No version info available.
pyarrow: 18.0.0
pydantic: 2.9.2
pydantic-settings: 2.6.0
pydantic<3.0.0,>=2.5.2;: Installed. No version info available.
pydantic<3.0.0,>=2.7.4;: Installed. No version info available.
PyYAML: 6.0.2
PyYAML>=5.3: Installed. No version info available.
pyyaml>=6.0.2: Installed. No version info available.
requests: 2.31.0
requests-toolbelt: 1.0.0
SQLAlchemy: 1.4.50
tenacity: 8.2.3
tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available.
tiktoken: 0.7.0
tomlkit: 0.12.3
typer[all]: Installed. No version info available.
typing-extensions>=4.7: Installed. No version info available.
uvicorn: 0.26.0
validators: 0.22.0
zstandard: 0.22.0

Originally created by @sundaraa-deshaw on GitHub (Jun 24, 2025). Originally assigned to: @casparb on GitHub. ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use GitHub Discussions. - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python import operator from typing import Annotated, Any, Literal from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.types import Command class State(TypedDict): # The operator.add reducer fn makes this append-only aggregate: Annotated[list, operator.add] def a(state: State): print(f'Adding "A" to {state["aggregate"]}') return {"aggregate": ["A"]} def b(state: State): print(f'Adding "B" to {state["aggregate"]}') return {"aggregate": ["B"]} def b_2(state: State): print(f'Adding "B_2" to {state["aggregate"]}') return {"aggregate": ["B_2"]} def c(state: State): print(f'Adding "C" to {state["aggregate"]}') return {"aggregate": ["C"]} def d(state: State): print(f'Adding "D" to {state["aggregate"]}') return {"aggregate": ["D"]} def e(state: State) -> Command[Literal["d", "e2"]]: return Command( # state update update={"foo": "bar"}, # control flow goto="e2" ) def conditional_edge(state) -> Literal["d", "e3"]: pass def e2(state: State): return {} def e3(state: State): return {} builder = StateGraph(State) builder.add_node(a) builder.add_node(b) builder.add_node(b_2) builder.add_node(c) builder.add_node( d, defer=True ) builder.add_node(e) builder.add_node(e2) builder.add_node(e3) builder.add_edge(START, "a") builder.add_edge("a", "b") builder.add_edge("a", "c") builder.add_edge("b", "b_2") builder.add_edge("b_2", "d") builder.add_edge("c", "d") builder.add_edge("d", "e") builder.add_conditional_edges("e2", conditional_edge) builder.add_edge("e3", END) graph = builder.compile() display(Image(graph.get_graph().draw_png())) ``` ### Error Message and Stack Trace (if applicable) ```shell ``` ### Description The given graph used parallel branches + deferred execution + Command + conditional edge. The graph looks incorrect with `defer=True`. ![Image](https://github.com/user-attachments/assets/6b660150-0680-4d3a-ae5b-87dd2f09ab21) Expected: the graph should look the same with or without defer. Atleast the __END__ not should not be displaced. without `defer=True`: ![Image](https://github.com/user-attachments/assets/c965d424-77b3-4bc7-a3ab-4e8572a41b92) ### System Info System Information ------------------ > OS: Linux > OS Version: #1 SMP Fri Apr 25 00:55:37 EDT 2025 > Python Version: 3.11.8 (main, Sep 17 2024, 14:28:20) [GCC 10.3.1 20210422 (Red Hat 10.3.1-1)] Package Information ------------------- > langchain_core: 0.3.55 > langchain: 0.3.14 > langchain_community: 0.3.14 > langsmith: 0.2.10 > langchain_anthropic: 0.2.4 > langchain_cli: 0.0.31 > langchain_experimental: 0.3.4 > langchain_google_vertexai: 2.0.21 > langchain_openai: 0.2.14 > langchain_text_splitters: 0.3.5 > langchainplus_sdk: 0.0.21 > langgraph_gen: 0.0.6 > langgraph_reflection: 0.0.1 > langgraph_sdk: 0.1.60 > langgraph_supervisor: 0.0.20 > langgraph_swarm: 0.0.11 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp: 3.8.4 > anthropic: 0.30.0 > anthropic[vertexai]: Installed. No version info available. > async-timeout: 4.0.3 > bottleneck: 1.3.7 > dataclasses-json: 0.6.3 > defusedxml: 0.7.1 > gitpython: 3.1.40 > google-cloud-aiplatform: 1.89.0 > google-cloud-storage: 2.13.0 > gritql: Installed. No version info available. > httpx: 0.27.0 > httpx-sse: 0.4.0 > jinja2>=3.1.5: Installed. No version info available. > jsonpatch<2.0,>=1.33: Installed. No version info available. > langchain-core<0.4.0,>=0.3.40: Installed. No version info available. > langchain-mistralai: Installed. No version info available. > langchain>=0.1.0: Installed. No version info available. > langgraph: 0.4.1 > langgraph-prebuilt<0.2.0,>=0.1.7: Installed. No version info available. > langgraph>=0.2.74: Installed. No version info available. > langgraph>=0.3.5: Installed. No version info available. > langserve[all]: Installed. No version info available. > langsmith-pyo3: Installed. No version info available. > langsmith<0.4,>=0.1.125: Installed. No version info available. > mypy>=1.8.0: Installed. No version info available. > numexpr: 2.8.7 > numpy: 1.26.4+deshaw5 > openai: 1.59.7 > orjson: 3.10.16+deshaw2 > packaging<25,>=23.2: Installed. No version info available. > pyarrow: 18.0.0 > pydantic: 2.9.2 > pydantic-settings: 2.6.0 > pydantic<3.0.0,>=2.5.2;: Installed. No version info available. > pydantic<3.0.0,>=2.7.4;: Installed. No version info available. > PyYAML: 6.0.2 > PyYAML>=5.3: Installed. No version info available. > pyyaml>=6.0.2: Installed. No version info available. > requests: 2.31.0 > requests-toolbelt: 1.0.0 > SQLAlchemy: 1.4.50 > tenacity: 8.2.3 > tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available. > tiktoken: 0.7.0 > tomlkit: 0.12.3 > typer[all]: Installed. No version info available. > typing-extensions>=4.7: Installed. No version info available. > uvicorn: 0.26.0 > validators: 0.22.0 > zstandard: 0.22.0
yindo added the bugpending labels 2026-02-20 17:41:32 -05:00
yindo closed this issue 2026-02-20 17:41:32 -05:00
Author
Owner

@casparb commented on GitHub (Sep 23, 2025):

Hi @sundaraa-deshaw, thanks for the clear repro.

A node with defer=True is a tricky thing to render with a static diagram. In your case, when we defer node d we are saying that it cannot execute until after all other pending tasks within its current "superstep" (i.e. parallel branches) have completed. In other words, d can't execute until e, e2, and e3, have executed. So while there may not be an explicitly defined conditional edge from e3 -> d, deferring d means e3 execution must precede d execution, hence we draw an 'inferred' conditional edge drawn e3 -> d to show that this transition is possible. Same thing for e2 -> d.

Re. the displaced edge to END - we don't actually store statically defined END edges, we synthesize them dynamically. add_edge(e3, END) doesn’t make the graph end after node e3 necessarily (only if you didn’t add any other edges too, as the graph simply ends when there’s no more nodes to go to). As a matter of fact, add_edge(e3, END) does nothing at all! Currently we only draw an END edge from terminal nodes (no outgoing edges), or if no terminal nodes exist, we fall back and draw an edge from the single node that produced writes in the final superstep. Deferring d creates inferred incoming edges into d, which makes e3 no longer necessarily terminal, so the edge e3 -> END isn't drawn explicitly.

I hope this cleared things up. Closing this issue but happy to answer any follow up questions, just tag me.

@casparb commented on GitHub (Sep 23, 2025): Hi @sundaraa-deshaw, thanks for the clear repro. A node with `defer=True` is a tricky thing to render with a static diagram. In your case, when we defer node `d` we are saying that it cannot execute until after all other pending tasks within its current "superstep" (i.e. parallel branches) have completed. In other words, `d` can't execute until `e`, `e2`, and `e3`, have executed. So while there may not be an explicitly defined conditional edge from `e3 -> d`, deferring `d` means `e3` execution must precede `d` execution, hence we draw an 'inferred' conditional edge drawn `e3 -> d` to show that this transition is possible. Same thing for `e2 -> d`. Re. the displaced edge to `END` - we don't actually store statically defined END edges, we synthesize them dynamically. `add_edge(e3, END)` doesn’t make the graph end after node `e3` necessarily (only if you didn’t add any other edges too, as the graph simply ends when there’s no more nodes to go to). As a matter of fact, `add_edge(e3, END)` does nothing at all! Currently we only draw an END edge from terminal nodes (no outgoing edges), or if no terminal nodes exist, we fall back and draw an edge from the single node that produced writes in the final superstep. Deferring `d` creates inferred incoming edges into `d`, which makes `e3` no longer necessarily terminal, so the edge `e3 -> END` isn't drawn explicitly. I hope this cleared things up. Closing this issue but happy to answer any follow up questions, just tag me.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#741