learn-langchain-with-phoebe / Builder session 5 of 10
Learn LangChain with Phoebe · Builder track · Session 5 of 10

The LangGraph mental model

Four sessions in, create_agent has carried DataDesk well - but it is ONE fixed graph shape, chosen by someone else. Tonight you eject downward and meet the runtime underneath: state, nodes, edges. By the end DataDesk has a spine you designed, and every "how do I make it do X?" question for the rest of the track has the same answer - change the graph.

🟠 Builder track Practitioners: DA · DE · DS · engineers Python 3.10+ · langgraph>=1.2 · your b3 DataDesk file 45 min
0-3 · Welcome 3-20 · From loop to graph 20-42 · Build-along: DataDesk gets a spine 42-45 · Q&A
Part 0

Why tonight changes the track

Sessions b2-b4 lived at the create_agent altitude: one model, some tools, middleware around a loop you never saw. That loop is a LangGraph graph - a prebuilt one. Real systems need shapes create_agent does not ship: a router in front of two specialists, a deterministic validation step after every model call, parallel branches, a cycle with an exit condition. LangGraph is the machine for building those shapes, and the whole API is smaller than you fear: one state schema, functions as nodes, edges between them, compile. Everything b6-b9 adds - memory, approval gates, multi-agent - is a feature OF this machine, which is why tonight is the hinge session of the track.

Live - presented in session Self-study - read after class ★ Try it now prompt Official docs + Academy covered
★ What you walk out with today The four LangGraph primitives (state, nodes, edges, conditional edges) in your fingers, DataDesk v2 rebuilt as a 3-node routed graph that wraps your b3 agent as one node, the ability to read any LangGraph codebase by finding its build sequence, and a printed diagram of a graph you built yourself.
Part 1 · covers LangGraph graph-api docs + Academy LangGraph Essentials

From loop to graph 9 min live

The agent loop you wrote by hand in b1 is one control-flow shape among many. Graphs let you draw the shape your problem actually has - and keep the drawing and the code the same artifact.

1 · Chain load summarize answer fixed edges only deterministic, auditable 2 · Router classify stats fallback conditional edges: a function picks the path 3 · Agent model tools a cycle: loops until the model stops asking for tools create_agent IS shape 3, prebuilt. LangGraph lets you build all three - and mix them.
🔍 Click to zoom - chain, router, agent: three shapes, one graph API
LiveWhy graphs - the shapes create_agent cannot draw4 min

create_agent gives you exactly one control flow: model → tools → model, repeat. It is the right default - but the moment DataDesk needs anything else, you are fighting the abstraction instead of using it:

  • Routers: "stats questions go to the CSV agent, docs questions go to retrieval, small talk gets a cheap model." A classifier in front of specialists - not expressible as one agent loop.
  • Deterministic segments: "ALWAYS validate the SQL before running it." Fixed steps the model cannot skip - a workflow edge, not a model choice.
  • Cycles with YOUR exit condition: "retry the extraction until the schema validates, max 3 times." You own the loop counter, not the model.
  • Parallel paths: "profile all 12 tables at once, then merge." Fan-out and fan-in.

The 2026 division of labor: start at create_agent, eject to LangGraph when the shape demands it. Because create_agent runs ON LangGraph, ejecting is a refactor, not a rewrite - your tools, middleware lessons and model swap all carry over.

★ Try it now (any chat AI)Here are 3 AI features on my team's wishlist: [list 3 real ones]. For each, tell me which control-flow shape it needs - fixed chain, router, cycle/agent, or parallel fan-out - and draw it as a 5-line ASCII graph. One sentence on why.
LiveThe four primitives - the entire vocabulary5 min

Everything in LangGraph is built from four ideas. Learn these and you have learned the framework:

Four primitives, the entire vocabulary STATE shared doc every node reads & writes State(TypedDict) NODES functions returning a PARTIAL update return {'route':'x'} EDGES fixed arrow, A then always B add_edge('a','b') COND. EDGES routing fn returns the next node name add_conditional_edges Nodes return partial updates only; routing functions return node names, not objects.
🔍 Click to zoom - learn these four ideas and you have learned the framework
PrimitiveWhat it isIn code
StateOne typed, shared document every node reads and writes - a TypedDict, dataclass, or Pydantic modelclass State(TypedDict): ...
NodesPlain Python functions: take the state, return a PARTIAL update (only the keys you changed)def step(state): return {"route": "stats"}
EdgesFixed arrows: after node A, always node B. START and END are built-in endpointsadd_edge("a", "b")
Conditional edgesA routing function reads the state and returns the NAME of the next nodeadd_conditional_edges("a", pick, {...})

Two details that prevent 90% of beginner bugs: nodes return partial updates, never the whole state (LangGraph merges them for you), and routing functions return strings - node names - not node objects.

Real world

The whiteboard that compiled. A data team designed their pipeline-triage assistant as boxes and arrows in a design review: classify → (sql_check | doc_search) → summarize. The LangGraph file that shipped two days later had exactly those node names. When an auditor later asked "what can this system do?", they printed the graph. The diagram IS the code - that is the property you are buying tonight.

