Edges and Flow: Branching and Looping

This chapter covers how control flows through a graph: plain edges, conditional edges, and cycles that let agents loop until done.

Plain Edges

The simplest. Always go from A to B.

builder.add_edge("parse", "validate")
builder.add_edge("validate", "save")

After parse runs, validate runs. After validate, save. No conditions.

Fan-Out: Multiple Edges Out

A node can have multiple outgoing edges. All target nodes run.

builder.add_edge("analyze", "send_email")
builder.add_edge("analyze", "log_metrics")

After analyze, both send_email and log_metrics run in parallel. They both read the same post-analyze state.

When those branches rejoin, LangGraph merges their state updates using the reducers.

Fan-In: Multiple Edges In

A node can have multiple incoming edges. It waits for all of them.

builder.add_edge("send_email", "finish")
builder.add_edge("log_metrics", "finish")

finish runs after both send_email and log_metrics complete.

Combined fan-out and fan-in is how you parallelize work.

Conditional Edges: Deciding at Runtime

The interesting case. You want to go to B sometimes and C other times, based on state.

from langgraph.graph import END

def should_retry(state: State) -> str:
    if state["attempts"] < 3 and not state["succeeded"]:
        return "retry"
    return "done"

builder.add_conditional_edges(
    "process",
    should_retry,
    {
        "retry": "process",    # loop back
        "done": END,
    },
)

The middle argument is a routing function. It takes state, returns a string, and LangGraph uses the mapping to pick the next node.

should_retry can return any key in the mapping. "retry" routes back to process (a cycle), "done" goes to END.

Shorthand: Direct Node Names

If the routing function returns a valid node name, you can skip the mapping:

def route(state: State) -> str:
    return "retry" if state["attempts"] < 3 else END

builder.add_conditional_edges("process", route)

LangGraph treats the return value as the next node directly.

Multiple Branches

def route(state: State) -> str:
    if state["tool_call"]:
        return "tools"
    if state["needs_clarification"]:
        return "ask_user"
    return "respond"

builder.add_conditional_edges("agent", route, {
    "tools": "tools",
    "ask_user": "ask_user",
    "respond": "respond",
})

Any number of branches. The router picks one key; LangGraph routes there.

Cycles: The Agent Loop

LangGraph allows cycles. In fact, the canonical agent loop is a cycle:

agent → (tool_call?) → tools → agent → (tool_call?) → tools → ... → respond
def needs_tool(state: State) -> str:
    last = state["messages"][-1]
    if last.tool_calls:
        return "tools"
    return END

builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)

builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", needs_tool, {
    "tools": "tools",
    END: END,
})
builder.add_edge("tools", "agent")   # loop back

The edge tools → agent is the loop. After tools run, we're back at agent, which looks at state and decides whether to call more tools or finish.

Recursion Limit

A buggy cycle could loop forever. LangGraph has a default recursion_limit of 25. You can raise it or lower it:

graph.invoke(initial, config={"recursion_limit": 50})

If the limit is hit, LangGraph raises an error. Treat that as a bug in the exit condition.

The Command Pattern

Newer LangGraph (0.2+) supports returning a Command from a node to specify both a state update and the next node in one return.

from langgraph.types import Command

def agent_node(state: State) -> Command:
    response = llm.invoke(state["messages"])
    next_node = "tools" if response.tool_calls else END

    return Command(
        update={"messages": [response]},
        goto=next_node,
    )

Benefits:

  • The routing logic lives with the node, not in a separate conditional-edges setup.
  • Handoffs between agents are cleaner.

You still use plain and conditional edges a lot; Command shines in multi-agent and handoff patterns (Chapter 9).

Parallel Execution

When you fan out, branches run in parallel (with some caveats; Python's GIL means CPU-bound work isn't truly parallel without processes, but I/O-bound calls to LLMs overlap).

builder.add_edge("plan", "search_web")
builder.add_edge("plan", "search_internal_docs")
builder.add_edge("plan", "search_database")

# All three run after plan; their results merge at the next node
builder.add_edge("search_web", "synthesize")
builder.add_edge("search_internal_docs", "synthesize")
builder.add_edge("search_database", "synthesize")

If each search takes 2 seconds and you serialize, 6 seconds. Parallel, 2 seconds.

Remember that the reducer merges results. If each search node returns {"results": [...]} with Annotated[list, add], all three get appended.

The Send API for Dynamic Fan-Out

If the number of branches depends on state (e.g. one branch per search result), use Send:

from langgraph.types import Send

def dispatch(state: State) -> list[Send]:
    return [Send("worker", {"item": item}) for item in state["items"]]

builder.add_conditional_edges("plan", dispatch, ["worker"])

Each Send kicks off one invocation of the target node with custom input. For map-reduce patterns, this is the tool.

Drawing the Graph

As in Chapter 2:

print(graph.get_graph().draw_ascii())

For conditional edges, the ASCII renderer shows them as dashed lines with the mapping. Useful for sanity-checking complex graphs.

Common Pitfalls

Forgotten path to END. Add a branch, forget to route it to END, graph loops forever or errors at recursion limit.

Conditional that returns a string not in the mapping. KeyError at runtime. Always cover every return value.

Cycles without progress. Loop that doesn't update state in a way the exit condition checks. Always verify the exit condition watches something that changes.

Parallel nodes that expect serialized reads. If two parallel branches both depend on the output of a third, they both get the pre-parallel state. If one needs the other's output, they're sequential, not parallel.

Overusing Command when plain edges are clearer. Command is powerful; plain and conditional edges read more clearly for simple flows. Don't reach for Command just because it's new.

Next Steps

Continue to 04-tools-and-agents.md to build a graph that actually does something.