What is LangGraph?
LangGraph is a framework from the LangChain ecosystem designed to build stateful, multi-actor applications powered by large language models (LLMs). At its core, LangGraph models workflows as state machines—directed graphs where nodes represent computational steps (like LLM calls, tool executions, or human interventions) and edges represent transitions between steps, including conditional branching.
Unlike traditional linear chains of prompts, LangGraph allows developers to define complex, looping, and branching logic that can persist state across multiple interactions, making it ideal for agentic systems that need to remember context, recover from errors, and involve humans when necessary.
Why State Machines Matter for Complex Agent Workflows
When building AI agents that handle multi-step tasks—like customer support triage, research assistants, or automated DevOps pipelines—you quickly run into challenges that simple sequential chains cannot solve:
- Branching logic: The agent must decide between different paths based on user input or intermediate results.
- Loops and retries: Tool calls may fail; the agent should retry, fall back, or ask for clarification.
- Persistent memory: The workflow must remember what happened in previous steps, even across multiple turns of a conversation.
- Human-in-the-loop: Critical decisions require pausing execution, waiting for human approval, then resuming from exactly where it stopped.
- Observability and debuggability: Complex logic needs clear traces of state transitions and node outputs.
A state-machine approach models these requirements naturally. LangGraph gives you a structured way to define states, transitions, and conditions, while handling the plumbing of state persistence, streaming, and checkpointing. This enables you to build robust, production-grade agent workflows that can recover from unexpected paths and maintain integrity over long-running operations.
Core Concepts: State, Nodes, and Edges
Every LangGraph workflow revolves around three fundamental building blocks:
State
The state is a shared data structure that flows through the entire graph. It holds all information the agent needs to make decisions, track progress, and communicate results. LangGraph uses a typed dictionary (via Python’s TypedDict or Pydantic models) to define the schema, ensuring type safety and default values.
Nodes
Nodes are Python functions (synchronous or asynchronous) that receive the current state and return an updated state (or a partial update). Each node performs a discrete operation: calling an LLM, executing a tool, formatting a response, or logging data.
Edges
Edges connect nodes and define the flow. There are two kinds:
- Normal edges: unconditional transitions from one node to the next.
- Conditional edges: route to different target nodes based on a function that reads the current state and returns the next node’s name.
These three elements combine into a StateGraph, which you compile into an executable application with built-in support for streaming, checkpointing, and thread-level persistence.
Building Your First LangGraph Agent
Let’s build a minimal customer support agent that classifies a user’s intent and routes accordingly. We’ll use the langgraph library along with LangChain for the LLM interaction.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
# 1. Define the shared state
class AgentState(TypedDict):
user_input: str
intent: str
response: str
# 2. Initialize the LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 3. Node: classify intent
def classify_intent(state: AgentState) -> dict:
prompt = f"Classify the intent of this message: '{state['user_input']}'. Choose from 'billing', 'technical', 'general'."
result = llm.invoke(prompt)
intent = result.content.strip().lower()
return {"intent": intent}
# 4. Nodes for each handler
def handle_billing(state: AgentState) -> dict:
# In reality you'd query billing system, etc.
return {"response": "I'll help with your billing inquiry. Please provide your account ID."}
def handle_technical(state: AgentState) -> dict:
return {"response": "Let me troubleshoot this technical issue. What error are you seeing?"}
def handle_general(state: AgentState) -> dict:
return {"response": "Thanks for your message! How can I assist you today?"}
# 5. Router function for conditional edges
def route_by_intent(state: AgentState) -> Literal["billing", "technical", "general"]:
intent = state.get("intent", "general")
if intent == "billing":
return "billing"
elif intent == "technical":
return "technical"
else:
return "general"
# 6. Build the graph
builder = StateGraph(AgentState)
# Add nodes
builder.add_node("classify_intent", classify_intent)
builder.add_node("handle_billing", handle_billing)
builder.add_node("handle_technical", handle_technical)
builder.add_node("handle_general", handle_general)
# Add edges
builder.set_entry_point("classify_intent") # start here
builder.add_conditional_edges(
"classify_intent",
route_by_intent,
{
"billing": "handle_billing",
"technical": "handle_technical",
"general": "handle_general"
}
)
# After handling, go to END
builder.add_edge("handle_billing", END)
builder.add_edge("handle_technical", END)
builder.add_edge("handle_general", END)
# 7. Compile and run
graph = builder.compile()
# Invoke with initial state
initial_state = {"user_input": "I have a problem with my internet connection", "intent": "", "response": ""}
final_state = graph.invoke(initial_state)
print(final_state["response"]) # Output from the appropriate handler
In this example, the state flows through classify_intent (which sets intent), then a conditional edge sends it to one of three handlers, and finally the graph ends. LangGraph automatically manages the state merging: each node returns only the fields it updates, and the graph merges them into the full state.
Adding Conditional Logic and Loops
Real agent workflows often need to loop—for example, retrying a tool call if it fails, or iterating over multiple steps until a task is complete. LangGraph supports loops naturally by allowing edges to point back to earlier nodes.
Let’s extend the previous example: after handling a technical issue, we’ll ask an LLM if the problem is resolved or needs escalation. If not resolved, we loop back to a troubleshooting node; otherwise we end.
# Additional state fields
class AgentState(TypedDict):
user_input: str
intent: str
response: str
resolution_status: str # "resolved", "unresolved"
def initial_troubleshoot(state: AgentState) -> dict:
# Simulate a troubleshooting step
return {"response": "Have you tried restarting your router?", "resolution_status": "unresolved"}
def check_resolution(state: AgentState) -> dict:
prompt = f"The user reported: '{state['user_input']}'. Our last suggestion: '{state['response']}'. Is the issue resolved? Reply with 'resolved' or 'unresolved'."
result = llm.invoke(prompt)
status = result.content.strip().lower()
return {"resolution_status": status}
def escalate_to_human(state: AgentState) -> dict:
return {"response": "Transferring to a human agent. One moment please."}
def route_after_check(state: AgentState) -> Literal["troubleshoot", "escalate", "end"]:
status = state.get("resolution_status")
if status == "resolved":
return "end"
elif status == "unresolved":
# In a real app you'd limit retries; we'll do that next
return "troubleshoot"
else:
return "escalate"
builder = StateGraph(AgentState)
builder.add_node("troubleshoot", initial_troubleshoot)
builder.add_node("check_resolution", check_resolution)
builder.add_node("escalate", escalate_to_human)
builder.set_entry_point("troubleshoot")
builder.add_edge("troubleshoot", "check_resolution")
builder.add_conditional_edges(
"check_resolution",
route_after_check,
{
"troubleshoot": "troubleshoot", # loop back
"escalate": "escalate",
"end": END
}
)
builder.add_edge("escalate", END)
graph = builder.compile()
# Run the loop with an initial user problem
initial = {"user_input": "Internet drops every 10 minutes", "intent": "", "response": "", "resolution_status": ""}
result = graph.invoke(initial)
print(result["response"])
However, an unconditional loop can cause infinite execution. LangGraph provides recursion limits (configurable via graph.invoke with config={"recursion_limit": 5}). Even better, we can bake a counter into the state and use it in the conditional edge.
class AgentState(TypedDict):
user_input: str
intent: str
response: str
resolution_status: str
retry_count: int
def initial_troubleshoot(state: AgentState) -> dict:
count = state.get("retry_count", 0) + 1
return {"response": f"Troubleshooting step {count}: please check cables.", "retry_count": count}
def route_after_check(state: AgentState) -> Literal["troubleshoot", "escalate", "end"]:
if state.get("retry_count", 0) >= 3:
return "escalate"
status = state.get("resolution_status", "unresolved")
return "troubleshoot" if status == "unresolved" else "end"
This demonstrates how state machines elegantly handle complex retry logic, keeping the workflow readable and maintainable.
Integrating Tools and Human-in-the-Loop
Agents often need to call external APIs, databases, or other tools, and sometimes pause for human approval. LangGraph’s architecture makes these patterns straightforward.
Tool-calling nodes
You can wrap any Python function that executes a tool and update the state with results. Here’s a node that fetches account details from a mock database:
def fetch_account(state: AgentState) -> dict:
account_id = extract_account_id(state["user_input"]) # custom extraction logic
# Simulate DB call
account_info = {"balance": 123.45, "status": "active"}
return {"account_info": account_info}
LangChain’s tool abstraction (with @tool decorators) integrates naturally: you can call them inside nodes and store the output in state.
Human-in-the-loop
To pause execution and wait for human input, LangGraph supports interrupts. You can mark a node as interruptible and later resume the graph with the human’s feedback. The state is persisted between the pause and resume, enabling long-running workflows.
# In the graph definition, add an interrupt before a sensitive node
builder.add_node("approve_refund", approve_refund, interrupt=True)
# When invoking, you can handle the interrupt
graph = builder.compile(checkpointer=memory_saver)
thread = {"configurable": {"thread_id": "conversation-42"}}
# First call: runs until the interrupt node
graph.invoke({"user_input": "I want a refund for order #1234"}, thread)
# Execution pauses, you can inspect state, and then resume with human decision
graph.invoke(None, thread, interrupt_input={"approved": True})
This pattern is invaluable for compliance-sensitive operations (refunds, account deletions, content publishing) where a human must be in the decision loop.
Best Practices for Complex Workflows
Building maintainable, production-grade agent workflows with LangGraph requires thoughtful design. Here are proven best practices:
- Keep state minimal and explicit: Only store what’s truly needed across nodes. Avoid bloating the state with ephemeral computation; instead derive it inside nodes or use separate caches.
- Design idempotent nodes: Each node should be safe to retry—avoid side effects that aren’t tracked in state. If a node sends an email, record a “email_sent” flag in state so retries don’t duplicate.
- Use typed state schemas: Leverage
TypedDictor Pydantic models for state. This catches shape errors early, provides editor autocompletion, and acts as living documentation. - Log and visualize: LangGraph provides built-in tracing. Use
graph.get_graph().draw_ascii()or the LangSmith integration to visualize the graph. This is crucial for debugging complex branching. - Handle errors gracefully: Wrap node logic in try/except blocks and update state with an error field. Add a conditional edge that routes to an error handler or a human escalation node when errors occur.
- Limit recursion and infinite loops: Always include a counter or a maximum step guard in conditional edges that can loop. Use
recursion_limitas a safety net. - Persist state across sessions: Use the built-in checkpointers (like
MemorySaverfor testing, or a DB-backed one likeSqliteSaverorPostgresSaverfor production) to survive restarts and enable thread-level history. - Test each node in isolation: Write unit tests for node functions using fake state inputs, and integration tests for the full graph with different branching scenarios.
- Version your graphs: Treat graph definitions as code; use version control and CI/CD. Changes to state shape or node logic should be reviewed like any other service change.
Conclusion
LangGraph brings the power of state machines to LLM-powered agent workflows, enabling you to move beyond simple chains into robust, looping, branching, and interruptible applications. By explicitly modeling state, nodes, and edges, you gain clarity, testability, and the ability to handle the messy realities of production-grade AI—retries, human approvals, persistent memory, and error recovery. The framework’s seamless integration with LangChain’s LLM and tool ecosystem, along with built-in checkpointing and streaming, makes it an excellent choice for building complex agents that are both powerful and predictable. Start with small, focused graphs, iterate based on observed behavior, and apply the best practices above to scale your agent workflows with confidence.