Unit 01.01: Branching, cycles, and resuming
Here is the smallest graph that does something a chain cannot: it writes a draft, reviews it, and goes back to writing if the review is not satisfied.
A cycle, and the counter that terminates it
The graph has three pieces beyond the nodes: an entry point, a conditional edge that reads state and returns a destination, and a state field that the cycle increments.
Read decide first. It is a function from state to a node name, and it is where the cycle is either bounded or not.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
draft: str
attempts: int
def write(state: State):
return {"draft": "draft v" + str(state["attempts"] + 1),
"attempts": state["attempts"] + 1}
def review(state: State):
return state # a real reviewer would set a flag here
def decide(state: State):
return END if state["attempts"] >= 3 else "write"
g = StateGraph(State)
g.add_node("write", write)
g.add_node("review", review)
g.add_edge(START, "write")
g.add_edge("write", "review")
g.add_conditional_edges("review", decide, {"write": "write", END: END})
print(g.compile().invoke({"draft": "", "attempts": 0}))
# The cycle write -> review -> write is the thing a chain cannot express.
# Note `attempts`: a cycle without a counter is an infinite loop, and the
# counter has to live in the state because that is the only thing that moves.
The edge review -> write is the cycle. Nothing else in the graph is unusual, and that single edge is what a chain cannot express.
attempts is doing the essential work. Without it, decide has nothing to test and the graph loops until LangGraph's recursion limit stops it - which surfaces as an exception rather than as the give-up path you wanted. A cycle needs a counter, and the counter has to live in the state because the state is the only thing that survives from one node to the next.
The mistake this prevents
The mistake is putting the counter in a Python variable outside the graph. It works in a single-process test and fails the moment the run is checkpointed and resumed, because the variable was never part of what got saved. Anything a router reads must be in the state.
Takeaway
A cycle is one conditional edge pointing backwards. Every cycle needs a counter in the state and a ceiling checked in the router, or it terminates by exception rather than by design.
