LangGraph Cheat Sheet — Stateful Agents & Graphs — LangGraph
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message_graph import MessageGraph
from langgraph.prebuilt import create_react_agent
from typing import TypedDict, Annotated
from operator import add State machine for agents: define nodes, edges, and persistent state between steps.
Like a flowchart with memory: each node is a decision/action point, edges are transitions, and state is a shared whiteboard that persists across the entire workflow. The agent can loop back, branch on conditions, and pause for human input.
Core Patterns
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
import os
from langchain_openai import ChatOpenAI
class AgentState(TypedDict):
messages: list
counter: int
def node_a(state: AgentState) -> AgentState:
"""First processing step"""
return {**state, "counter": state["counter"] + 1}
def node_b(state: AgentState) -> AgentState:
"""Second processing step"""
return {**state, "counter": state["counter"] * 2}
graph = StateGraph(AgentState)
graph.add_node("step_a", node_a)
graph.add_node("step_b", node_b)
graph.add_edge(START, "step_a")
graph.add_edge("step_a", "step_b")
graph.add_edge("step_b", END)
compiled = graph.compile()
result = compiled.invoke({"messages": [], "counter": 0})
print(result["counter"]) # 2 counter incremented sequentially through nodes from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Literal
class AgentState(TypedDict):
messages: list
sentiment: str
def analyze_sentiment(state: AgentState) -> AgentState:
text = state["messages"][-1]["content"]
sentiment = "positive" if "good" in text.lower() else "negative"
return {**state, "sentiment": sentiment}
def route_based_sentiment(state: AgentState) -> Literal["happy_path", "escalate"]:
"""Conditional router—returns next node name"""
return "happy_path" if state["sentiment"] == "positive" else "escalate"
def happy_path_node(state: AgentState) -> AgentState:
state["messages"].append({"role": "assistant", "content": "Glad to help!"})
return state
def escalate_node(state: AgentState) -> AgentState:
state["messages"].append({"role": "assistant", "content": "Let me escalate this."})
return state
graph = StateGraph(AgentState)
graph.add_node("analyze", analyze_sentiment)
graph.add_node("happy_path", happy_path_node)
graph.add_node("escalate", escalate_node)
graph.add_edge(START, "analyze")
graph.add_conditional_edges("analyze", route_based_sentiment)
graph.add_edge("happy_path", END)
graph.add_edge("escalate", END)
compiled = graph.compile()
result = compiled.invoke({"messages": [{"role": "user", "content": "This is good"}], "sentiment": ""})
print(result["messages"][-1]["content"]) # Glad to help! routing decision based on state from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Literal
import json
class AgentState(TypedDict):
messages: list
iterations: int
stop: bool
def should_continue(state: AgentState) -> Literal["tools", END]:
"""Break loop if stop=True or max iterations reached"""
if state["stop"] or state["iterations"] >= 3:
return END
return "tools"
def agent_node(state: AgentState) -> AgentState:
"""Simulate agent reasoning step"""
state["iterations"] += 1
state["messages"].append({"role": "assistant", "content": f"Step {state['iterations']}"})
if state["iterations"] >= 2:
state["stop"] = True
return state
def tool_node(state: AgentState) -> AgentState:
"""Execute tools"""
state["messages"].append({"role": "tool", "content": "Tool result"})
return state
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
compiled = graph.compile()
result = compiled.invoke({"messages": [], "iterations": 0, "stop": False})
print(f"Completed in {result['iterations']} iterations") loop continues until break condition triggers from langgraph.graph.message_graph import MessageGraph
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"])
# MessageGraph automatically manages messages as state
graph = MessageGraph()
graph.add_node("agent", llm)
graph.add_edge("agent", "__end__")
graph.set_entry_point("agent")
compiled = graph.compile()
# Invoke with just message content
result = compiled.invoke({"messages": [{"role": "user", "content": "What is 2+2?"}]})
print(result["messages"][-1].content) LLM response to query from langgraph.graph import StateGraph, START, END
from langgraph.types import Interrupt
from typing import TypedDict
class AgentState(TypedDict):
messages: list
approved: bool
def propose_action(state: AgentState) -> AgentState:
state["messages"].append({"role": "assistant", "content": "Proposed action"})
# This raises an Interrupt—execution pauses here
raise Interrupt({"status": "awaiting_approval"})
def execute_action(state: AgentState) -> AgentState:
if not state["approved"]:
state["messages"].append({"role": "system", "content": "Action rejected"})
return state
state["messages"].append({"role": "system", "content": "Action executed"})
return state
graph = StateGraph(AgentState)
graph.add_node("propose", propose_action)
graph.add_node("execute", execute_action)
graph.add_edge(START, "propose")
graph.add_edge("propose", "execute")
graph.add_edge("execute", END)
compiled = graph.compile()
# First invocation pauses at interrupt
thread_id = "thread_1"
try:
result = compiled.invoke({"messages": [], "approved": False}, {"configurable": {"thread_id": thread_id}})
except Exception as e:
print(f"Paused: {e}")
# Resume with human approval
final = compiled.invoke({"messages": [], "approved": True}, {"configurable": {"thread_id": thread_id}}) execution pauses, waits for input, then resumes from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
import os
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
llm = ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"])
tools = [multiply, add]
# ReAct agent: LLM + tool loop built-in
agent = create_react_agent(llm, tools)
result = agent.invoke({"messages": [{"role": "user", "content": "What is (5 * 3) + 2?"}]})
print(result["messages"][-1].content) agent response with tool calls executed Key APIs & Methods
| Method / Property | Description | Returns |
|---|---|---|
StateGraph(state_schema: Type) | Create a graph with typed state dict. State persists across all nodes. | StateGraph instance |
graph.add_node(name: str, func: Callable) | Add a processing node. func receives state, returns modified state. | StateGraph (for chaining) |
graph.add_edge(source: str, target: str) | Add deterministic edge from source to target node. | StateGraph (for chaining) |
graph.add_conditional_edges(source: str, condition: Callable) | Route from source node based on condition function. Condition returns target node name. | StateGraph (for chaining) |
graph.compile() | Convert graph to executable runnable. Must call before invoke/stream. | CompiledGraph (Runnable) |
compiled.invoke(input: dict, config: dict = {}) | Run graph to completion, return final state. config includes thread_id for persistence. | dict (final state) |
compiled.stream(input: dict, config: dict = {}) | Stream node outputs as they execute. Yields (node_name, state) tuples. | Iterator[(str, dict)] |
create_react_agent(llm, tools, **kwargs) | Prebuilt ReAct agent: LLM → tool loop → done. Use for quick prototypes. | CompiledGraph |
Interrupt(value: dict) | Raise in a node to pause execution. Resume requires checkpointing. | Exception (raise, don't return) |
Common Configuration Parameters
StateGraph & Compilation
| Parameter | Type | Default | Purpose |
|---|---|---|---|
state_schema | TypedDict class | required | Defines state shape and types. Passed to StateGraph(). Example: AgentState(TypedDict) with fields |
checkpointer | BaseCheckpointStorage | None (no persistence) | Enables state persistence across invocations. Use MemorySaver() for dev, PostgresSaver() for prod. Required for interrupts. |
Common Errors & Fixes
ValueError: Unknown node '<name>' Cause: Conditional edge router returned a node name that doesn't exist in graph.
Check your routing function return value matches a defined node name exactly. Example:
def router(state):
return "node_a" # Must match graph.add_node("node_a", ...)
compiled = graph.compile()
result = compiled.invoke({"field": "value"}) KeyError: 'field_name' in node function Cause: Node tries to access state field that doesn't exist or TypedDict schema is incorrect.
Ensure all node functions return complete state dict. Use {**state, "new_field": value} to merge updates:
def node(state: AgentState) -> AgentState:
return {**state, "counter": state.get("counter", 0) + 1}
# Or use Annotated reducer for aggregation:
from typing import Annotated
from operator import add
class State(TypedDict):
messages: Annotated[list, add] # Messages accumulate
def node(state) -> State:
return {"messages": [{"role": "user", "content": "Hi"}]} AttributeError: 'dict' object has no attribute 'content' Cause: MessageGraph expects LangChain Message objects, but received plain dicts.
Convert dicts to Message objects using LangChain schema:
from langchain.schema import HumanMessage, AIMessage
# Instead of:
messages = [{"role": "user", "content": "Hi"}]
# Use:
messages = [HumanMessage(content="Hi")]
# In invoke:
result = compiled.invoke({"messages": messages}) RuntimeError: Interrupt raised but no checkpointer configured Cause: Node raised Interrupt() but graph.compile() didn't include checkpointer.
Add checkpointer to compile() and configure thread_id in invoke config:
from langgraph.checkpoint.memory import MemorySaver
compiled = graph.compile(checkpointer=MemorySaver())
result = compiled.invoke(
{"messages": [], "approved": False},
{"configurable": {"thread_id": "thread_1"}}
)
# Later:
result = compiled.invoke(
{"messages": [], "approved": True},
{"configurable": {"thread_id": "thread_1"}}
) TypeError: add_edge() takes positional arguments but got 0 Cause: Calling add_edge() without required source and target arguments.
graph.add_edge() requires exactly source and target node names:
# Correct:
graph.add_edge("node_a", "node_b")
graph.add_edge(START, "node_a")
graph.add_edge("node_b", END)
# Wrong (will fail):
graph.add_edge() # Missing args Production Gotchas
If two nodes both update the same state field and you don't return the full state dict, the second update overwrites the first. Always use {**state, ...} or configure Annotated reducers (add, append, etc.) for fields that should accumulate.
Routing functions that return wrong types (True/False, integers, or typos in node names) cause silent failures or 'Unknown node' errors. Test routing paths explicitly: compile().invoke() and check all edge cases.
Calling Interrupt() without a checkpointer means you cannot resume the workflow. Always use MemorySaver() in dev and PostgresSaver/RedisCheckpointer in production when using interrupts or multi-turn workflows.
Passing raw Python dicts to MessageGraph causes serialization errors downstream. Convert inputs to HumanMessage/AIMessage objects, or use StateGraph with explicit TypedDict for full control.
When you iterate compiled.stream(config={"outputMode": "nodes"}), the final tuple is ('__end__', {}) or similar. Filter these in your code to avoid trying to process empty states.
create_react_agent() expects tools as langchain.tool decorated functions or Tool instances with structured signatures. Passing plain Python functions without @tool fails silently at tool invocation time.
Complete Multi-Turn Agentic Workflow with Interrupts & Persistence
from langgraph.graph import StateGraph, START, END
from langgraph.types import Interrupt
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Literal
from langchain_openai import ChatOpenAI
from langchain.tools import tool
import os
import json
# 1. Define state schema
class WorkflowState(TypedDict):
task: str
steps_completed: list
needs_approval: bool
approved: bool
result: str
# 2. Define tools
@tool
def analyze_task(task: str) -> str:
"""Analyze the task and propose steps."""
return f"Steps for '{task}': [Step 1, Step 2, Step 3]"
# 3. Define nodes
def planning_node(state: WorkflowState) -> WorkflowState:
"""Generate execution plan."""
state["steps_completed"].append("planning")
state["needs_approval"] = True
return state
def approval_node(state: WorkflowState) -> WorkflowState:
"""Request human approval."""
if not state["needs_approval"]:
return state
# Interrupt: pause and wait for human input
raise Interrupt({"action": "requires_approval", "step": "planning"})
def execution_node(state: WorkflowState) -> WorkflowState:
"""Execute if approved, otherwise reject."""
if not state["approved"]:
state["result"] = "Execution rejected by user"
state["steps_completed"].append("rejected")
return state
state["steps_completed"].append("execution")
state["result"] = f"Task '{state['task']}' completed successfully"
return state
def should_continue(state: WorkflowState) -> Literal["execution", END]:
"""Route based on approval status."""
return "execution" if state["needs_approval"] else END
# 4. Build graph
graph = StateGraph(WorkflowState)
graph.add_node("planning", planning_node)
graph.add_node("approval", approval_node)
graph.add_node("execution", execution_node)
graph.add_edge(START, "planning")
graph.add_edge("planning", "approval")
graph.add_conditional_edges("approval", should_continue)
graph.add_edge("execution", END)
# 5. Compile with persistence
compiled = graph.compile(checkpointer=MemorySaver())
# 6. First invocation (pauses at interrupt)
thread_id = "workflow_1"
initial_state = {
"task": "analyze_data",
"steps_completed": [],
"needs_approval": False,
"approved": False,
"result": ""
}
print("=== Starting workflow ===")
try:
result = compiled.invoke(
initial_state,
{"configurable": {"thread_id": thread_id}}
)
except Exception as e:
print(f"Workflow paused: {e}")
print(f"Steps completed so far: {initial_state['steps_completed']}")
# 7. Resume with human approval
print("\n=== Resuming with approval ===")
final_state = {
"task": "analyze_data",
"steps_completed": [],
"needs_approval": True,
"approved": True, # Human approves here
"result": ""
}
final = compiled.invoke(
final_state,
{"configurable": {"thread_id": thread_id}}
)
print(f"Final result: {final['result']}")
print(f"All steps: {final['steps_completed']}")