TechByteByByte

Send: Dynamic Parallelism for an Unknown Number of Branches

Static parallel branches only work when you already know how many you need. See exactly where that assumption breaks, and how Send fans out to a genuinely unknown number of branches, decided at runtime.

#LangGraph#Send#Parallelism#Map-Reduce

Recall Module 5’s fan-in example — several nodes converging on one shared next step. This module covers the mirror image: fanning out from one node to several parallel branches. And it covers a genuine, real limitation with the obvious first approach, one that shows up the moment a real workflow’s needs stop being predictable in advance.

The obvious approach, and exactly where it breaks

Suppose a research workflow needs to investigate three fixed topics in parallel.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    findings: list[str]

def research_topic_a(state: State) -> dict:
    return {"findings": state["findings"] + ["Finding on Topic A"]}

def research_topic_b(state: State) -> dict:
    return {"findings": state["findings"] + ["Finding on Topic B"]}

def research_topic_c(state: State) -> dict:
    return {"findings": state["findings"] + ["Finding on Topic C"]}

builder = StateGraph(State)
builder.add_node("research_topic_a", research_topic_a)
builder.add_node("research_topic_b", research_topic_b)
builder.add_node("research_topic_c", research_topic_c)

builder.add_edge(START, "research_topic_a")
builder.add_edge(START, "research_topic_b")
builder.add_edge(START, "research_topic_c")
builder.add_edge("research_topic_a", END)
builder.add_edge("research_topic_b", END)
builder.add_edge("research_topic_c", END)

graph = builder.compile()
print(graph.invoke({"findings": []}))

Three edges from START, three genuinely parallel branches. This works — but notice the real, honest assumption baked directly into the code: exactly three topics, known in advance, at the time you wrote this graph. Now ask a genuinely realistic question: what happens the moment a user asks you to research five topics instead of three? Or one? This graph, as written, simply cannot do that — the number of branches is fixed, hardcoded, at definition time.

Example 1: the real fix — Send

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send

class State(TypedDict):
    topics: list[str]
    findings: list[str]

class ResearchState(TypedDict):
    topic: str
    findings: list[str]

def research_one_topic(state: ResearchState) -> dict:
    return {"findings": [f"Finding on {state['topic']}"]}

def fan_out(state: State) -> list[Send]:
    return [Send("research_one_topic", {"topic": t, "findings": []}) for t in state["topics"]]

builder = StateGraph(State)
builder.add_node("research_one_topic", research_one_topic)
builder.add_conditional_edges(START, fan_out, ["research_one_topic"])
builder.add_edge("research_one_topic", END)

graph = builder.compile()
print(graph.invoke({"topics": ["LLMs", "RAG", "Agents", "MCP", "GraphRAG"], "findings": []}))

Notice fan_out doesn’t return a single node name — it returns a genuine, real list of Send objects, one for each topic in state["topics"]. However many topics actually exist at runtime — three, five, one, fifty — this exact same code handles all of them, launching exactly that many parallel executions of research_one_topic. This is the real, structural difference from the hardcoded version above: the number of branches is decided by real, live data, not fixed when you wrote the graph.

What each Send actually carries

Send("research_one_topic", {"topic": "RAG", "findings": []})

Every Send genuinely carries two things: which node should run, and the exact, specific input that particular branch should receive. This matters — each parallel branch gets its own, independent slice of data, not the entire shared state. research_one_topic, running for the “RAG” branch, only ever sees {"topic": "RAG", ...}, completely unaware that four other branches are running concurrently on different topics.

Example 2: combining Send with a real planner node

A genuinely realistic version doesn’t hardcode the topic list either — a planner node decides it dynamically, based on the actual request.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from langchain.chat_models import init_chat_model

class State(TypedDict):
    user_query: str
    topics: list[str]
    findings: list[str]

class ResearchState(TypedDict):
    topic: str
    findings: list[str]

model = init_chat_model("openai:gpt-4o-mini")

def plan(state: State) -> dict:
    # in a real app, this would ask the model to genuinely produce a topic list
    return {"topics": ["current state of RAG", "agent orchestration"]}

def research_one_topic(state: ResearchState) -> dict:
    return {"findings": [f"Finding on {state['topic']}"]}

def fan_out(state: State) -> list[Send]:
    return [Send("research_one_topic", {"topic": t, "findings": []}) for t in state["topics"]]

builder = StateGraph(State)
builder.add_node("plan", plan)
builder.add_node("research_one_topic", research_one_topic)
builder.add_edge(START, "plan")
builder.add_conditional_edges("plan", fan_out, ["research_one_topic"])
builder.add_edge("research_one_topic", END)

graph = builder.compile()
print(graph.invoke({"user_query": "Research recent RAG and agent developments", "topics": [], "findings": []}))

The topic count is now genuinely determined in two real, honest stages — the model (inside plan) decides how many topics matter, and fan_out launches exactly that many parallel branches, with no hardcoded number anywhere in the graph’s own structure.

Why this connects directly to the very next module

Run the two-topic example above and look closely at findings in the final result. Both parallel branches wrote to the exact same findings field — and you haven’t yet learned what actually happens when that occurs, or how to make sure both branches’ results genuinely survive rather than one silently overwriting the other. That’s precisely the real, honest gap the next module exists to close.

flowchart LR
    plan --> fanout{fan_out}
    fanout -->|Send| r1[research_one_topic\ntopic: RAG]
    fanout -->|Send| r2[research_one_topic\ntopic: Agents]
    r1 --> agg["? — how do both\nresults survive?"]
    r2 --> agg

Common mistakes worth avoiding

Assuming Send shares the full, overall state with every branch. Recall this module’s own emphasis — each Send carries its own, specific payload, not the entire graph’s state. A branch expecting a field you never actually included in that Send’s payload will genuinely fail to find it.

Using Send when a fixed, known number of parallel branches was actually sufficient. Recall this module’s opening example — if you genuinely, always need exactly three fixed research branches, three plain edges from START are simpler and more directly readable than Send’s added indirection. Reach for Send specifically when the branch count is genuinely unknown until runtime.

Forgetting that parallel branches genuinely run concurrently, with no guaranteed order. Don’t write logic that assumes Send branches complete in the order they were created — real, concurrent execution offers no such guarantee, and design accordingly.

What you should take away from this module

  • Static, hardcoded parallel edges only work when the exact number of branches is genuinely known when you write the graph.
  • Send(node_name, payload) fans out to a real, dynamic number of parallel branches, decided by actual data at runtime — the same code handles three topics or fifty.
  • Each Send carries its own specific payload, not the graph’s full, shared state — branches are genuinely isolated from each other.
  • Multiple parallel branches writing to the same state field raises a real question this course hasn’t answered yet — that’s exactly what reducers, covered next, exist to solve.

Where this goes next

The next module covers Reducers — the real mechanism that decides what happens when parallel branches, like the ones you just built, write to the same state field at the same time.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed