Langgraph's defer=True logic (or way of doing things) doesn't solve much and is fundamentally broken #938

Closed
opened 2026-02-20 17:42:26 -05:00 by yindo · 8 comments
Owner

Originally created by @spandan-sharma on GitHub (Aug 25, 2025).

Originally assigned to: @casparb on GitHub.

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • 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

#!/usr/bin/env python3

# pylint: disable=missing-docstring

import asyncio
from pathlib import Path
from typing import Annotated, TypedDict

from langchain_core.messages import BaseMessage
from langgraph.graph import END, START, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph


class State(TypedDict):
    messages: Annotated[list[str | BaseMessage], add_messages]


async def node1(state: State) -> State:
    print("Node1:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>")
    print("-------------------")
    return State(messages=["from n1"])


async def node2(state: State) -> State:
    print("Node2:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>")
    print("-------------------")
    return State(messages=["from n2"])


async def node3(state: State) -> State:
    print("Node3:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n3"])


async def node4(state: State) -> State:
    print("Node4:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n4"])


async def node5(state: State) -> State:
    print("Node5:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n5"])


async def main():
    graph = StateGraph(State)
    graph.add_node(node1)
    graph.add_node(node2)
    graph.add_node(node3)
    graph.add_node(node4, defer=True)
    graph.add_node(node5, defer=True)

    graph.add_edge(START, "node1")
    graph.add_edge("node1", "node2")
    graph.add_edge("node1", "node4")
    graph.add_edge("node1", "node5")
    graph.add_edge("node2", "node3")
    graph.add_edge("node3", "node4")
    graph.add_edge("node4", "node5")
    ###############################################################
    graph.add_edge("node5", END)

    workflow: CompiledStateGraph = graph.compile(name="app2")
    io_dir = Path("./io.d")
    io_dir.mkdir(exist_ok=True)
    # workflow.get_graph().draw_mermaid_png(output_file_path=f"{io_dir}/graph_app3c.png")
    workflow.get_graph().draw_mermaid_png(output_file_path=io_dir / "graph_app3c.png")

    _result = await workflow.ainvoke(input=State())


if __name__ == "__main__":
    asyncio.run(main())

Error Message and Stack Trace (if applicable)

Node1: <No incoming state>
-------------------
Node2: from n1
-------------------
Node3: from n1
from n2
-------------------
Node4: from n1
from n2
from n3
-------------------
Node5: from n1
from n2
from n3
-------------------
Node5: from n1
from n2
from n3
from n4
from n5
-------------------

Description

        __start__
            |
          node1
            |
  ---------------------
 |          |          |
node2       |          |
 |          |          |
node3       |          |
 |          |          |
  ----------           |
      |                |
     node4             |
      |                |
       ----------------
               |
             node5
               |
            __end__

  • I had raised an issue last week, https://github.com/langchain-ai/langgraph/issues/5989, where the node execution was not following the constructed graph. The children in the graph were being executed before all their ancestors were. That to me itself is a bug because what's the use of creating a graph if nodes can just be executed without their dependencies being satisfied?
    • However, I found out about defer=True feature which was meant to address this and if set on a node then when a node is encountered for execution, it's instead (conceptually) shoved into a defer queue which will be de-queued only when the graph can no longer make progress - ie., all the ones that aren't deferred and the immediate children of such nodes are all executed already.
  • This doesn't solve much. We can create infinitely many graphs (trivial and complex) where two (or more) nodes that need to be deferred are such that node-x is an ancestor of the other, node-y, and there's a direct path to node-y from one of the ancestors of node-x itself such that the graph cannot make progress until its de-queued.
    • In any such combination, both node-x and node-y will be deferred but node-y will be de-queued and executed before node-x and then it'll be re-executed again at some point after node-x has been de-queued and executed.
    • So the first execution is wrong because it breaks the dependency ordering (the linked issue above) and node-y has executed without inputs from or side-effects of node-x have been realised and defer=True method solved nothing.

