A linear chain of LLM calls, step 1 feeds step 2 feeds step 3, is easy to reason about and easy to build. It’s also structurally unable to express one of the most common patterns in agent design: “if step 3’s output fails a check, go back to step 2 and try again.” A chain has no step before the one it’s currently on. Once you need a step to conditionally route backward instead of only forward, you’ve left pipeline territory and you need something that can represent a loop: a state graph, with actual back-edges, not just a sequence.
What a back-edge looks like as code
In a research-agent project built on LangGraph, the critic step doesn’t sit at the end of a chain, it sits inside a cycle. After the critic scores a draft, the routing function decides whether the graph is done or needs to go back to an earlier node:
def route_after_critic(state: AgentState) -> str:
"""Decides if we are done or need to retry, and routes through memory writer."""
feedback = state.get("critic_feedback", "")
revisions = state.get("revision_count", 0)
if feedback == "APPROVED":
return "memory_writer"
elif revisions >= 3:
console.print(
"[bold red]Max revisions reached. Ending to save costs.[/bold red]"
)
return END
else:
return "synthesizer"
That last branch is the back-edge: route_after_critic can send control flow to synthesizer, the same node that produced the draft the critic just rejected. In a pipeline, “synthesizer” would only ever appear once, upstream of “critic.” Here it appears as a destination the graph can return to, conditionally, based on state computed downstream of it. That’s not expressible as a sequence of function calls chained together; it requires something with named nodes and edges that can point anywhere, including backward, which is what build_graph() sets up:
builder.add_conditional_edges("critic", route_after_critic)
The same file has a second conditional edge earlier in the graph, route_after_memory_retriever, which is a fork rather than a loop but shows the same underlying need: normal pipeline code executes what’s next, a graph node decides what’s next based on the state it just computed.
def route_after_memory_retriever(state: AgentState) -> str:
"""If a cached report was found, short-circuit the pipeline."""
if state.get("draft_report"):
return "critic"
return "researcher"
If a cached report already exists for the topic, the graph skips straight to the critic and never runs the researcher at all. A fixed pipeline can’t skip a step conditionally any more than it can repeat one; both require a node that gets to choose its own successor.
The loop needs a way to end, and that has to be explicit
A back-edge on its own is a way to write an infinite loop. route_after_critic guards against that with a counter checked before the loop path is taken:
revisions = state.get("revision_count", 0)
...
elif revisions >= 3:
return END
revision_count is incremented inside the critic node itself, every time a draft is rejected, and the cap (three) is what turns “loop until the critic is satisfied” into “loop until the critic is satisfied or we’ve spent three revisions trying.” Without that cap, a critic that’s slightly too strict, or a synthesizer that keeps making the same mistake, would retry forever. The cap doesn’t make the system correct, it makes it terminate, which is a weaker but non-optional guarantee: a system with no bound on retries isn’t a system you can put a cost or latency budget on at all.
Why this is the actual reason to reach for a graph
The instinct to reach for LangGraph, or any graph-based orchestration, is often framed around flexibility in the abstract: nodes and edges are more general than a chain. The concrete reason it matters is narrower and easier to check for in your own design: does any step’s output determine whether an earlier step needs to run again? If yes, you have a cycle, and a cycle needs three things a plain chain doesn’t give you for free: a routing function that inspects state instead of always advancing, a named back-edge to the node that needs to re-run, and a counter with a cap so the cycle is guaranteed to end. A prompt-chaining library that only supports “call this, then call that” can approximate a fixed number of retries by unrolling them into repeated steps, but it can’t express “however many retries the critic actually needs, up to a cap” as a single reusable path, because it has no concept of returning to a node it already visited.