LangGraph
LangGraph stateful agents, graph nodes, edges, checkpointing, and multi-agent workflows.
Core Graph Setup
2 topics
Define Typed State and Build a Minimal Graph
Create a strongly-typed state object and a small graph with START/END nodes for predictable workflow execution.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class AppState(TypedDict):
user_input: str
response: str
def generate_response(state: AppState) -> AppState:
# In production, call your LLM here
return {**state, "response": f"Echo: {state['user_input']}"}
graph_builder = StateGraph(AppState)
graph_builder.add_node("generate", generate_response)
graph_builder.add_edge(START, "generate")
graph_builder.add_edge("generate", END)
graph = graph_builder.compile()
result = graph.invoke({"user_input": "Hello LangGraph", "response": ""})
print(result)
# {'user_input': 'Hello LangGraph', 'response': 'Echo: Hello LangGraph'}Keep state fields minimal and explicit to avoid brittle node contracts.
Prefer pure node functions (input state -> output delta/state) for easier testing.
Use Message-Oriented State for Chat Apps
Represent chat history in state so each node can add messages while preserving conversation context.
from typing import TypedDict, List
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, START, END
class ChatState(TypedDict):
messages: List[BaseMessage]
def assistant_node(state: ChatState) -> ChatState:
last_user = state["messages"][-1].content
reply = AIMessage(content=f"I received: {last_user}")
return {"messages": state["messages"] + [reply]}
builder = StateGraph(ChatState)
builder.add_node("assistant", assistant_node)
builder.add_edge(START, "assistant")
builder.add_edge("assistant", END)
app = builder.compile()
initial = {"messages": [HumanMessage(content="Summarize quarterly revenue trends")]}
out = app.invoke(initial)
print(out["messages"][-1].content)Store full message objects (not only strings) to support tool calls and metadata later.
Append to history rather than overwrite for traceability.
Building a Graph
1 topic
StateGraph Basics
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
# 1. Define state
class AgentState(TypedDict):
messages: list
next: str
# 2. Define nodes (functions)
def call_llm(state: AgentState) -> AgentState:
response = llm.invoke(state["messages"])
return {"messages": state["messages"] + [response]}
def should_continue(state: AgentState) -> str:
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tools"
return END
# 3. Build graph
graph = StateGraph(AgentState)
graph.add_node("agent", call_llm)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
app = graph.compile()
# 4. Run
result = app.invoke({"messages": [("human", "Hello!")]})💡 State is immutable — always return the full updated state from nodes
⚡ Conditional edges based on state make complex branching logic explicit
Routing and Control Flow
2 topics
Conditional Routing by Intent
Route requests to different nodes (e.g., FAQ vs escalation) based on classifier output in state.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class TicketState(TypedDict):
text: str
intent: Literal["faq", "escalate"]
output: str
def classify_intent(state: TicketState):
t = state["text"].lower()
intent = "escalate" if "refund" in t or "angry" in t else "faq"
return {**state, "intent": intent}
def faq_node(state: TicketState):
return {**state, "output": "Here are help center steps for your issue."}
def escalate_node(state: TicketState):
return {**state, "output": "I've escalated this to a human support agent."}
def route_by_intent(state: TicketState):
return state["intent"]
builder = StateGraph(TicketState)
builder.add_node("classify", classify_intent)
builder.add_node("faq", faq_node)
builder.add_node("escalate", escalate_node)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent, {
"faq": "faq",
"escalate": "escalate"
})
builder.add_edge("faq", END)
builder.add_edge("escalate", END)
app = builder.compile()
print(app.invoke({"text": "I want a refund now", "intent": "faq", "output": ""}))Return deterministic route keys from routing functions.
Log route decisions for debugging and analytics.
Loop Until Quality Threshold Is Met
Create iterative refinement loops where a draft is reviewed and regenerated until acceptable quality is reached.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class DraftState(TypedDict):
topic: str
draft: str
score: int
max_iters: int
iters: int
def write_draft(state: DraftState):
new_text = f"Draft v{state['iters'] + 1} on {state['topic']}"
return {**state, "draft": new_text, "iters": state["iters"] + 1}
def review_draft(state: DraftState):
# Placeholder scoring logic
score = min(10, len(state["draft"]) // 4)
return {**state, "score": score}
def should_continue(state: DraftState):
if state["score"] >= 8 or state["iters"] >= state["max_iters"]:
return "end"
return "rewrite"
builder = StateGraph(DraftState)
builder.add_node("write", write_draft)
builder.add_node("review", review_draft)
builder.add_edge(START, "write")
builder.add_edge("write", "review")
builder.add_conditional_edges("review", should_continue, {
"rewrite": "write",
"end": END
})
app = builder.compile()
print(app.invoke({"topic": "SOC 2 controls", "draft": "", "score": 0, "max_iters": 3, "iters": 0}))Always enforce loop limits to prevent runaway costs.
Track iteration count and quality score in state for observability.
Checkpointing & Memory
1 topic
Persist Agent State
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.postgres import PostgresSaver
# In-memory (dev/testing)
memory = MemorySaver()
app = graph.compile(checkpointer=memory)
# PostgreSQL (production)
with PostgresSaver.from_conn_string(DATABASE_URL) as saver:
app = graph.compile(checkpointer=saver)
# Use thread_id for conversation memory
config = {"configurable": {"thread_id": "user-123"}}
# First message
app.invoke({"messages": [("human", "My name is Alice")]}, config=config)
# Second message — remembers context
app.invoke({"messages": [("human", "What is my name?")]}, config=config)
# Output: "Your name is Alice"
# Get state history
for state in app.get_state_history(config):
print(state)💡 thread_id scopes memory to a conversation — use user ID + session ID in production
⚡ Checkpointing enables time-travel debugging — replay from any historical state
Tools and External Systems
2 topics
Call Business APIs from a Node
Use nodes to fetch live data (inventory, CRM, billing) and enrich downstream LLM responses.
from typing import TypedDict, Optional
import requests
from langgraph.graph import StateGraph, START, END
class InventoryState(TypedDict):
sku: str
inventory: Optional[int]
answer: str
def fetch_inventory(state: InventoryState):
# Example only: replace with your real API endpoint/auth
# resp = requests.get(f"https://api.example.com/inventory/{state['sku']}", timeout=10)
# qty = resp.json()["available"]
qty = 42
return {**state, "inventory": qty}
def respond(state: InventoryState):
return {**state, "answer": f"SKU {state['sku']} has {state['inventory']} units available."}
builder = StateGraph(InventoryState)
builder.add_node("fetch_inventory", fetch_inventory)
builder.add_node("respond", respond)
builder.add_edge(START, "fetch_inventory")
builder.add_edge("fetch_inventory", "respond")
builder.add_edge("respond", END)
app = builder.compile()
print(app.invoke({"sku": "LAPTOP-15", "inventory": None, "answer": ""})["answer"])Use request timeouts and retries for external services.
Store normalized API outputs in state so later nodes remain provider-agnostic.
Add Tool Fallback Logic
Handle tool/API failures gracefully by routing to fallback nodes instead of crashing the workflow.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class ToolState(TypedDict):
query: str
tool_ok: bool
result: str
def run_tool(state: ToolState):
try:
# Simulate flaky tool
raise RuntimeError("Tool temporarily unavailable")
except Exception:
return {**state, "tool_ok": False, "result": ""}
def fallback_llm(state: ToolState):
return {**state, "result": f"Fallback answer for: {state['query']}"}
def finalize_tool(state: ToolState):
return {**state, "result": "Tool answer: 2024 revenue was $12.4M"}
def route_after_tool(state: ToolState):
return "ok" if state["tool_ok"] else "fallback"
builder = StateGraph(ToolState)
builder.add_node("run_tool", run_tool)
builder.add_node("fallback", fallback_llm)
builder.add_node("finalize", finalize_tool)
builder.add_edge(START, "run_tool")
builder.add_conditional_edges("run_tool", route_after_tool, {
"ok": "finalize",
"fallback": "fallback"
})
builder.add_edge("fallback", END)
builder.add_edge("finalize", END)
app = builder.compile()
print(app.invoke({"query": "What is FY revenue?", "tool_ok": True, "result": ""})["result"])Capture error type/message in state for postmortems.
Prefer degraded but useful responses over hard failures.
Persistence, Streaming, and Debugging
2 topics
Persist Thread State with a Checkpointer
Enable resumable, multi-turn workflows by storing graph state keyed by thread/session ID.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
class SessionState(TypedDict):
user_id: str
messages: list[str]
def append_message(state: SessionState):
return {**state, "messages": state["messages"] + ["assistant: got it"]}
builder = StateGraph(SessionState)
builder.add_node("append", append_message)
builder.add_edge(START, "append")
builder.add_edge("append", END)
checkpointer = MemorySaver()
app = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "customer-123"}}
app.invoke({"user_id": "u1", "messages": ["user: hello"]}, config=config)
# Later turn can continue same thread_idUse durable backing stores in production (not in-memory) for restart safety.
Thread IDs should map cleanly to conversation/session identity.
Stream Node Progress for Real-Time UX
Stream intermediate events to power responsive interfaces that show users what the agent is doing.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class StreamState(TypedDict):
task: str
status: str
def plan(state: StreamState):
return {**state, "status": "planned"}
def execute(state: StreamState):
return {**state, "status": "executed"}
builder = StateGraph(StreamState)
builder.add_node("plan", plan)
builder.add_node("execute", execute)
builder.add_edge(START, "plan")
builder.add_edge("plan", "execute")
builder.add_edge("execute", END)
app = builder.compile()
for event in app.stream({"task": "generate report", "status": "new"}):
print(event)
# Useful for websocket/SSE updates in web appsMap streamed events to frontend step indicators.
Avoid leaking sensitive state fields in client-visible streams.
Related Cheat Sheets
More hands-on references connected to this topic.
Complete guide to building AI agents — architecture, tool use, memory, planning patterns, multi-agent systems, frameworks, evaluation, and production best practices.
shared topics, same difficulty
AI workflow cheat sheet for building software with AI — planning, incremental development, testing strategies, and codebase management tips.
shared topics
Practical Generative AI playbook for building reliable LLM features: prompting, context design, RAG, evaluation, safety, and production operations.
shared topics
Essential Git commands for version control, branching, merging, and collaboration workflows.
shared topics
Related Articles
Background reading and deeper explanations for this sheet.
Build an Agentic App with FastAPI and Azure AI Foundry: A Practical Beginner-to-Pro Guide
Learn how to design, build, and deploy an agentic application using FastAPI and Azure AI Foundry with practical, real-world examples. This guide walks you from architecture and setup to tool-calling, memory, observability, and production deployment patterns.
keyword overlap
Building Autonomous Agents Without Overengineering: A Practical Guide for Real-World Teams
Autonomous agents can automate meaningful work, but many projects fail because teams overbuild too early. This practical guide shows you how to design, ship, and improve useful agents with simple patterns, clear guardrails, and real-world examples—without turning your stack into a science project.
keyword overlap
Cost Optimization in Agentic Systems: The Most Underrated Lever for Reliable AI at Scale
Most teams building agentic systems obsess over accuracy and latency, but ignore the fastest path to sustainable scale: cost optimization. In this guide, you’ll learn practical cost models, real architecture patterns, and implementation tactics to cut spend without sacrificing quality or autonomy.
keyword overlap
Event-Driven Agents with Kafka Streams: A Practical Guide for Building Real-Time AI Workflows
Event-driven agents let you move from slow, request/response automation to responsive systems that react in milliseconds to real-world signals. In this hands-on guide, you’ll learn how to design, build, and operate agentic workflows using Apache Kafka and Kafka Streams, with practical patterns, code snippets, and production-ready advice.
keyword overlap