Self-studyCommand, the functional API, and when to eject4 min read
  • Command - update and go in one move. A node can return Command(update={"route": "stats"}, goto="stats") instead of relying on a separate conditional edge - state change and routing decision travel together. Cleaner when the node that computes the decision should also act on it.
  • Send - the map-reduce edge. A conditional edge can return a list of Send("worker", {...}) objects to fan one item of work out to N parallel node runs, each with its own payload. This is the engine under session b9's map-reduce pattern - namecheck now, hands-on later.
  • The functional API (@entrypoint / @task) wraps ordinary Python control flow - ifs, loops - and still gets checkpointing. It suits "mostly normal code with a few LLM calls"; the graph API suits systems you want to draw, inspect, and gate. This course teaches the graph API; know the other exists.
  • The eject checklist. Move from create_agent down to LangGraph when you need: custom control flow (routers, validation steps, retry loops with your exit condition), deterministic segments the model must not skip, or multi-agent coordination. Stay at create_agent when the tool-calling loop plus middleware covers it - altitude is a feature, not a compromise.
Part 2 · covers the graph-api build sequence + langgraph 1.2 node options

Reading a graph like code 5 min live

Every LangGraph file, from tutorials to Uber-scale production, is the same five-call build sequence. Once you can spot it, unfamiliar codebases open like a map.

LiveThe build sequence - the whole API in five calls5 min

Say it as a sentence: declare the state, register the nodes, wire the edges, compile. In code:

★ The five-call skeleton - every LangGraph file everfrom langgraph.graph import StateGraph, START, END g = StateGraph(DataDeskState) # 1 · the shared state schema g.add_node("classify", classify) # 2 · functions become nodes g.add_edge(START, "classify") # 3 · fixed arrows g.add_conditional_edges( # 4 · a function picks the path "classify", pick_route, {"stats": "stats", "other": "fallback"}) graph = g.compile() # 5 · build it - returns a runnable

Reading order for someone else's graph: find the StateGraph(...) call to learn the state schema, scan the add_node lines for the cast of characters, then follow edges from START. The compile() call is where the later superpowers plug in - compile(checkpointer=...) is next week's entire session in one argument.

The five-call build sequence: every LangGraph file ever STEP 1 STATEGRAPH declare the state schema STEP 2 ADD_NODE functions become nodes STEP 3 ADD_EDGE fixed arrows, wire them STEP 4 COND_EDGES a function picks the path STEP 5 COMPILE() build it, get a runnable Say it as a sentence: declare the state, register the nodes, wire the edges, compile.
🔍 Click to zoom - learn the five-call sequence and any LangGraph file opens like a map
Naming discipline Node names are strings you will see again in traces, interrupts and diagrams. Name them like verbs in a runbook ("classify", "draft", "save_report"), not like variables ("node1", "llm_call_2") - future-you reads these under incident pressure.
Self-studyReducers, multiple schemas, and the 1.2 node options4 min read
  • Reducers - how updates merge. By default a returned key OVERWRITES the old value. Annotate a key with a reducer to change that: messages: Annotated[list, add_messages] makes message updates APPEND (and dedupe by id) instead of replace. That one annotation is why every node can return just its new message and the conversation still accumulates. create_agent uses exactly this under the hood.
  • Multiple schemas. A graph can declare separate input and output schemas, and nodes can use private state channels the caller never sees - useful when internal bookkeeping (retry counts, routing labels) should not leak into your API.
  • Per-node timeouts + error handlers (langgraph 1.2). Nodes can now carry their own timeout and an error handler, so one slow or flaky step fails alone instead of hanging the whole run. Pair with b4's retry middleware thinking: retries at the agent altitude, timeouts at the node altitude.
  • State typing choices. TypedDict is the lightweight default; dataclasses add defaults; Pydantic models add runtime validation at a small speed cost. Start TypedDict, upgrade the day bad state actually bites.
Demo 1 of 2

DataDesk gets a spine ★ 12 min · everyone builds

DataDesk v2: a router graph that reads each question, sends stats questions to your existing b3 create_agent (now demoted to one node among peers), and answers everything else with a plain model call. Three nodes, one conditional edge, about 40 lines.

Open your DataDesk project. Confirm pip install "langgraph>=1.2,<2" and that your b3 file exposes agent (the create_agent object) importably.

Define DataDeskState: a TypedDict with messages (annotated with add_messages) and route. This is the shared document all nodes will pass around.

Write three node functions: classify (asks the model for one word - stats, docs or other - and writes it to route), stats_agent (wraps your b3 agent), and fallback (plain model answer).

Wire it: START → classify, conditional edges off pick_route, both specialists → END. Compile.

Run the three test questions below and watch route differ per question - a stats question exercises your whole b3 agent inside one node; a haiku request never touches it. That divergence is the moment: control flow is yours now.

