Graph Basics — Nodes, Edges, Compile¶
1. Why this matters¶
If you understand state (Chapter 2) and this chapter, you can build 80% of LangGraph apps. Everything else — conditional routing, loops, persistence, agents — is built on this skeleton.
2. Mental model¶
Build → wire → compile → run.
flowchart LR
Schema[Define State<br/>TypedDict] --> Build[builder = StateGraph state]
Build --> Nodes[add_node 'name', fn]
Nodes --> Edges[add_edge a, b<br/>add_conditional_edges ...]
Edges --> Compile[graph = builder.compile]
Compile --> Run[graph.invoke initial_state]
The compile step validates the graph and binds optional features (checkpointer, interrupts, debug). The runnable returned implements LCEL's interface, so it slots into bigger pipelines.
3. Architecture / Flow¶
A complete graph in pictures:
flowchart TD
START((START)) --> A[Node A]
A --> B[Node B]
B --> R{Condition}
R -->|yes| C[Node C]
R -->|no| D[Node D]
C --> ENDN((END))
D --> ENDN
In code that becomes:
builder.add_edge(START, "A")
builder.add_edge("A", "B")
builder.add_conditional_edges("B", route_fn, {"yes": "C", "no": "D"})
builder.add_edge("C", END)
builder.add_edge("D", END)
4. Core concepts¶
StateGraph(StateSchema)— the builder. Pass theTypedDict(or Pydantic) class as the schema.add_node("name", fn)— registers a function as a node. Name is how edges refer to it.add_edge("from", "to")— static edge. After "from" runs, "to" always runs next.add_conditional_edges("from", router_fn, mapping?)—router_fn(state) -> strpicks the destination node name; optionalmappingtranslates router output to actual node names.START— pseudo-node where the run begins. Always add an edge fromSTART.END— pseudo-node where the run terminates. Nodes that reachENDproduce the final state.set_entry_point("name")— shortcut foradd_edge(START, "name").compile(checkpointer=..., interrupt_before=..., debug=...)— finalize.invoke(input, config)— run synchronously, return final state.stream(input, config, stream_mode=...)— yield intermediate state.get_graph().print_ascii()/.draw_mermaid()— visualize.
5. Code — minimal working example¶
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
x: int
y: int
def add_one(state: State):
return {"y": state["x"] + 1}
builder = StateGraph(State)
builder.add_node("add_one", add_one)
builder.add_edge(START, "add_one")
builder.add_edge("add_one", END)
graph = builder.compile()
print(graph.invoke({"x": 41, "y": 0})) # {'x': 41, 'y': 42}
6. Code — real-world pattern¶
Three-stage processing pipeline with logging via state:
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
class State(TypedDict):
topic: str
outline: str
blog: str
summary: str
log: Annotated[list[str], add] # accumulating audit log
def make_outline(state: State):
out = llm.invoke([HumanMessage(f"Outline a blog about {state['topic']}.")])
return {"outline": out.content, "log": ["outline_done"]}
def write_blog(state: State):
out = llm.invoke([HumanMessage(f"Write the blog using this outline:\n{state['outline']}")])
return {"blog": out.content, "log": ["blog_done"]}
def summarize(state: State):
out = llm.invoke([HumanMessage(f"One-sentence TL;DR of:\n{state['blog']}")])
return {"summary": out.content, "log": ["summary_done"]}
b = StateGraph(State)
b.add_node("outline", make_outline)
b.add_node("write", write_blog)
b.add_node("sum", summarize)
b.add_edge(START, "outline")
b.add_edge("outline", "write")
b.add_edge("write", "sum")
b.add_edge("sum", END)
graph = b.compile()
final = graph.invoke({
"topic": "vector embeddings",
"outline": "", "blog": "", "summary": "", "log": [],
})
print(final["summary"])
print("Audit:", final["log"]) # ['outline_done', 'blog_done', 'summary_done']
Visualize it:
Stream intermediate state instead of waiting for the final value:
for snapshot in graph.stream(initial, stream_mode="updates"):
# 'updates' yields {node_name: partial_update} per step
print(snapshot)
7. Common pitfalls¶
- ❗ Forgetting
STARTandENDedges. Compile fails with "no entry point" / "no terminal node". Every graph needs both. - ❗ Node name conflicts. Two nodes with the same name overwrite. Use clear, unique names.
- ❗ Using a Python variable instead of a string in
add_edge. Edges reference nodes by name string, not function object. - ❗ Conditional router returning a node name not in the mapping. Raises at runtime. Either avoid the mapping (return node names directly) or include all paths including
END. - ❗ Calling
.invoke()on the builder. You mustcompile()first — the builder isn't runnable. - ❗ Multiple nodes pointing to one downstream node without a join semantics decided. LangGraph runs parallel branches concurrently; reducers must handle the merge.
8. When to use vs not use¶
| Use this | When |
|---|---|
add_edge (static) |
Step always follows step |
add_conditional_edges |
Routing depends on state |
set_entry_point |
Shorthand for the one START edge |
compile() with checkpointer |
You need persistence (see Chapter 8) |
compile() with interrupt_before |
Human-in-the-loop |
compile() with debug=True |
Verbose logging during development |
9. Cheatsheet¶
from langgraph.graph import StateGraph, START, END
# Build
builder = StateGraph(StateSchema)
builder.add_node("name1", fn1)
builder.add_node("name2", fn2)
builder.set_entry_point("name1") # or add_edge(START, "name1")
builder.add_edge("name1", "name2")
builder.add_conditional_edges(
"name2",
router_fn, # state -> str
{"branch_a": "name1", "branch_b": END}, # optional mapping
)
builder.set_finish_point("name2") # or add_edge("name2", END)
# Compile
graph = builder.compile(
checkpointer=...,
interrupt_before=["risky_node"], # HITL pause
interrupt_after=[],
debug=False,
)
# Run
graph.invoke(initial_state, config={"configurable": {"thread_id": "t1"}})
graph.stream(initial_state, config=..., stream_mode="updates") # or "values" / "messages" / "debug"
graph.batch([state1, state2])
# Inspect
graph.get_graph().print_ascii()
graph.get_graph().draw_mermaid() # returns Mermaid string
graph.get_state(config) # current snapshot (needs checkpointer)
list(graph.get_state_history(config)) # all snapshots
graph.update_state(config, {"key": "new"})
10. Q&A — recall test¶
-
Q: Two ways to set the entry of a graph? A:
builder.add_edge(START, "first")orbuilder.set_entry_point("first"). They're equivalent. -
Q: What does
add_conditional_edgesneed thatadd_edgedoesn't? A: A router function that takes state and returns a string (the next node's name). Optionally a mapping translating router output to node names. -
Q: Why is
compile()separate from building? A: Compile validates the graph (entry, terminal, no orphans), binds optional features (checkpointer, interrupts), and returns an immutable Runnable. Keeps build-time vs run-time concerns clean. -
Q: What's the difference between
stream_mode="values"and"updates"? A:"values"yields the full state after each step."updates"yields the partial update each node returned."messages"yields LLM tokens for streaming chat UIs. -
Q: How do you visualize a compiled graph? A:
graph.get_graph().print_ascii()(terminal) orgraph.get_graph().draw_mermaid()(Mermaid source — drop into a Markdown file).
Practice¶
What does this print?
Expected: True
Compile the graph before invoking it
Expected: True
Quiz — Quick check¶
What you remember
Q1. What's a node in LangGraph?
- A function that takes the current state and returns a partial state update
- A class
- An LLM
- A tool
Why: Nodes are functions. They read state (first arg) and return a dict with the fields they want to update. LangGraph merges that into the global state using the schema's reducers.
Q2. What's the role of START and END in a graph?
- Special sentinel nodes marking where execution begins (START) and terminates (END)
- Required imports
- Logging hooks
- State fields
Why: Every graph needs at least one entry edge from
STARTto a real node, and at least one edge ending atEND. Without these, the graph can't execute or terminate.
Q3. Why must you .compile() a graph before invoking it?
- Compilation validates the structure, sets up checkpointers, and creates the executable Runnable
- To save it to disk
- To save memory
- Optional — works without
Why: Before compilation, the graph is just a builder.
.compile()produces aCompiledGraph(a Runnable) with.invoke(),.stream(), etc. Pass options likecheckpointer=to compile.
Common doubts¶
Can a node call another node?
No, not directly. Nodes communicate through state. To "call" another node, return state that routes the graph to it (via conditional edges). This separation keeps the graph predictable and debuggable.
How do I have multiple nodes share computation?
Compute once in a shared node early in the graph, store the result in state, then read it from subsequent nodes. Don't duplicate computation across nodes.
What happens to fields a node doesn't return?
They keep their previous value. Nodes return partial state updates — only the fields that changed. Unchanged fields are inherited from the previous state.