Unit 04.02: Retries that do not loop forever
Retry is the most common cycle in an agentic graph and the easiest one to get subtly wrong.
Counter, ceiling, and a give-up node
Three pieces are needed together: a counter in the state, a ceiling checked in the router, and a give-up node distinct from the success node.
The code runs a step that succeeds on the third attempt.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
MAX_ATTEMPTS = 3
class State(TypedDict):
attempts: int
ok: bool
outcome: str
def attempt(state: State):
n = state["attempts"] + 1
return {"attempts": n, "ok": n >= 3} # succeeds on the third try
def decide(state: State):
if state["ok"]:
return "succeed"
if state["attempts"] >= MAX_ATTEMPTS:
return "give_up"
return "attempt"
g = StateGraph(State)
g.add_node("attempt", attempt)
g.add_node("succeed", lambda s: {"outcome": f"ok after {s['attempts']}"})
g.add_node("give_up", lambda s: {"outcome": f"failed after {s['attempts']}"})
g.add_edge(START, "attempt")
g.add_conditional_edges("attempt", decide,
{"attempt": "attempt", "succeed": "succeed", "give_up": "give_up"})
g.add_edge("succeed", END)
g.add_edge("give_up", END)
print(g.compile().invoke({"attempts": 0, "ok": False, "outcome": ""}))
# Three things make a retry loop safe: a counter in the state, a ceiling checked
# in the router, and a distinct give-up node. Drop any one and the graph either
# runs until the recursion limit or reports failure as success.
The router checks success before the ceiling, so a run that succeeds on the final permitted attempt is reported as success rather than as exhaustion. That ordering matters and is easy to get backwards.
The separate give_up node is what makes the outcome distinguishable downstream. Falling through to the success node after the ceiling produces a run that reports success having achieved nothing - the worst of the available failure modes, because nothing alerts.
The mistake this prevents
The mistake is retrying without asking whether the failure is retryable. A timeout is worth retrying; a validation error on the same input will fail identically three times, costing three times as much and delaying the error by three attempts. Classify the failure before looping on it.
Takeaway
A safe retry needs a counter in the state, a ceiling in the router, and a distinct give-up path. Check success before the ceiling, and only retry failures that could plausibly succeed next time.
