RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Fredoline Eruo
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /Courses
  5. /LangGraph for Local Agents
  6. /Ch. 2
LangGraph for Local Agents

02. StateGraph Basics

Chapter 2 of 18 · 15 min
KEY INSIGHT

The `Annotated` + `operator.add` pattern is how LangGraph handles list fields without data loss. Skipping it causes silent state overwrites that are hard to debug because they produce no error.

Every LangGraph application starts with a TypedDict schema that defines the shape of your application state. This is not ceremonial—it is the contract that every node reads from and writes to. When a node returns a dict, LangGraph performs a deep merge of that dict into the existing state using your schema. If your schema has a messages: list[str] field, returning {"messages": ["hello"]} appends to the list, not replaces it. This behavior is called functional state updates and it is the reason LangGraph works well for multi-turn conversations.

To define the schema:

from typing import TypedDict, Annotated
from operator import add

class AgentState(TypedDict):
    messages: Annotated[list[str], add]
    current_step: str
    artifact: str | None

The Annotated with operator.add tells LangGraph to merge list updates by appending rather than overwriting. Without it, returning a new list from a node replaces the old one. This distinction trips up many operators who then wonder why their message history disappears mid-conversation.

Building the graph follows the same four-step pattern: create the builder, add nodes, connect edges (or the START/END sentinels), compile. The START and END constants are string constants that resolve to "__start__" and "__end__" internally. Adding an edge from "my_node" to END means that node is a terminal node—no outgoing edge from it will be followed.

builder = StateGraph(AgentState)
builder.add_node("process", process_node)
builder.add_edge(START, "process")
builder.add_edge("process", END)
compiled = builder.compile()

One structural nuance: compile() returns a CompiledGraph which is a LangChain Runnable. This means it works with .stream(), .asyncinvoke(), .batch(), and LangChain's built-in callbacks. The graph does not run until you call .invoke() or .stream() on the compiled object.

EXERCISE

Define an AgentState with two fields: turns: Annotated[list[str], add] and active_worker: str | None. Build a graph with one node that appends a message and sets active_worker. Inspect the compiled graph's get_input_schema() to confirm your schema appears.

← Chapter 1
What is LangGraph?
Chapter 3 →
Nodes and Edges