Branching and looping are what make a graph more than a fixed pipeline.
A normal edge always goes to the same node. A conditional edge calls a routing function after a node finishes; the function looks at the state and returns the name of the next node.
def route(state: State) -> Literal["refund_flow", "faq"]:
return "refund_flow" if state["category"] == "billing" else "faq"
builder.add_conditional_edges("classify", route)Literal[...] in the return type lists the possible answers. LangGraph uses it to know where the arrows go (for drawing and validation). Instead, you can pass the options explicitly: add_conditional_edges("classify", route, ["refund_flow", "faq"]), or a dict mapping return values to node names.
A router may return END to stop, or the name of an earlier node to form a loop. Loops are how agents work: call the model, run tools, go back to the model, until a condition says stop.
Every loop needs an exit condition. As a safety net LangGraph counts super-steps (rounds of node execution) and raises GraphRecursionError when a run exceeds recursion_limit. Since LangGraph 1.0.6 the default is 1000. Set a tighter limit per run with graph.invoke(inputs, {"recursion_limit": 25}); note it goes at the top level of the config, not inside configurable.
Keep routing functions pure: read the state, return a name, no side effects. Do the work in nodes.
Example
from typing import Literal, TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
draft: str
revisions: int
def write(state: State) -> dict:
n = state["revisions"] + 1
return {"draft": f"draft v{n}", "revisions": n}
def good_enough(state: State) -> Literal["write", "__end__"]:
return END if state["revisions"] >= 3 else "write" # loop back or stop
b = StateGraph(State)
b.add_node(write)
b.add_edge(START, "write")
b.add_conditional_edges("write", good_enough)
graph = b.compile()
print(graph.invoke({"draft": "", "revisions": 0}, {"recursion_limit": 10}))
# {'draft': 'draft v3', 'revisions': 3}Remember
- add_conditional_edges(source, router) picks the next node at run time
- Declare possible targets with Literal or an explicit list/dict
- Return END to stop or an earlier node to loop
- recursion_limit (default 1000) guards against endless loops