In the code above, this node-x is node-4 and node-y is node-5. defer=True is needed for node-4 because it needs to be paused until nodes 2 and 3 have been executed. defer=True is needed for node-5 but due to the graph structure, it solves nothing and as in the output, it's executed twice breaking the dependency contract.

  1. How to prevent the spurious execution of node-5 in such a case?
  2. This is nightmarish to work out in any big workflow graph and highly error prone. Why has such a solution been chosen instead of just doing the "sane" thing of constraining the order of execution to what the graph naturally says and which the user has crafted?
  3. Another drawback is that defer=True isn't smart enough to figure out lineages and take advantage of lineage based parallelism opportunity. It appears more like a sledgehammer-ish approach. Consider that node3 in the example a completely separate subtree under it which has nothing to do with nodes 4 and 5. That big subtree eventually just merges into __end__. Due to the way defer works, as long as the graph can find there are nodes it can execute, it will. So this whole subtree under node3 will be executed before dequeuing and executing node4, making it wait for no reason (almost like head-of-the-queue blocking scenario).

System Info

The following is from pip freeze:

langchain==0.3.27
langchain-community==0.3.27
langchain-core==0.3.74
langchain-openai==0.3.31
langchain-text-splitters==0.3.9
langgraph==0.6.6
langgraph-checkpoint==2.1.1
langgraph-checkpoint-sqlite==2.0.11
langgraph-prebuilt==0.6.4
langgraph-sdk==0.2.3
langsmith==0.4.15

Python:

$ python --version
Python 3.13.5

OS: MacOS

