# Feature request: let a add\_conditional\_edges branch write into an existing add\_edge(\[...\], dest) barrier

**URL:** <https://forum.langchain.com/t/feature-request-let-a-add-conditional-edges-branch-write-into-an-existing-add-edge-dest-barrier/4513>\
**Category:** LangGraph\
**Tags:** python-help, feature-request\
**Created:** [September 9, 2026, 9:11am UTC](https://forum.langchain.com/t/feature-request-let-a-add-conditional-edges-branch-write-into-an-existing-add-edge-dest-barrier/4513 "2026-09-09T09:11:36Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![AlejandroTorresMunoz](https://yyz1.discourse-cdn.com/flex007/user_avatar/forum.langchain.com/alejandrotorresmunoz/32/3551_2.png) [@AlejandroTorresMunoz](https://forum.langchain.com/u/AlejandroTorresMunoz)\
**Post date:** [September 9, 2026, 9:11am UTC](https://forum.langchain.com/t/feature-request-let-a-add-conditional-edges-branch-write-into-an-existing-add-edge-dest-barrier/4513/1 "2026-09-09T09:11:36Z")

</div>

### The gap

`add_edge([source1, source2, ...], dest)` creates a `NamedBarrierValue` channel: `dest` becomes a real AND-join, firing only once every named source has written to it since it last fired. This works great, as long as every source is a plain node reached only through `add_edge`.

If one of `dest`’s incoming connections instead comes from an `add_conditional_edges` branch, there is no way to make that branch’s write count as one of the barrier’s named sources. `attach_branch`’s `get_writes` always formats the destination as the generic per-node channel every node already gets by default (`branch:to:{dest}`, fires on any single write, no counting), and there’s no path in that code for “write into this existing `join:...` `NamedBarrierValue` channel by name instead.”

Consequence: if a conditional branch’s destination is _also_ a `NamedBarrierValue` join, `dest` ends up with **two independent trigger channels** : the generic one (unguarded, fires on the very first write) and the real barrier (properly synchronized, fires only when complete). `dest` runs whenever _either_ fires, so it fires early with incomplete data from the branch, and fires again later when the barrier itself completes.

This makes sense given the destination of a conditional edge isn’t known until the routing function runs, since the compiler can’t statically pre-register it as a fixed barrier source the way it can a plain edge. But once the routing function _has_ run and picked a destination, that write is exactly as “real” as a static edge’s, and there’s currently no way to route it into a barrier by name.

### Minimal repro

This needs a timing gap between the branch and the barrier’s other source(s) to actually show up: if everything resolves in the same super-step, Pregel’s trigger check happens to see both channels ready at once and only schedules the node once. So one track needs an extra hop to be one super-step “deeper” than the router’s branch: here `a -> a2` is that extra hop, and `b`/`router` are both direct successors of `split`:

```python
from langgraph.graph import END, START, StateGraph
from typing import Annotated, TypedDict
import operator

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

def make_node(name):
    def _node(state):
        return {"calls": [name]}
    return _node

builder = StateGraph(State)
for name in ("split", "router", "a", "a2", "b", "join"):
    builder.add_node(name, make_node(name))

builder.add_edge(START, "split")
builder.add_edge("split", "router")
builder.add_edge("split", "a")
builder.add_edge("split", "b")
builder.add_edge("a", "a2") # one extra hop, so this track resolves one super-step later

# The router's only branch goes straight to "join" -- no plain node in
# between, so it can never be named in join's barrier source list.
builder.add_conditional_edges("router", lambda state: "direct", {"direct": "join"})

# join's barrier only knows about "a2" and "b".
builder.add_edge(["a2", "b"], "join")
builder.add_edge("join", END)

graph = builder.compile()

calls = []
for step in graph.stream({"calls": []}, stream_mode="updates"):
    for node_name, update in step.items():
        calls.extend(update.get("calls", []))

print(calls)
print("join ran:", calls.count("join"), "times") # -> 2, should be 1

```

Output: `['split', 'a', 'b', 'router', 'a2', 'join', 'join']`. `join` runs twice: once as soon as `router` writes (through the generic channel, one super-step before `a2` even runs), and once again when the `["a2", "b"]` barrier legitimately completes.

### Current workaround

Insert a synthetic no-op pass-through node between the branch and the join, so the branch’s destination becomes a plain node whose own single outgoing edge (`add_edge(passthrough, join)`) can be named in the barrier’s source list:

```python
builder.add_node("router_passthrough", make_node("router_passthrough"))
builder.add_conditional_edges("router", lambda state: "direct", {"direct": "router_passthrough"})
builder.add_edge(["a2", "b", "router_passthrough"], "join")

```

With this, `join` runs exactly once: `['split', 'a', 'b', 'router', 'a2', 'router_passthrough', 'join']`.

This works, but costs an extra node and an extra super-step for every conditional branch that needs to converge at a barrier join, purely as plumbing to route around the gap.

### Request

Some way to let a `add_conditional_edges` `path_map` entry target a name already declared as a source of an existing barrier, so the branch’s write lands in that `NamedBarrierValue` channel instead of the generic per-node one, removing the need for the pass-through node. Even just exposing the join-channel name (e.g. via `add_edge`’s return value or a lookup) so a `path_map` could point directly at it would be enough; happy to sketch an API shape if useful.

Ran into this building a workflow compiler with real AND-splits where a conditional branch legitimately short-circuits straight to the join on some condition. It’s a fairly natural shape once branches and joins are both first-class.
