Why Graphs, Not Chains
A chain runs its steps in a fixed order. A graph can choose the next step and go back to an earlier one. Learn the difference with everyday examples, real code, and your first LangGraph program.
What you will be able to do
- Understand the words used in this module: workflow, chain, graph, node, edge, state, cycle
- Explain what a chain is, and which kind of work it suits
- Say exactly what a chain can do (choose a path, retry an error) and what it cannot do (go back because of a result)
- Explain what a graph is, using an everyday example
- Read and run your first LangGraph program, line by line
- Explain why every AI agent is a graph
- Explain why a loop needs state and a limit
- Decide, for a new task, whether you need a chain or a graph
The idea, in plain English
This module teaches LangGraph. LangGraph is a Python library for building workflows as graphs. A workflow is a list of steps that together finish a job - for example: read a support ticket, find the customer’s order, write a reply.
In Module 2 you built workflows with LangChain chains. A chain is a line of steps: step 1, then step 2, then step 3. Every input goes through the same steps in the same order. For many jobs this is perfect.
But some jobs cannot be a straight line. Example: a customer says "my payment failed". You check the payment. The bank says "pending" - not finished yet. Now you must go back and check again, maybe three times, and if it is still pending, give the case to a human. The next step depends on what just happened. A chain cannot go back to an earlier step. A graph can.
In this lesson we compare chains and graphs with everyday examples and real code. At the end you will write your first small LangGraph program. Lesson 3.2 then explains each part of LangGraph in detail. All code below was run with LangChain 1.4.3, LangGraph 1.2.14 and the llama3 model on Ollama.
Worked example: A payment support workflow that checks a transaction, checks again while it is "pending", and asks a human after 3 tries.
1 - Classify, then choose a path
The classify step decides the question is about billing. Then the workflow chooses the next step from that answer. A chain can also do this (with RunnableBranch).
Customer: "My payment failed but money was deducted." This run is real: the payment check returned pending, pending, then settled.
The model and the tools take turns until the model has the answer. Nobody knows in advance how many turns that takes.
Words you will see in this module
Before we start, here are the words this module uses again and again. Come back to this table whenever a word is not clear.
WorkflowAll the steps that together finish one job. Example: read ticket -> find order -> write reply.StepOne piece of work inside a workflow, like "call the LLM" or "check the payment".ChainA workflow whose steps always run in the same fixed order (LangChain, Module 2).GraphA workflow drawn as boxes and arrows, where the next step can change and arrows can go back.NodeA box in the graph: one step. In LangGraph a node is a Python function.EdgeAn arrow in the graph: it says which node runs next.Conditional edgeAn arrow with a choice: a small function looks at the data and picks the next node.Cycle (loop)An arrow that goes back to a node that already ran, so that step can run again.StateThe shared data of one run - the question, the answers so far, a counter. Every node can read it and change it.START / ENDWhere a run begins and where it stops.CompileTurn your graph definition into something you can run.InvokeRun it once with some input: graph.invoke(...).An everyday example: making tea and washing clothes
Making tea is like a chain. Boil water, add tea leaves, add milk and sugar, pour into a cup. You always do the same steps in the same order. You never need to go back.
A washing machine is like a graph. Fill water, wash, then check: are the clothes still dirty? If yes, go back and wash again. Is the water drained? If no, wait and check again. Only then rinse, spin, and finish. The next step depends on what the machine finds. Some steps can run many times.
Software works the same way. When every input follows the same steps, use a chain. When the next step depends on a result, or a step may have to run again, you need a graph.
Making teaChain. Same steps, same order, every time.Washing machineGraph. "Still dirty?" sends it back to wash again.Printing a documentChain. Open -> format -> print.Calling customer careGraph. "Press 1 for billing" chooses a path; "invalid option, try again" goes back.Google Maps navigationGraph. Missed a turn? It goes back to "plan the route" and plans again.What a chain is good at
A chain is a pipeline: each step takes the output of the step before it. You built many in Module 2: prompt | llm | parser. Summarize a document. Translate English to Telugu. Classify a message. Extract JSON from text. In all of these the steps are known before you start, and every input takes the same path.
Because the path never changes, a chain is easy to read, easy to test, and easy to fix. For this kind of work a chain is the right tool - do not replace it with a graph. Here is a real chain with llama3. It has three steps, and it will always run exactly these three steps.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
# Step 1: a prompt template with one empty slot, {ticket}
prompt = ChatPromptTemplate.from_template(
"Summarize this support ticket in one short sentence:\n\n{ticket}"
)
# Step 2: the language model
llm = ChatOllama(model="llama3", temperature=0)
# Step 3: turn the model's message into a plain string
parser = StrOutputParser()
# The | symbol joins the steps into a chain: prompt, then llm, then parser
chain = prompt | llm | parser
ticket = (
"Hi, I paid for the Python course yesterday. The payment page showed an error, "
"but the money was taken from my bank account. I still cannot open the course."
)
print(chain.invoke({"ticket": ticket}))The customer paid for a Python course yesterday, despite an error on the payment page, but is still unable to access the course.A chain can choose a path
People often say "chains cannot make decisions". That is not fully true. LangChain has RunnableBranch: it checks conditions one by one and runs the first branch whose condition is true. The last item is the default - it runs when no condition matches.
In the example below, a simple keyword function decides the category. Then RunnableBranch sends billing questions to the billing team, technical questions to the tech team, and everything else to the general desk. So a chain can choose one path. But notice: after choosing, it still only moves forward.
from langchain_core.runnables import RunnableBranch, RunnableLambda
def classify(question):
q = question.lower()
if any(w in q for w in ("payment", "refund", "invoice", "charged")):
return "billing"
if any(w in q for w in ("error", "crash", "login", "bug")):
return "technical"
return "general"
route = RunnableBranch(
# (condition, what to run if the condition is true)
(lambda x: x["category"] == "billing", RunnableLambda(lambda x: f"billing team: {x['question']}")),
(lambda x: x["category"] == "technical", RunnableLambda(lambda x: f"tech team: {x['question']}")),
# the default: runs when no condition above is true
RunnableLambda(lambda x: f"general desk: {x['question']}"),
)
# step 1 adds the category, step 2 chooses the branch
chain = RunnableLambda(lambda q: {"question": q, "category": classify(q)}) | route
print(chain.invoke("My payment failed"))
print(chain.invoke("Login shows an error"))
print(chain.invoke("What courses do you have?"))billing team: My payment failed
tech team: Login shows an error
general desk: What courses do you have?A chain can retry an error
A chain can also repeat a step when that step fails with an error (an exception). with_retry(stop_after_attempt=3) means: if this step raises an error, try again, up to 3 times in total.
In the example, a pretend service fails twice with TimeoutError and works on the third try. with_retry handles that. But look at the comment at the end: with_retry only reacts to errors, and it only repeats this one step.
from langchain_core.runnables import RunnableLambda
attempts = {"n": 0}
def flaky_service(_):
attempts["n"] += 1
if attempts["n"] < 3:
raise TimeoutError("service timeout") # fails on try 1 and try 2
return f"ok on attempt {attempts['n']}"
step = RunnableLambda(flaky_service).with_retry(stop_after_attempt=3, wait_exponential_jitter=False)
print(step.invoke(None))
# with_retry works only when the step RAISES AN ERROR,
# and it repeats only this one step.
# A bank answer "pending" is a normal result, not an error -
# so with_retry does nothing, and nothing sends the run back.ok on attempt 3What a chain cannot do: go back because of a result
Here is the real limit of a chain. Imagine the payment check returns "pending". That is not an error, so with_retry does not fire. It is a normal answer that means "ask me again later". Now the workflow must go back to the check step. Maybe once, maybe three times - we do not know before we start.
An AI agent has the same problem. The model reads the question and says "I need the weather tool". After the tool answers, the model may ask for another tool, or it may give the final answer. The number of rounds is decided while the program runs.
A chain’s order is fixed when you write the code. It has no way to say "now go back to that earlier step". To do this you need a loop. In plain Python you would write a while loop - you will see that in the code section below. LangGraph gives you the same loop as an arrow in a graph, which you can see, print and control.
Steps in a fixed orderChain: yes - its natural shape. Graph: yes.Choose one path by a conditionChain: yes, with RunnableBranch. Graph: yes, with conditional edges.Repeat a step that raised an errorChain: yes, with with_retry. Graph: yes.Go back because of a resultChain: no. Graph: yes - an edge back to an earlier node.Loop until the model stops asking for toolsChain: no. Graph: yes - the agent loop.Shared data for all stepsChain: each step only gets what the step before passes on. Graph: one state that every node reads and updates.Pause and wait for a humanChain: no. Graph: yes - Lesson 3.7.What is a graph?
A graph is boxes and arrows. The boxes are called nodes. Each node is one piece of work: call an LLM, run a tool, query a database, check a value, ask a human. The arrows are called edges. An edge says which node runs next.
There are two kinds of edges. A normal edge always goes to the same next node: "after escalate, always run respond". A conditional edge makes a choice: it calls a small function (a routing function) that looks at the current data and returns the name of the next node.
When an edge goes back to a node that already ran, the graph has a cycle - a loop. In our support workflow, after "check" the routing function can return "check" again. Cycles are what make retries and agent loops possible.
NodeOne piece of work: classify, check, respond.EdgeAlways run this node next.Conditional edgeA function looks at the state and picks the next node.CycleAn edge back to an earlier node.START / ENDWhere a run enters and where it leaves.Your first LangGraph program
Let us write the smallest useful graph. Install LangGraph first: pip install langgraph. It installs langchain-core with it, but your nodes do not have to use LangChain at all - here they are plain Python functions, with no LLM.
The program has four parts. 1. State: a TypedDict that describes the data of one run - here a name and a message. 2. Nodes: functions that receive the state and return only the fields they want to change. 3. Edges: START -> greet -> shout -> END. 4. Compile and invoke: compile() checks the graph and makes it runnable; invoke() runs it once with the input {"name": "Ravi"}.
This graph is a straight line - that is allowed. A graph does not have to branch or loop. Lesson 3.2 explains each of these four parts in much more detail.
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
# 1. The state: the data that every node can read
class State(TypedDict):
name: str
message: str
# 2. The nodes: plain Python functions.
# Each one receives the state and returns only what it changes.
def greet(state: State):
print(" greet is running")
return {"message": f"Hello, {state['name']}!"}
def shout(state: State):
print(" shout is running")
return {"message": state["message"].upper()}
# 3. The graph: add the nodes, then connect them with edges
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_node("shout", shout)
builder.add_edge(START, "greet") # the run starts at greet
builder.add_edge("greet", "shout") # after greet, run shout
builder.add_edge("shout", END) # after shout, stop
# 4. Compile, then run once
graph = builder.compile()
result = graph.invoke({"name": "Ravi"})
print(result) greet is running
shout is running
{'name': 'Ravi', 'message': 'HELLO, RAVI!'}__start__ --> greet;
greet --> shout;
shout --> __end__;
How to read it:
__start__ / __end__ START and END
--> a normal edge (always goes this way)
-.-> a conditional edge (a function chooses) - you will see it belowEvery AI agent is a graph
Look at the second diagram above. An agent works like this: the model reads the question. If it needs information, it asks for a tool (a tool call). The tool runs and returns a result. The model reads the result and decides again: another tool, or the final answer. This repeats until the model stops asking for tools.
Here is a real run from Lesson 3.5. Question: "Should I bring an umbrella in Mumbai, and what is 12 * 8?" The model asked for get_weather("Mumbai") and got "Rainy, 27°C". Then it asked for calculate("12 * 8") and got 96. Then it answered: "Yes, bring an umbrella in Mumbai. The answer to 12 * 8 is 96." That is model -> tool -> model -> tool -> model: a loop with two rounds. Another question may need zero rounds, or five.
You have already used this graph. create_agent in Lesson 2.7 returned a LangGraph graph: printing it showed the nodes __start__, model, tools, __end__ and the edges model -> tools or __end__, and tools -> model. In this module you will build that loop yourself (Lessons 3.4 and 3.5), so you can change it - for example, add a human approval before a tool runs (Lesson 3.7).
Loops need state and a limit
To decide "go round again or stop?", a loop must remember what happened so far: the status of the last check, and how many times we checked. This shared memory of one run is called state. In our support graph the state holds question, category, attempts and status. Nodes read the state and return changes to it. Routing functions read it to choose the next edge. Lesson 3.2 is all about state.
A loop also needs a limit, or it may never stop. We use two limits. First, your own rule: our routing function escalates after 3 attempts. Second, LangGraph’s safety limit, recursion_limit: the maximum number of steps in one run. We ran the graph with recursion_limit=4 and a payment that stayed pending forever. It stopped with GraphRecursionError: Recursion limit of 4 reached without hitting a stop condition.
Watch out: A loop without an exit rule runs until it hits recursion_limit and fails with an error. Always write your own exit rule (a success check and a maximum count). recursion_limit is only the safety net.
Practice: chain or graph?
Read each task and decide before you look at the answer. The question to ask is always the same: does the next step depend on a result, or might a step need to run again?
Translate an email to TeluguChain. Prompt -> LLM -> text, the same for every email.Summarize a PDFChain. Load -> split -> summarize. No step goes back.Route a ticket to billing, tech or generalEither. A chain with RunnableBranch is enough if nothing comes back.Check a payment until it is settled, max 3 timesGraph. "Pending" sends it back to check.Chatbot that can use a calculator and web searchGraph. The model decides how many tool calls to make - a loop.Write code, run tests, fix and retry until tests passGraph. Failing tests send it back to "fix".Draft an email, wait for the manager to approve, then sendGraph. It must pause and continue later (Lesson 3.7).Extract name, phone and city from a form as JSONChain. Prompt -> LLM -> JSON parser.When to use which - and common questions
Use a chain when every input follows the same steps. Use a graph when the next step depends on what happened: a category that chooses a path, a result that needs another try, a model that decides whether to call a tool, a human who must approve. A good habit: draw the workflow on paper first. If every arrow just points to the next box, write a chain.
Do I need an LLM to use LangGraph? No. The support graph in this lesson has no LLM at all - only Python functions. LangGraph controls the order of the steps; what happens inside a step is up to you.
Does LangGraph make the model smarter? No. It does not change the model. It gives you control over the workflow around the model: which step runs, how many times, and when to stop or wait.
Is LangGraph only for multi-agent systems? No. One agent with one tool loop, or one workflow with a retry, is already a good reason to use it. Multi-agent systems (Lesson 3.8) are just one use.
Step-by-step code
# Uses classify() from Example 2.
# service_results pretends to be the bank: one answer per check.
def run_support(question, service_results, max_attempts=3):
state = {"question": question, "category": classify(question), "attempts": 0, "path": ["classify"]}
if state["category"] != "billing":
state["path"].append(state["category"])
return state
while True: # the loop (the cycle)
state["attempts"] += 1
state["status"] = service_results[state["attempts"] - 1]
state["path"].append(f"check#{state['attempts']}={state['status']}")
if state["status"] == "settled": # exit 1: success
state["answer"] = "Payment went through."
break
if state["attempts"] >= max_attempts: # exit 2: too many tries
state["path"].append("escalate")
state["answer"] = "Escalated to a human."
break
state["path"].append("respond")
return state
question = "My payment failed but money was deducted"
for results in (["settled"], ["pending", "pending", "settled"], ["pending"] * 4):
s = run_support(question, results)
print(" -> ".join(s["path"]), "|", s["answer"])classify -> check#1=settled -> respond | Payment went through.
classify -> check#1=pending -> check#2=pending -> check#3=settled -> respond | Payment went through.
classify -> check#1=pending -> check#2=pending -> check#3=pending -> escalate -> respond | Escalated to a human.
run_support("Login shows an error", [])["path"] -> classify -> technical# Uses classify() from Example 2 and question from Example 5.
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
# THE STATE: everything the run needs to remember
class SupportState(TypedDict, total=False):
question: str
category: str
attempts: int
status: str
answer: str
results: list # the pretend bank answers
path: list # which nodes ran, for printing
# THE NODES: each does one job and returns what it changes
def classify_node(s):
return {"category": classify(s["question"]), "attempts": 0, "path": ["classify"]}
def check(s):
attempt = s["attempts"] + 1
status = s["results"][attempt - 1]
return {"attempts": attempt, "status": status, "path": s["path"] + [f"check#{attempt}={status}"]}
def technical(s): return {"answer": "technical answer", "path": s["path"] + ["technical"]}
def general(s): return {"answer": "general answer", "path": s["path"] + ["general"]}
def escalate(s): return {"answer": "Escalated to a human.", "path": s["path"] + ["escalate"]}
def respond(s): return {"answer": s.get("answer") or "Payment went through.", "path": s["path"] + ["respond"]}
# THE ROUTING FUNCTIONS: they return the NAME of the next node
def after_classify(s): # choose a path
return {"billing": "check", "technical": "technical"}.get(s["category"], "general")
def after_check(s): # the loop, and its two exits
if s["status"] == "settled":
return "respond"
return "escalate" if s["attempts"] >= 3 else "check"
# THE GRAPH: nodes + edges
builder = StateGraph(SupportState)
for name, fn in [("classify", classify_node), ("check", check), ("technical", technical),
("general", general), ("escalate", escalate), ("respond", respond)]:
builder.add_node(name, fn)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", after_classify, ["check", "technical", "general"])
builder.add_conditional_edges("check", after_check, ["respond", "escalate", "check"])
for name in ("technical", "general", "escalate"):
builder.add_edge(name, "respond")
builder.add_edge("respond", END)
graph = builder.compile()
out = graph.invoke({"question": question, "results": ["pending", "pending", "settled"]})
print(" -> ".join(out["path"]))classify -> check#1=pending -> check#2=pending -> check#3=settled -> respond
graph.get_graph().draw_mermaid() - the edges, as LangGraph compiled them
__start__ --> classify
classify -.-> check classify -.-> technical classify -.-> general
check -.-> respond check -.-> escalate check -.-> check <- the loop
technical --> respond general --> respond escalate --> respond
respond --> __end__
( -.-> conditional edge, --> always )
The same three runs as Example 5 - the graph gives the same paths:
classify -> check#1=settled -> respond
classify -> check#1=pending -> check#2=pending -> check#3=settled -> respond
classify -> check#1=pending -> check#2=pending -> check#3=pending -> escalate -> respond
graph.invoke({... "results": ["pending"] * 50}, {"recursion_limit": 4})
GraphRecursionError: Recursion limit of 4 reached without hitting a stop condition.Tip: Compare Example 5 and Example 6. They do the same thing. In plain Python the loop is hidden inside while True and if statements. In LangGraph the loop is an edge, check -.-> check, that you can print and see. As workflows grow - more branches, more loops, pauses for humans - that visibility is what keeps them understandable.
Chains and graphs at a glance
ChainSteps in a fixed order.
prompt | llm | parser
RunnableBranchA chain choosing one of several paths.
RunnableBranch((condition, runnable), ..., default)
with_retryRepeat one step when it raises an error.
step.with_retry(stop_after_attempt=3)
Install LangGraphThe library used in this module.
pip install langgraph
StateGraphA graph whose nodes share one state.
StateGraph(State)
EdgeAlways go to this node next.
builder.add_edge("greet", "shout")Conditional edgeA function picks the next node.
add_conditional_edges("check", after_check)Run itCompile once, then invoke.
graph = builder.compile(); graph.invoke({...})recursion_limitThe maximum number of steps in one run.
graph.invoke(state, {"recursion_limit": 10})draw_mermaidPrint the graph’s nodes and edges.
graph.get_graph().draw_mermaid()
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“In Example 4, add a third node "add_emoji" between shout and END that adds " :)" to the message. Run it and print the graph.”
“On paper, draw a password-reset workflow: request, send code, verify, try again up to 3 times, lock the account. Circle the arrows that go back.”
“In Example 5, remove the max_attempts check and give ["pending"] * 10. What happens? Then put the check back.”
“In Example 2, add an "account" branch for questions about passwords.”
“In Example 6, invoke with ["pending"] * 50 and {"recursion_limit": 4}. Read the error. Then remove the limit and see your own rule stop it.”
“Think of an app you use - food delivery, train booking. Name two places where a step goes back because of a result.”
What usually goes wrong
Prompt -> LLM -> parser is a chain. Building a graph for it only adds code. Use a graph when something can branch, repeat or wait.
RunnableBranch chooses a path, and with_retry repeats a step after an error. The real limit is going back because of a normal result.
The routing function must, at some point, return a node that leaves the loop. Count the attempts in the state and stop at a maximum.
✗ return "check" if s["status"] != "settled" else "respond"✓ if s["status"] == "settled": return "respond"
return "escalate" if s["attempts"] >= 3 else "check"A graph is the structure of a workflow. It can have one model, many, or none - the support graph here has no LLM at all.
Edges only list the possible paths. Your routing functions - or a model inside a node - choose which path is taken.
Key points
- A workflow is the list of steps that finish a job.
- A chain runs its steps in a fixed order - best when every input takes the same steps.
- Chains can choose a path (RunnableBranch) and retry an error (with_retry).
- A chain cannot go back to an earlier step because of a result - a graph can.
- A graph is nodes (pieces of work) and edges (what runs next); a conditional edge chooses the next node.
- An edge back to an earlier node is a cycle (loop) - the base of retries and agents.
- Every AI agent is a loop: model -> tool -> model, until there is no tool call.
- A loop needs state to decide and a limit to stop: your own rule first, recursion_limit as the safety net.
- A LangGraph program has four parts: state, nodes, edges, compile + invoke.
Quick check before you move on
Quiz
- 1.
with_retry repeats a failing step. Why is that not enough for "check again while the payment is pending"?
- 2.
In the printed graph you see check -.-> check. What does it mean?
- 3.
In Example 4, which function runs first, and why?
- 4.
What stops a loop that never reaches its exit?
- 5.
Does a graph have to contain an LLM?
- 6.
A task: "Write code, run the tests, and fix the code until the tests pass." Chain or graph?
Interview questions
Why would you use LangGraph instead of a LangChain chain?
When the workflow needs control that a fixed sequence cannot give: loops driven by results, agent tool loops, shared state, saving progress, or pausing for human approval. For a fixed sequence, a chain is simpler.
Chains support branching and retries. So what do graphs add?
Cycles and shared state. A chain can choose a branch and retry an exception, but it cannot go back to an earlier step because of a result, and it has no single state that every step reads and updates.
How is an agent different from a chain?
A chain’s steps are fixed when you write the code. An agent’s path is chosen while it runs - the model decides which tools to call and how many times - so it is a loop with a condition decided by the model.
What are nodes and edges in LangGraph?
Nodes are functions that do work and return updates to the state. Edges decide what runs next; conditional edges call a routing function on the state to pick the next node.
How do you prevent infinite loops in a graph?
Write an explicit exit in the routing function - a success condition and a maximum count kept in the state - and keep recursion_limit as the hard stop.
Is LangGraph only for multi-agent systems?
No. A single agent with one tool loop, or a workflow with one retry, already benefits. Multi-agent systems are one use among many.
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned