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.