Originally created by @spandan-sharma on GitHub (Aug 25, 2025). Originally assigned to: @casparb on GitHub. ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/). - [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 #!/usr/bin/env python3 # pylint: disable=missing-docstring import asyncio from pathlib import Path from typing import Annotated, TypedDict from langchain_core.messages import BaseMessage from langgraph.graph import END, START, StateGraph, add_messages from langgraph.graph.state import CompiledStateGraph class State(TypedDict): messages: Annotated[list[str | BaseMessage], add_messages] async def node1(state: State) -> State: print("Node1:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>") print("-------------------") return State(messages=["from n1"]) async def node2(state: State) -> State: print("Node2:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>") print("-------------------") return State(messages=["from n2"]) async def node3(state: State) -> State: print("Node3:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n3"]) async def node4(state: State) -> State: print("Node4:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n4"]) async def node5(state: State) -> State: print("Node5:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n5"]) async def main(): graph = StateGraph(State) graph.add_node(node1) graph.add_node(node2) graph.add_node(node3) graph.add_node(node4, defer=True) graph.add_node(node5, defer=True) graph.add_edge(START, "node1") graph.add_edge("node1", "node2") graph.add_edge("node1", "node4") graph.add_edge("node1", "node5") graph.add_edge("node2", "node3") graph.add_edge("node3", "node4") graph.add_edge("node4", "node5") ############################################################### graph.add_edge("node5", END) workflow: CompiledStateGraph = graph.compile(name="app2") io_dir = Path("./io.d") io_dir.mkdir(exist_ok=True) # workflow.get_graph().draw_mermaid_png(output_file_path=f"{io_dir}/graph_app3c.png") workflow.get_graph().draw_mermaid_png(output_file_path=io_dir / "graph_app3c.png") _result = await workflow.ainvoke(input=State()) if __name__ == "__main__": asyncio.run(main()) ``` ### Error Message and Stack Trace (if applicable) ```shell Node1: <No incoming state> ------------------- Node2: from n1 ------------------- Node3: from n1 from n2 ------------------- Node4: from n1 from n2 from n3 ------------------- Node5: from n1 from n2 from n3 ------------------- Node5: from n1 from n2 from n3 from n4 from n5 ------------------- ``` ### Description ``` __start__ | node1 | --------------------- | | | node2 | | | | | node3 | | | | | ---------- | | | node4 | | | ---------------- | node5 | __end__ ``` - I had raised an issue last week, https://github.com/langchain-ai/langgraph/issues/5989, where the node execution was not following the constructed graph. The children in the graph were being executed before all their ancestors were. That to me itself is a bug because what's the use of creating a graph if nodes can just be executed without their dependencies being satisfied? - However, I found out about `defer=True` feature which was meant to address this and if set on a node then when a node is encountered for execution, it's instead (conceptually) shoved into a `defer` queue which will be de-queued only when the graph can no longer make progress - ie., all the ones that aren't deferred and the immediate children of such nodes are all executed already. - This doesn't solve much. We can create infinitely many graphs (trivial and complex) where two (or more) nodes that need to be deferred are such that `node-x` is an ancestor of the other, `node-y`, _and_ there's a direct path to `node-y` from one of the ancestors of `node-x` itself such that the graph cannot make progress until its de-queued. - In any such combination, both `node-x` and `node-y` will be deferred but `node-y` will be de-queued and executed before `node-x` and then it'll be re-executed again at some point after `node-x` has been de-queued and executed. - So the first execution is wrong because it breaks the dependency ordering (the linked issue above) and `node-y` has executed without inputs from or side-effects of `node-x` have been realised and `defer=True` method solved nothing. In the code above, this `node-x` is `node-4` and `node-y` is `node-5`. `defer=True` is needed for `node-4` because it needs to be paused until nodes `2` and `3` have been executed. `defer=True` is needed for `node-5` but due to the graph structure, it solves nothing and as in the output, it's executed twice breaking the dependency contract. 1. How to prevent the spurious execution of `node-5` in such a case? 2. This is nightmarish to work out in any big workflow graph and highly error prone. Why has such a solution been chosen instead of just doing the "sane" thing of constraining the order of execution to what the graph naturally says and which the user has crafted? 3. Another drawback is that `defer=True` isn't smart enough to figure out lineages and take advantage of lineage based parallelism opportunity. It appears more like a sledgehammer-ish approach. Consider that `node3` in the example a completely separate subtree under it which has nothing to do with nodes `4` and `5`. That big subtree eventually just merges into `__end__`. Due to the way defer works, as long as the graph can find there are nodes it can execute, it will. So this whole subtree under `node3` will be executed _before_ dequeuing and executing `node4`, making it wait for no reason (almost like head-of-the-queue blocking scenario). ### System Info The following is from `pip freeze`: ``` langchain==0.3.27 langchain-community==0.3.27 langchain-core==0.3.74 langchain-openai==0.3.31 langchain-text-splitters==0.3.9 langgraph==0.6.6 langgraph-checkpoint==2.1.1 langgraph-checkpoint-sqlite==2.0.11 langgraph-prebuilt==0.6.4 langgraph-sdk==0.2.3 langsmith==0.4.15 ``` Python: ``` $ python --version Python 3.13.5 ``` OS: `MacOS`
yindo added the bugpending labels 2026-02-20 17:42:26 -05:00
yindo closed this issue 2026-02-20 17:42:26 -05:00
Author
Owner

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

Hi @spandan-sharma, thanks for the clear repro.

This is working as intended. LangGraph is a reactive state machine, not a DAG scheduler. Edges are possible next hops, not dependency constraints. Nodes may run multiple times as state evolves. defer=True only delays a node until no more non-deferred work can run, it does not mean "run after all ancestors."

In your graph, node5 is reachable from both node1 and node4, so it can run before and after node4. If you only wanted node 1 to have an edge to node 5 after node 4 has executed, you could do something like:

def node4(state: State) -> State:
    return State(messages=["from n4"], n4_ready=True)

def gate_to_5(state: State) -> Literal["
    return ["node5"] if state.get("n4_ready") else []

graph.add_conditional_edges("node1", gate_to_5)

See the docs for more on conditional branching.

Closing as working as designed. For future questions on design patterns, please use the forum.

@casparb commented on GitHub (Sep 10, 2025): Hi @spandan-sharma, thanks for the clear repro. This is working as intended. LangGraph is a reactive state machine, not a DAG scheduler. Edges are possible next hops, not dependency constraints. Nodes may run multiple times as state evolves. `defer=True` only delays a node until no more non-deferred work can run, it does not mean "run after all ancestors." In your graph, `node5` is reachable from both `node1` and `node4`, so it can run before and after `node4`. If you only wanted node 1 to have an edge to node 5 after node 4 has executed, you could do something like: ```python def node4(state: State) -> State: return State(messages=["from n4"], n4_ready=True) def gate_to_5(state: State) -> Literal[" return ["node5"] if state.get("n4_ready") else [] graph.add_conditional_edges("node1", gate_to_5) ``` [See the docs for more on conditional branching.](https://docs.langchain.com/oss/python/langgraph/use-graph-api#conditional-branching) Closing as working as designed. For future questions on design patterns, please use [the forum](https://forum.langchain.com/c/help/langgraph/13).
Author
Owner

@ForestLH commented on GitHub (Sep 15, 2025):

Hi,there.
I had the same problem, I use Send seems to work, but should not be a good way.

@casparb @spandan-sharma

@ForestLH commented on GitHub (Sep 15, 2025): Hi,there. I had the same problem, I use [Send](https://docs.langchain.com/oss/python/langgraph/use-graph-api#map-reduce-and-the-send-api) seems to work, but should not be a good way. @casparb @spandan-sharma
Author
Owner

@QuinRiva commented on GitHub (Oct 24, 2025):

Hi @casparb - I understand that it this may not be a bug, but description of how it works seems to contradict the link you provided on conditional branch - or at the very least, that documentation can be logically read in such a way that a user would interpret that then node does (or should) wait.

Context7 documentation of Langgraph unfortunately also aligns with this interpretation too

"When multiple parallel branches converge to a single node, that node waits for ALL predecessors to complete before executing"

I think the key issue here, is that there actual process is is unclear, and there is conflicting documentation.

@QuinRiva commented on GitHub (Oct 24, 2025): Hi @casparb - I understand that it this _may_ not be a bug, but description of how it works seems to contradict the link you provided on conditional branch - or at the very least, that documentation _can_ be logically read in such a way that a user would interpret that then node **does** (or **should**) wait. Context7 documentation of Langgraph unfortunately also aligns with this interpretation too > "When multiple parallel branches converge to a single node, that node waits for ALL predecessors to complete before executing" I think the key issue here, is that there actual process is is unclear, and there is conflicting documentation.
Author
Owner

@casparb commented on GitHub (Oct 25, 2025):

Hi @QuinRiva, LangChain is not responsible for the docs produced by Context7.

Could I ask which part of the official LangGraph conditional branch docs I linked above are misleading? From the docs:

We set defer=True on node d so it will not execute until all pending tasks are finished.

"pending tasks" refer to all non-deferred nodes that are scheduled to run in upcoming super-steps. Essentially, any work that can be done, excluding deferred nodes.

@ForestLH - if you can achieve what you want with Send, then great! Nothing wrong with that as a design pattern if you want to pass custom state to a node. In terms of best practices, the more popular method for custom routing would probably be the conditional edge approach I outlined above, or returning a Command(goto="dest_node"), which also allows you to update state at the same time. Docs on Command

@casparb commented on GitHub (Oct 25, 2025): Hi @QuinRiva, LangChain is not responsible for the docs produced by Context7. Could I ask which part of the official LangGraph conditional branch docs I linked above are misleading? From the docs: > We set defer=True on node d so it will not execute until all pending tasks are finished. "pending tasks" refer to all non-deferred nodes that are scheduled to run in upcoming super-steps. Essentially, any work that can be done, excluding deferred nodes. @ForestLH - if you can achieve what you want with Send, then great! Nothing wrong with that as a design pattern if you want to pass custom state to a node. In terms of best practices, the more popular method for custom routing would probably be the conditional edge approach I outlined above, or returning a `Command(goto="dest_node")`, which also allows you to update state at the same time. [Docs on Command](https://docs.langchain.com/oss/python/langgraph/graph-api#command)
Author
Owner

@QuinRiva commented on GitHub (Oct 25, 2025):

Cheers @casparb , I think it was that line in particular:

Deferring node execution is useful when you want to delay the execution of a node until all other pending tasks are completed. This is particularly relevant when branches have different lengths, which is common in workflows like map-reduce flows

Because there's not a clear definition on the page of "pending tasks" it's easy to misinterpret that. Also, until your comment, I had thought that deferred nodes execute in the next super step, not between super steps.

Also, I interpreted different lengths as any different length (rather than just N+1).

I think a really good approach would be too provide an example where one of the parallel branches has three nodes, and the other has one node. The current example (2 and 1) has the unfortunate effect of reinforcing a DAG style misinterpretation, because (and by coincidence), in that example, either interpretation would actual produce the same result.

@QuinRiva commented on GitHub (Oct 25, 2025): Cheers @casparb , I think it was that line in particular: > Deferring node execution is useful when you want to delay the execution of a node until all other pending tasks are completed. This is particularly relevant when branches have different lengths, which is common in workflows like map-reduce flows Because there's not a clear definition on the page of "pending tasks" it's easy to misinterpret that. Also, until your comment, I had thought that deferred nodes execute in the **next** super step, not between super steps. Also, I interpreted different lengths as any different length (rather than just N+1). I think a really good approach would be too provide an example where one of the parallel branches has three nodes, and the other has one node. The current example (2 and 1) has the unfortunate effect of reinforcing a DAG style misinterpretation, because (and by coincidence), in that example, either interpretation would actual produce the same result.
Author
Owner

@casparb commented on GitHub (Oct 28, 2025):

Hi @QuinRiva, I was incorrect in my response above. I've edited the comment, and let me try to explain again here:

Deferred node execution does not necessarily occur at the end of the super-step that it is encountered in by the scheduler. Deferred nodes may be delayed many super-steps. A deferred node will only execute when:

  • All non-deferred work has completed
  • No no additional non-deferred nodes can be triggered

To summarize: deferred nodes execute when we reach a super-step where there's no other work to do. In the case of parallel branches converging at a deferred node, one length 3 and one length 1, the deferred node will only execute after all nodes in the parallel branches have executed.

Now while this might seem like DAG-style behavior, if we look at @spandan-sharma's example above:

        __start__
            |
          node1
            |
  ---------------------
 |          |          |
node2       |          |
 |          |          |
node3       |          |
 |          |          |
  ----------           |
      |                |
     node4             |
      |                |
       ----------------
               |
             node5
               |
            __end__

(node4 and node5 are deferred)

node4 and node5 will execute in the same super-step after node1, node2, and node3 have executed, because at that point there's no more non-deferred work to be done. But we also have an edge from node4 to node5, so in the next super-step, we re-execute node5. Not DAG-like here!

You're spot on thought that the deferred docs could use some work, I'll flag it as a TODO. I see questions like this popping up quite often. Thank you for your feedback and I apologise for the confusing initial reply.

@casparb commented on GitHub (Oct 28, 2025): Hi @QuinRiva, I was incorrect in my response above. I've edited the comment, and let me try to explain again here: Deferred node execution does not necessarily occur at the end of the super-step that it is encountered in by the scheduler. Deferred nodes may be delayed many super-steps. A deferred node will only execute when: - All non-deferred work has completed - No no additional non-deferred nodes can be triggered To summarize: deferred nodes execute when we reach a super-step where there's no other work to do. In the case of parallel branches converging at a deferred node, one length 3 and one length 1, the deferred node will only execute after all nodes in the parallel branches have executed. Now while this might seem like DAG-style behavior, if we look at @spandan-sharma's example above: ``` __start__ | node1 | --------------------- | | | node2 | | | | | node3 | | | | | ---------- | | | node4 | | | ---------------- | node5 | __end__ ``` (node4 and node5 are deferred) node4 and node5 will execute in the same super-step after node1, node2, and node3 have executed, because at that point there's no more non-deferred work to be done. But we also have an edge from node4 to node5, so in the next super-step, we re-execute node5. Not DAG-like here! You're spot on thought that the deferred docs could use some work, I'll flag it as a TODO. I see questions like this popping up quite often. Thank you for your feedback and I apologise for the confusing initial reply.
Author
Owner

@HamaWhiteGG commented on GitHub (Jan 5, 2026):

@casparb @spandan-sharma

I've found that when there are multiple defer in a graph, the above problem occurs. For example, when there is a dependency relationship between node4 and node5 such as node4->node5, it may still not take effect. Not only do these two nodes execute simultaneously, but after node4 completes execution, it can also cause node5 to execute repeatedly.

For a given node, if none of its multiple input edges originate directly or indirectly from conditional branch nodes, the waiting_edges mechanism can be used to resolve the issue. That is, when adding edges, the start_key can be added as an array. In this case, the edge is added to waiting_edges, and the end_key will only execute after all start_keys have completed.
You can check the add_edge(self, start_key: str | list[str], end_key: str)method.

def add_edge(self, start_key: str | list[str], end_key: str) -> Self:
    """Add a directed edge from the start node (or list of start nodes) to the end node.

    When a single start node is provided, the graph will wait for that node to complete
    before executing the end node. When multiple start nodes are provided,
    the graph will wait for ALL of the start nodes to complete before executing the end node.

    Args:
        start_key: The key(s) of the start node(s) of the edge.
        end_key: The key of the end node of the edge.

    Raises:
        ValueError: If the start key is 'END' or if the start key or end key is not present in the graph.

    Returns:
        Self: The instance of the state graph, allowing for method chaining.
    """
    
    if isinstance(start_key, str):
        self.edges.add((start_key, end_key))
        return self
    
    ...
    self.waiting_edges.add((tuple(start_key), end_key))
    return self

Regarding the test code above, we have changed it to the following:

graph.add_edge(["node1","node3"], "node4")
graph.add_edge(["node1","node4"], "node5")

Note: The following writing style belongs to ordinary edges and will be added to edges.

graph.add_edge("node1", "node4")
graph.add_edge("node3", "node4")

graph.add_edge("node1", "node5")
graph.add_edge("node4", "node5")

Here is my modified code.

#!/usr/bin/env python3

# pylint: disable=missing-docstring

import asyncio
from pathlib import Path
from typing import Annotated, TypedDict

from langchain_core.messages import BaseMessage
from langgraph.graph import END, START, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph


class State(TypedDict):
    messages: Annotated[list[str | BaseMessage], add_messages]


async def node1(state: State) -> State:
    print("Node1:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>")
    print("-------------------")
    return State(messages=["from n1"])


async def node2(state: State) -> State:
    print("Node2:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>")
    print("-------------------")
    return State(messages=["from n2"])


async def node3(state: State) -> State:
    print("Node3:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n3"])


async def node4(state: State) -> State:
    print("Node4:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n4"])


async def node5(state: State) -> State:
    print("Node5:", "\n".join(m.content for m in state.get("messages")))
    print("-------------------")
    return State(messages=["from n5"])


async def main():
    graph = StateGraph(State)
    graph.add_node(node1)
    graph.add_node(node2)
    graph.add_node(node3)
    graph.add_node(node4)
    graph.add_node(node5)

    graph.add_edge(START, "node1")
    graph.add_edge("node1", "node2")
    graph.add_edge("node2", "node3")
    # graph.add_edge("node1", "node4")
    # graph.add_edge("node3", "node4")
    graph.add_edge(["node1","node3"], "node4")

    # graph.add_edge("node1", "node5")
    # graph.add_edge("node4", "node5")
    graph.add_edge(["node1","node4"], "node5")

    ###############################################################
    graph.add_edge("node5", END)

    workflow: CompiledStateGraph = graph.compile(name="app2")
    io_dir = Path("./io.d")
    io_dir.mkdir(exist_ok=True)
    # workflow.get_graph().draw_mermaid_png(output_file_path=f"{io_dir}/graph_app3c.png")
    workflow.get_graph().draw_mermaid_png(output_file_path=io_dir / "graph_app3c.png")

    _result = await workflow.ainvoke(input=State())


if __name__ == "__main__":
    asyncio.run(main())

The running results are as follows, with each node executed only once.

Node1: <No incoming state>
-------------------
Node2: from n1
-------------------
Node3: from n1
from n2
-------------------
Node4: from n1
from n2
from n3
-------------------
Node5: from n1
from n2
from n3
from n4
-------------------
@HamaWhiteGG commented on GitHub (Jan 5, 2026): @casparb @spandan-sharma I've found that when there are multiple defer in a graph, the above problem occurs. For example, when there is a dependency relationship between node4 and node5 such as node4->node5, it may still not take effect. Not only do these two nodes execute simultaneously, but after node4 completes execution, it can also cause node5 to execute repeatedly. For a given node, if none of its multiple input edges originate directly or indirectly from conditional branch nodes, the `waiting_edges` mechanism can be used to resolve the issue. That is, when adding edges, the `start_key` can be added as an array. In this case, the edge is added to `waiting_edges`, and the `end_key` will only execute after all `start_keys` have completed. You can check the `add_edge(self, start_key: str | list[str], end_key: str)`method. ```python def add_edge(self, start_key: str | list[str], end_key: str) -> Self: """Add a directed edge from the start node (or list of start nodes) to the end node. When a single start node is provided, the graph will wait for that node to complete before executing the end node. When multiple start nodes are provided, the graph will wait for ALL of the start nodes to complete before executing the end node. Args: start_key: The key(s) of the start node(s) of the edge. end_key: The key of the end node of the edge. Raises: ValueError: If the start key is 'END' or if the start key or end key is not present in the graph. Returns: Self: The instance of the state graph, allowing for method chaining. """ if isinstance(start_key, str): self.edges.add((start_key, end_key)) return self ... self.waiting_edges.add((tuple(start_key), end_key)) return self ``` Regarding the test code above, we have changed it to the following: ```python graph.add_edge(["node1","node3"], "node4") graph.add_edge(["node1","node4"], "node5") ``` Note: The following writing style belongs to ordinary edges and will be added to edges. ```python graph.add_edge("node1", "node4") graph.add_edge("node3", "node4") graph.add_edge("node1", "node5") graph.add_edge("node4", "node5") ``` Here is my modified code. ```python #!/usr/bin/env python3 # pylint: disable=missing-docstring import asyncio from pathlib import Path from typing import Annotated, TypedDict from langchain_core.messages import BaseMessage from langgraph.graph import END, START, StateGraph, add_messages from langgraph.graph.state import CompiledStateGraph class State(TypedDict): messages: Annotated[list[str | BaseMessage], add_messages] async def node1(state: State) -> State: print("Node1:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>") print("-------------------") return State(messages=["from n1"]) async def node2(state: State) -> State: print("Node2:", "\n".join(m.content for m in state.get("messages")) or "<No incoming state>") print("-------------------") return State(messages=["from n2"]) async def node3(state: State) -> State: print("Node3:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n3"]) async def node4(state: State) -> State: print("Node4:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n4"]) async def node5(state: State) -> State: print("Node5:", "\n".join(m.content for m in state.get("messages"))) print("-------------------") return State(messages=["from n5"]) async def main(): graph = StateGraph(State) graph.add_node(node1) graph.add_node(node2) graph.add_node(node3) graph.add_node(node4) graph.add_node(node5) graph.add_edge(START, "node1") graph.add_edge("node1", "node2") graph.add_edge("node2", "node3") # graph.add_edge("node1", "node4") # graph.add_edge("node3", "node4") graph.add_edge(["node1","node3"], "node4") # graph.add_edge("node1", "node5") # graph.add_edge("node4", "node5") graph.add_edge(["node1","node4"], "node5") ############################################################### graph.add_edge("node5", END) workflow: CompiledStateGraph = graph.compile(name="app2") io_dir = Path("./io.d") io_dir.mkdir(exist_ok=True) # workflow.get_graph().draw_mermaid_png(output_file_path=f"{io_dir}/graph_app3c.png") workflow.get_graph().draw_mermaid_png(output_file_path=io_dir / "graph_app3c.png") _result = await workflow.ainvoke(input=State()) if __name__ == "__main__": asyncio.run(main()) ``` The running results are as follows, with each node executed only once. ``` Node1: <No incoming state> ------------------- Node2: from n1 ------------------- Node3: from n1 from n2 ------------------- Node4: from n1 from n2 from n3 ------------------- Node5: from n1 from n2 from n3 from n4 ------------------- ```
Author
Owner

@spandan-sharma commented on GitHub (Jan 8, 2026):

@HamaWhiteGG ooo fantastic! Ta!

@spandan-sharma commented on GitHub (Jan 8, 2026): @HamaWhiteGG ooo fantastic! Ta!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#938