★ datadesk_v2.py - the whole filefrom typing import TypedDict, Annotated from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from datadesk_v1 import agent # your b3 create_agent, unchanged model = init_chat_model("claude-haiku-4-5-20251001") # or "ollama:llama3.1" class DataDeskState(TypedDict): messages: Annotated[list, add_messages] route: str def classify(state: DataDeskState): q = state["messages"][-1].content verdict = model.invoke("Reply with exactly one word - stats, docs or other. " f"Which kind of question is this: {q}") return {"route": verdict.content.strip().lower()} def stats_agent(state: DataDeskState): result = agent.invoke({"messages": state["messages"]}) return {"messages": result["messages"][-1:]} def fallback(state: DataDeskState): return {"messages": [model.invoke(state["messages"])]} def pick_route(state: DataDeskState) -> str: return state["route"] if state["route"] in ("stats", "docs") else "other" g = StateGraph(DataDeskState) g.add_node("classify", classify) g.add_node("stats", stats_agent) g.add_node("fallback", fallback) g.add_edge(START, "classify") g.add_conditional_edges("classify", pick_route, {"stats": "stats", "docs": "fallback", "other": "fallback"}) # docs gets a real home in b8 g.add_edge("stats", END) g.add_edge("fallback", END) graph = g.compile() for q in ["What is the median of the amount column in data.csv?", "Where is our data dictionary kept?", "Write me a haiku about dashboards"]: out = graph.invoke({"messages": [{"role": "user", "content": q}]}) print(f"{q[:40]:42} route={out['route']:6} {out['messages'][-1].content[:60]}")
If the router misroutes Small local models sometimes answer "Stats." with punctuation - that is why the code lowercases and strips, and why pick_route defaults anything unrecognized to "other". Defensive routing is production thinking, not demo polish.
Demo 2 of 2

Draw what you built ★ 8 min · build your own

A compiled graph knows its own shape. Print it, trace one run through the printed picture, and prove the engine swap still holds at this altitude.

Add two lines to the bottom of datadesk_v2.py and re-run - the compiled structure appears as a Mermaid diagram (paste it into any Mermaid renderer) or straight ASCII in your terminal.

Trace last demo's haiku question through the printed diagram with your finger: START → classify → fallback → END. The stats branch never fired. What you drew is what ran - no hidden steps.

Engine-swap test: flip the init_chat_model line to the other engine and re-run all three questions. The graph, the routing logic and the diagram are engine-agnostic - only answer quality and speed change.

Keep the printed diagram. In b6 the same picture grows a checkpointer; in b7 a pause gate appears between two of these boxes.

★ Two lines to see the machineprint(graph.get_graph().draw_mermaid()) # paste into a Mermaid renderer graph.get_graph().print_ascii() # or straight to the terminal
Real world

The diagram in the design review. Teams that ship agents to regulated environments increasingly attach the rendered graph to change requests: reviewers approve a shape, not a prose description. Your two-line print is the toy version of a real governance artifact - the leader track's a4 audience is being taught to ASK for this exact picture.

Homework

Try it yourself - this week ◐ 30-45 min total

Source material

Official sources covered

This track teaches from the official docs and the free LangChain Academy curricula (login required for lesson content; certificates stay with the Academy - all free). This page covers:

LangGraph graph-api docsParts 1-2 · StateGraph, nodes, edges, conditional edges, START/END, compile, reducers
Academy: LangGraph EssentialsWhole session · the four primitives + build sequence are its core module, taught here on DataDesk
Academy: Intro to LangGraph (M1)Demo 1 covers the router-graph build; M1's chatbot variant left as an exercise pattern
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · A LangGraph node is...

No classes, no magic: functions in, partial dict updates out, LangGraph merges them (via reducers where declared). Returning the whole state is the classic beginner bug.

2 · How does execution decide between the stats node and the fallback node in DataDesk v2?

add_conditional_edges registers a plain function that returns a string. The model only wrote "stats" into state; YOUR function turned that into control flow.

3 · When should you eject from create_agent down to LangGraph?

create_agent is production-grade for the tool-loop shape and runs ON LangGraph. Eject when the SHAPE no longer fits - routers, guaranteed steps, cycles you control, multiple agents.

Builder session 5 cheat sheet · pin this

The four primitivesState (shared typed dict) · Nodes (functions, partial updates) · Edges (fixed) · Conditional edges (routing functions).
The build sequenceStateGraph(State) → add_node → add_edge / add_conditional_edges → compile(). Every LangGraph file, ever.
Three shapesChain (fixed), router (conditional), agent (cycle). create_agent = the cycle, prebuilt. Mix freely.
ReducersDefault: overwrite. Annotated[list, add_messages] = append. Nodes return only what changed.
Command / SendCommand(update=..., goto=...) = state change + routing in one. Send() = map-reduce fan-out (b9).
See the machinegraph.get_graph().draw_mermaid() or .print_ascii() - the diagram IS the code.
Eject checklistCustom control flow · deterministic must-run steps · multi-agent. Otherwise stay at create_agent.
Running projectDataDesk v2: router graph wrapping the b3 agent as one node. Next: b6 gives it memory that survives a restart.