LangGraph: state, nodes and edges

LangChain · Advanced · 15 min

When you need control over every step, branches and loops, LangGraph is the tool LangChain agents themselves use.

LangGraph models an application as a graph over shared state:

  • State is a TypedDict (or Pydantic model/dataclass). Each key can have a reducer that says how updates merge: by default a new value overwrites; Annotated[list, operator.add] appends; Annotated[list, add_messages] appends and updates messages by ID. MessagesState is a ready-made state with a messages key.
  • Nodes are functions node(state) -> dict returning only the keys they update.
  • Edges connect nodes: add_edge(START, "a"), add_edge("a", "b"), add_edge("b", END).
  • Conditional edges route dynamically: add_conditional_edges("check", route_fn) where route_fn(state) returns the next node's name (or END).
builder = StateGraph(State)
builder.add_node("classify", classify)
...
graph = builder.compile()
graph.invoke({"text": "..."})

The compiled graph is a runnable: invoke, stream (with modes values, updates, messages, custom), async variants, and graph.get_graph().draw_mermaid() for a diagram.

Loops are just edges back to earlier nodes, guarded by a condition; recursion_limit in config stops runaway loops.

Use LangGraph directly when the flow is known (classify, then branch, then validate), mixing deterministic code with LLM calls. Use create_agent when the model should decide the flow. They combine: an agent can be a node, and a graph can be a tool.

Example

import operator
from typing import Annotated, TypedDict
from langgraph.graph import END, START, StateGraph

class State(TypedDict):
    text: str
    category: str
    log: Annotated[list[str], operator.add]     # reducer: append updates
def classify(state: State):
    cat = "refund" if "refund" in state["text"].lower() else "general"
    return {"category": cat, "log": [f"classified as {cat}"]}
def route(state: State) -> str:
    return "refund_flow" if state["category"] == "refund" else "general_flow"

builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("refund_flow", lambda s: {"log": ["opened refund ticket"]})
builder.add_node("general_flow", lambda s: {"log": ["sent FAQ link"]})
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route, ["refund_flow", "general_flow"])
builder.add_edge("refund_flow", END)
builder.add_edge("general_flow", END)
graph = builder.compile()
print(graph.invoke({"text": "I want a refund", "log": []})["log"])
# ['classified as refund', 'opened refund ticket']

Remember

  • State is a TypedDict; reducers decide how node updates merge
  • Nodes return partial updates; edges and conditional edges define the flow
  • Use explicit graphs for known flows, agents when the model should decide

Watch out for

  • Returning the whole state from a node while a key has an append reducer, which duplicates list entries.

Check yourself

  1. A state key is declared as Annotated[list, operator.add]. Two nodes return {'log': ['a']} and {'log': ['b']}. What is log afterwards (starting from [])?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.langchain.com/oss/python/langgraph/graph-api