The Graph Mental Model state that flows through nodes
This is the fast fresher on the basics. By the end you can picture any agent as a graph,
explain what state, nodes, and edges actually are, and build a tiny working graph from scratch. We move quick,
because the real climb starts in Module 02. Build along, do not just read.
Imagine you wired up an LLM the obvious way: one function takes a question, calls the model, returns the answer.
That works until the moment the task needs more than one step. The model wants to search the web, read the result,
decide it needs to search again, and only then answer. Suddenly your one straight line of code has to loop, branch,
remember what it already found, and sometimes pause for a human to approve something. Stuff a few if
statements and while loops around the model call and within a week you have a tangle nobody can follow.
LangGraph exists to give that tangle a shape. Instead of control flow buried inside one giant function,
you describe your agent as a graph: small steps (nodes) connected by arrows (edges), all reading and writing
to one shared piece of data (state). The framework then runs the graph for you, step by step, and it can save progress,
resume after a crash, stream partial output, and stop to ask a human, all because the work is broken into named, inspectable pieces.
One sentence to remember
LangGraph is a way to model an agent as a graph of steps over shared state, so that control flow becomes
something you can see, persist, and control, instead of something hidden inside a wall of conditionals.
If you have used LangChain before, here is the honest framing: LangChain gives you the building blocks (models, tools,
prompts). LangGraph gives you the orchestration, the part that decides what runs next and carries state between steps.
You can use LangGraph completely on its own; it does not require the rest of LangChain.
1
Meet Sahil's research agent
One character, one project, carried through all four modules. Sahil wants to build a research assistant:
you give it a question, it thinks, it looks things up, and it writes you a short answer. By Module 04 this becomes a small
team of cooperating agents. Right now, in Module 01, it is the simplest possible version: a question goes in, the model answers, done.
Sahil's first instinct is a single function. It is worth writing the naive version once, because seeing its limits is
the whole reason the graph idea will feel necessary rather than ceremonial.
naive_agent.py · the version we will outgrow
# Sahil's first attempt. Fine for one shot, painful the moment it needs to loop.defanswer(question):
reply = llm.invoke(question) # one model callreturn reply.content
# Now try to add: "if the model asks to search, search, then call it again".# You start nesting if/while around the call. State lives in scattered local# variables. There is no way to pause, save, or replay. This is the wall.
Sahil's problem
The instant the agent needs to decide and loop, a single function stops scaling. Sahil needs steps he can
name, a place to keep findings between steps, and someone to drive the loop. That someone is the LangGraph runtime.
2
The graph mental model
Hold one picture in your head and most of LangGraph follows from it. An agent is a graph made of three things:
State
One shared object every step can read and write. The agent's memory for this run. Think of a whiteboard in a room everyone walks through.
Nodes
The steps. Each node is just a function: it receives the current state and returns an update to it. Where the actual work happens.
Edges
The arrows. They decide which node runs next. Some are fixed (always go A then B), some are conditional (look at state, then choose).
The runtime walks the graph: it starts at a special START marker, runs whatever node the edges
point to, applies that node's update to the state, looks at the edges again to find the next node, and repeats until it
reaches the special END marker. That walk is the entire execution model. Everything else in this
whole track is a richer version of these three pieces.
START
edge
node A
edge
node B
edge
END
Every node reads the same state whiteboard, writes its change, and passes control along the edge.
The mental flip
You do not write a script that calls steps in order. You declare the steps and the wiring, then hand the
whole graph to the runtime and let it drive. Your job is to describe the map; the runtime does the walking.
3
State, the shared whiteboard
Before you can build a graph you have to answer one question: what does this agent need to remember while it runs?
That answer is your state schema. In Python you usually declare it as a TypedDict, which is just a
dictionary with named, typed fields. It is a contract: these are the slots on the whiteboard.
state.py · describe the whiteboard, do not fill it
from typing_extensions import TypedDict
# The schema is just "what fields exist and what type each is".classResearchState(TypedDict):
question: str # what the user asked
answer: str # what we will fill in# Module 02 will add findings, message history, and more.
When the graph runs, the state starts as whatever you pass in, and each node returns a small dictionary saying which
fields to change. You never mutate the state object directly. You return an update, and the runtime merges it.
That return-an-update rule is what lets LangGraph snapshot every step later, which is the entire basis of persistence and time travel in Module 02.
Common beginner trip
A node returns only the keys it wants to change, not the whole state. Return {"answer": text},
not a full copy of every field. By default the runtime overwrites just those keys. (How updates merge, and how to make
them append instead of overwrite, is the reducers topic in Module 02.)
4
Nodes and edges, made concrete
A node is the least mysterious thing in the framework: it is a plain function. It takes the current state and returns a
dict of updates. That is the whole interface. If you can write a function that reads a value and returns a new one, you can write a node.
nodes.py · a node is a function: state in, update out
defresearch_node(state: ResearchState) -> dict:
# read from state ...
q = state["question"]
reply = llm.invoke(q) # ... do the work ...return {"answer": reply.content} # ... return ONLY what changed
Edges are the wiring between nodes. There are two kinds, and the difference matters for the rest of the track:
Edge type
What it does
You declare it with
Normal edge
Always go from node A to node B. No decision.
add_edge(A, B)
Conditional edge
Run a small router function that looks at state and returns the name of the next node.
add_conditional_edges(A, router)
Two special markers bookend every graph. START is where execution begins; you draw an edge from
it to your first node. END is the exit; when an edge leads there, the run finishes and you get the final state back.
5
Build your first graph
Now the four moves you will repeat for the rest of your LangGraph life. Read them as a recipe: create the builder,
add nodes, add edges, compile. Sahil's one-node agent is the smallest thing that exercises all four.
StateGraph(ResearchState)1 · create a builder around your state schema.add_node(...)2 · register each step function.add_edge(...)3 · wire START → nodes → END.compile()4 · freeze it into a runnable graph
graph.py · the four moves (skeleton, you fill the body)
from langgraph.graph import StateGraph, START, END
builder = StateGraph(ResearchState) # 1 · builder over the schema
builder.add_node("research", research_node) # 2 · register the step
builder.add_edge(START, "research") # 3 · where to begin
builder.add_edge("research", END) # where to stop
graph = builder.compile() # 4 · freeze into a runnable# Run it: pass the initial state, get the final state back.
final = graph.invoke({"question": "What is LangGraph?"})
print(final["answer"])
That is a complete, working LangGraph application. One node, two edges. It is intentionally trivial, because the point of
Module 01 is the shape, not the size. Notice the symmetry of invoke: you hand the graph an
initial state dict and it hands you the final state dict. Same in, same out.
Why compile?
.compile() is the line between "describing the graph" and "having a thing you can run".
Compiling validates the wiring (every node reachable, edges point somewhere real) and returns an object with
invoke, stream, and more. It is also where you will later attach a
checkpointer for memory (Module 02).
Your turn
Add a second node called format that takes the raw answer and wraps it as "Answer: ...".
Wire it as research → format → END. You should only need to add one node and rewire one edge.
If you can do this without copying, the four moves have landed.
6
Conditional edges and the loop
A straight line of nodes is just a pipeline. What makes something feel like an agent is the ability to look at
the current state and decide what to do next, including doing the same step again. That decision lives in a
conditional edge: you give it a small router function that reads state and returns the name of the next node.
routing.py · a router returns the NAME of the next node
defshould_continue(state) -> str:
# Look at state, decide. Return a node name or END.if state["needs_more_research"]:
return"research"# loop back, run research againreturn END # good enough, finish
builder.add_conditional_edges("research", should_continue, ["research", END])
# the list is just the set of possible destinations, for validation
Read what that buys you: the research node can now point back at itself. The runtime keeps
walking the loop until the router returns END. This is exactly how the classic agent loop works,
think, act, look at the result, think again, and you built it from nothing but a function that returns a string.
START
research
router
decide
decide
more?
research
done
END
The dashed edge is the decision. Loop back, or exit. That branch is the whole difference between a pipeline and an agent.
Interview gold
"What is the difference between a normal edge and a conditional edge?" A normal edge is an unconditional jump (always A
to B). A conditional edge runs a router function over the current state and that function returns which node comes next,
which is how LangGraph expresses branching and loops. Cycles are allowed and expected; that is what "graph" buys you over a plain chain.
7
MessagesState, the shortcut you will use constantly
Almost every agent's state needs to hold a growing list of chat messages: the human's turn, the model's reply, tool
results, and so on. This is so common that LangGraph ships a ready-made state schema for it called
MessagesState. It has a single field, messages, and that field is special:
when a node returns new messages, they are appended to the list rather than replacing it.
messages.py · the append-by-default chat state
from langgraph.graph import StateGraph, START, MessagesState
defchat_node(state: MessagesState) -> dict:
reply = llm.invoke(state["messages"]) # model sees full historyreturn {"messages": [reply]} # appended, not overwritten
builder = StateGraph(MessagesState)
builder.add_node("chat", chat_node)
builder.add_edge(START, "chat")
graph = builder.compile()
graph.invoke({"messages": [{"role": "user", "content": "hi"}]})
That append behaviour is not magic; it comes from a reducer attached to the messages
field. A reducer is a small rule that says "when a node returns a value for this field, combine it with the old value like
this". For messages the rule is "add to the list". You will define your own reducers in Module 02; for now just
know that MessagesState is the batteries-included version, and reaching for it saves you boilerplate in most agents.
Where this is going
You now have every primitive an agent needs: a place to keep history (state), steps that act (nodes), and a way to
decide what runs next (edges, including loops). In Module 02 the agent stops forgetting between runs. In Module 03 it learns
to pause for you. In Module 04 it becomes a team.
8
Hands-on: prove it to yourself
Do not move to Module 02 until you have actually run a graph. Set up a fresh environment, install the package, and build
the loop with your own fingers. The skeleton below is deliberately incomplete in the places that matter, so you have to think.
setup · terminal
# a clean venv keeps your machine sane
python -m venv .venv && source .venv/bin/activate
pip install langgraph langchain # core + model helpers# pick ONE model provider you have a key for, e.g. langchain-openai or langchain-anthropic
exercise.py · fill the three TODOs
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
classState(TypedDict):
topic: str
joke: str
rounds: int
deftell_joke(state):
# TODO 1: call your llm with state["topic"], return {"joke": ...,# "rounds": state.get("rounds", 0) + 1}
...
defgood_enough(state):
# TODO 2: return END if rounds >= 3, else "tell_joke" (loop)
...
builder = StateGraph(State)
builder.add_node("tell_joke", tell_joke)
builder.add_edge(START, "tell_joke")
# TODO 3: add a conditional edge from "tell_joke" using good_enough
graph = builder.compile()
print(graph.invoke({"topic": "databases", "joke": "", "rounds": 0}))
You created a StateGraph over a TypedDict and compiled it without errors.
You ran graph.invoke(...) and got a final state dict back.
Your conditional edge made the graph loop and then stop on its own.
You can explain, out loud, what state / node / edge each mean in your own words.
9
Interview check
A small taste here; the full bank lives in Module 05. Cover the answer, say yours out loud, then expand.
= must-know cold.
Q1In one breath, what are the three core pieces of a LangGraph graph?
State (one shared, typed object every step reads and writes), nodes (functions that take state and
return an update), and edges (the wiring that decides which node runs next, fixed or conditional). The runtime walks
from START to END, applying each node's update to state.
Q2Why a graph instead of just a chain or a loop in plain Python?
A graph makes control flow explicit and inspectable: branches and cycles become named edges instead of buried
conditionals. Because each step returns an update rather than mutating shared state, the runtime can snapshot every step,
which unlocks persistence, resuming after a crash, streaming, and human-in-the-loop. Plain loops give you none of that for free.
Q3What does a node receive and what must it return?
It receives the current state and returns a dict of only the fields it wants to change. The runtime merges
that update into state (overwrite by default, or via a reducer for fields like messages). A node never
mutates state in place.
Q4What is the difference between a normal edge and a conditional edge?
A normal edge (add_edge(A, B)) always goes A to B. A conditional edge
(add_conditional_edges(A, router)) runs a router function over the current state, and that function
returns the name of the next node, which is how you express branching and loops.
Q5What is MessagesState and why is its messages field special?
A built-in state schema with a single messages list, pre-wired with a reducer that appends
new messages instead of overwriting. It saves you from rebuilding chat-history plumbing in every agent.
Q6What does .compile() actually do?
It turns the builder (a description) into a runnable graph: it validates the wiring and returns an object exposing
invoke, stream, etc. It is also where you attach cross-cutting features like a
checkpointer (Module 02) for persistence.