Conditional edges and loops
A router function reads the state and names the next node. Point it back at an earlier node and the graph loops — which is powerful, and needs a stop condition.
The router is a function
A conditional edge is a function of the state that returns the name of the next node. It does not call anything and it does not mutate anything; it decides. "revise" if the critique found a problem, "finish" if it did not.
Keeping the decision in one small function is what makes a graph readable. The nodes do the work; the routers say where the work goes. A router that also does work is a node in disguise.
A worked router reads the state in a fixed order. If attempts is three or more, return “give_up” first, so the cap cannot be overridden by a high score. Otherwise, if score is below 0.8, return “revise”. Otherwise return “finish”. Swap the first two checks and a failing third attempt revises again, past the cap, until the recursion limit.
A loop is just an edge backwards
There is no loop construct. You add an edge from a later node back to an earlier one, and the runtime follows it again. The state carries what changed, so the second pass starts from the first pass’s result.
That is exactly the draft, critique, revise pattern. Draft once, critique, and if the critique is unhappy, go back to the draft node with the critique in the state.
A worked pass: draft writes text and attempts one. Critique writes score 0.6 and a note. The router returns “revise”, the edge goes back to draft, and the second draft reads both the first text and the critique. If the critique is not in the state, the second draft is just another first draft with a higher counter. The critique also has to be replaced, not appended: a stale note from the first pass sends the second pass in the same direction.
Routing back
def should_revise(state) -> str:
if state["attempts"] >= 3:
return "give_up"
if state["score"] < 0.8:
return "revise"
return "finish"
builder.add_conditional_edges("critique", should_revise, {
"revise": "draft",
"finish": "publish",
"give_up": END,
})A loop with a counter
Run it and watch the attempts rise. Then remove the counter check and see the step limit fire — that is the backstop, and it is not a stop condition.
Every loop needs three things
A counter in the state, a condition that checks it, and a route out that is guaranteed to be taken. Without the counter a graph that keeps failing runs until the recursion limit, which is a crash rather than a decision.
The escape route should be a real node, not an exception. A graph that gives up should write that into the state, so the caller can see what happened and decide whether to retry later.
The three things are a counter, a check on it, and an exit that is always reachable. Write them into the state: attempts, status, and the reason. A give-up route that writes status “gave up” and the best draft is a result the caller can act on; an exception is not. The counter belongs in the state because the recursion limit is a crash, not a decision.
LangGraph stops a runaway loop with a recursion limit and raises. Treat that as a bug in the router, not as the stop condition — it is the backstop, not the design.
Guard the router, not the node
The router is the smallest function in the graph and the one most worth testing. Give it a table of states and expected routes: attempts 0 score 0.5 → revise; attempts 3 score 0.9 → give_up; attempts 1 score 0.9 → finish. The table runs in milliseconds.
A default branch is the other guard. If the router returns a name the graph does not know, the run fails at the edge rather than silently stopping. Map every return value explicitly and let an unknown value raise.
Loops change the cost model
A chain costs the same every time. A loop costs more when it runs more, and a bad input can triple the bill. That is another reason the counter belongs in the state from the first version rather than after the first surprise.
If the loop calls a model, consider escalating rather than repeating: the second attempt can use a stronger model or a longer prompt, since the first attempt has already failed.
A budget makes the cost concrete. Three attempts at two thousand tokens each is six thousand tokens per request, and a bad input reaches the cap every time. Set the cap from the worst case you can afford, not from the number that makes the demo pass.
Escalate instead of repeating
The first attempt uses a cheap model and the second uses a strong one. Run it, then make the strong model succeed on the first try and confirm the loop exits earlier.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What is a conditional edge?
