LangGraph tutorial for beginners: build your first workflow
A hands-on tutorial for building your first LangGraph agent — state graphs, nodes, edges, and how to deploy it. No prior LangChain experience required.
The LangGraph documentation defines state graphs as the core abstraction — nodes represent LLM calls or tool executions, edges define control flow. This architecture enables both simple sequences and complex agent loops.
TL;DR: LangGraph uses directed graphs (nodes + edges) instead of linear chains, making it ideal for agent workflows with branching and cycles. This tutorial builds a customer query agent with conditional routing and human-in-the-loop — all from scratch without LangChain experience.
LangGraph keeps getting recommended everywhere. Every AI engineer I follow on X talks about it. But every tutorial I found assumed I already knew LangChain.
I didn’t. And you probably don’t need to either.
Here’s the thing about LangGraph: its core idea — state graphs — is simpler than LangChain’s chain-of-thought abstraction. A graph with nodes and edges is easier to reason about than a pipeline that chains abstractions on top of abstractions.
This tutorial walks through building a LangGraph agent from scratch. No LangChain experience required. We’ll build a customer query agent that classifies intent, retrieves information, and formulates a response — with conditional branching and human-in-the-loop.
Key takeaways:
- LangGraph uses directed graphs (nodes + edges), not linear chains — better for agent workflows with branching and cycles
- The state schema is the contract for your graph — define it carefully
- Conditional routing lets your agent make decisions about which path to follow
- Human-in-the-loop is a first-class concept, not an afterthought
What is LangGraph?
LangGraph is a framework for building stateful, multi-step agent workflows using directed graphs.
Think of it as a state machine for LLMs. You define:
- State — the data that flows through your workflow (messages, tool results, decisions)
- Nodes — functions that process the state (LLM calls, tool execution, data transformation)
- Edges — connections between nodes that define the flow
- Conditional edges — functions that decide which node to go to next based on state
The key insight: agent workflows aren’t linear. An LLM calls a tool, gets a result, decides what to do next, possibly calls another tool, possibly returns a final answer. You can model this as a chain, but a graph is more natural.
Why graphs over chains?
Before LangGraph, most agent frameworks used chains — linear sequences of operations. A chain looks like this:
User Input → Classify Intent → Retrieve Info → Generate Response
This works for simple flows. But agents need branching:
User Input → Classify Intent → Needs more info? → Retrieve Info → Generate Response
→ Simple question? → Generate Response
→ Escalate? → Transfer to Human
A chain handles this poorly — you end up with nested conditionals, complex routing logic, and a system that’s hard to debug. A graph handles it naturally — each branch is just a different path through the nodes.
Building your first state graph
Let’s build a customer query agent step by step.
Step 1: Install LangGraph
pip install langgraph langchain-anthropic
That’s it. Two dependencies. You don’t need the full LangChain suite.
Step 2: Define your state schema
The state is a typed dictionary that defines what flows through your graph:
from typing import TypedDict, List, Optional, Literal
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
messages: List[dict] # The conversation so far
intent: Optional[str] # Classified intent: "billing", "technical", "general"
retrieved_info: Optional[str] # Retrieved information from knowledge base
needs_escalation: bool # Whether to escalate to a human
final_response: Optional[str] # The final response to the user
This is the contract for your graph. Every node reads from and writes to this state.
Step 3: Add nodes
Nodes are functions that take the state and return updates:
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
def classify_intent(state: AgentState) -> dict:
"""Classify the user's intent based on their message."""
response = llm.invoke([
{"role": "system", "content": "Classify the intent as: billing, technical, or general. Return just the label."},
{"role": "user", "content": state["messages"][-1]["content"]}
])
return {"intent": response.content.strip().lower()}
def retrieve_information(state: AgentState) -> dict:
"""Retrieve information from the knowledge base based on intent."""
# Simple mock retrieval — in production, use vector search
knowledge_base = {
"billing": "Our billing cycle is monthly. Payments are processed on the 1st.",
"technical": "API documentation is at docs.example.com. Rate limit: 100/min.",
"general": "We're available 24/7 via email or chat. Typical response time: 2hrs."
}
info = knowledge_base.get(state["intent"], "No specific information found.")
return {"retrieved_info": info}
def generate_response(state: AgentState) -> dict:
"""Generate a final response based on intent and retrieved info."""
response = llm.invoke([
{"role": "system", "content": f"You are a support agent. Use this info: {state['retrieved_info']}"},
{"role": "user", "content": state["messages"][-1]["content"]}
])
return {"final_response": response.content}
Each node returns a dictionary of state updates. LangGraph merges these into the main state.
Step 4: Define edges
Now connect the nodes. In LangGraph, you add nodes, then add edges between them:
# Create the graph
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("classify", classify_intent)
workflow.add_node("retrieve", retrieve_information)
workflow.add_node("generate", generate_response)
# Add edges
workflow.set_entry_point("classify")
workflow.add_edge("classify", "retrieve")
workflow.add_edge("retrieve", "generate")
workflow.add_edge("generate", END)
This gives you a linear graph: classify → retrieve → generate → end.
Step 5: Compile and run
app = workflow.compile()
result = app.invoke({
"messages": [{"role": "user", "content": "My API key stopped working suddenly."}],
"intent": None,
"retrieved_info": None,
"needs_escalation": False,
"final_response": None
})
print(result["final_response"])
When you run this, the graph executes: classify intent → retrieve info → generate response. Each node updates the state, and the final state contains the result.
Adding conditional branching
Linear graphs are fine, but the real power is conditional edges — edges that decide which node to go to next based on the state.
Let’s add escalation logic:
def should_escalate(state: AgentState) -> Literal["escalate", "retrieve"]:
"""Decide whether to escalate or retrieve info."""
# Escalate if the user sounds frustrated or the issue is critical
response = llm.invoke([
{"role": "system", "content": "Does this message require escalation? Keywords: 'frustrated', 'cancel', 'refund', 'manager'. Answer 'yes' or 'no'."},
{"role": "user", "content": state["messages"][-1]["content"]}
])
if "yes" in response.content.lower():
return "escalate"
return "retrieve"
def escalate_to_human(state: AgentState) -> dict:
"""Transfer the conversation to a human agent."""
return {
"final_response": "I understand you need additional help. I'm transferring you to a human agent who can resolve this.",
"needs_escalation": True
}
# Add the escalation node
workflow.add_node("escalate", escalate_to_human)
# Replace the simple edge with a conditional edge
workflow.add_conditional_edges(
"classify",
should_escalate,
{"escalate": "escalate", "retrieve": "retrieve"}
)
workflow.add_edge("escalate", END)
Now the graph decides: after classification, should it escalate or retrieve? The conditional function should_escalate examines the state and returns the next node name.
Adding human-in-the-loop
One of LangGraph’s best features is first-class human-in-the-loop support. You can mark a node as interrupt_before, which pauses execution before that node runs:
# Compile with interrupt before the escalate node
app = workflow.compile(interrupt_before=["escalate"])
When the graph reaches the escalate node, it pauses and returns control:
# Run the graph
thread = {"configurable": {"thread_id": "1"}}
# First run — might hit the interrupt
result = app.invoke({
"messages": [{"role": "user", "content": "I want a refund. This is ridiculous."}],
# ... other initial state
}, thread)
# Check if interrupted
if app.get_state(thread).next:
# Prompt the human agent for approval
print(f"Awaiting human approval for: {result.get('intent')}")
approval = input("Approve escalation? (y/n): ")
if approval.lower() == "y":
# Resume the graph
result = app.invoke(None, thread)
else:
# Update state and continue without escalation
app.update_state(thread, {"intent": "general"})
result = app.invoke(None, thread)
This pattern is powerful. Your agent runs autonomously for standard flows, pauses for approval on sensitive actions, and continues once the human approves or modifies the state.
Adding tools to LangGraph
Let’s add real tool execution. Here’s a weather agent with a search tool:
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
@tool
def get_weather(location: str) -> str:
"""Get the current weather for a location."""
# In production, call a weather API
return f"25°C, partly cloudy in {location}"
@tool
def search_docs(query: str) -> str:
"""Search the documentation for information."""
# In production, use vector search
return f"Found relevant docs for: {query}"
# Add tools to the LLM
llm_with_tools = llm.bind_tools([get_weather, search_docs])
def agent_node(state: AgentState) -> dict:
"""The main agent node — LLM decides to use tools or respond."""
response = llm_with_tools.invoke(state["messages"])
return {"messages": state["messages"] + [response]}
# Tool node executes any tool calls the LLM generated
tool_node = ToolNode([get_weather, search_docs])
# Define routing: after agent, check if tools were called
def should_continue(state: AgentState) -> Literal["tools", "generate"]:
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return "generate"
# Build the graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
workflow.add_node("generate", generate_response)
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {
"tools": "tools",
"generate": "generate"
})
workflow.add_edge("tools", "agent") # Loop back to agent after tools
workflow.add_edge("generate", END)
The graph now loops: agent calls LLM → if tools are called, execute them and loop back → if no tools called, generate final response.
This is the pattern for most production agents — an agent node that can use tools, a tool execution node, and conditional routing that loops until the task is complete.
Deploying the graph
LangGraph compiles to a callable, so you can deploy it with any web framework:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
graph = workflow.compile()
class Query(BaseModel):
message: str
thread_id: str
@app.post("/agent")
def run_agent(query: Query):
result = graph.invoke(
{"messages": [{"role": "user", "content": query.message}]},
{"configurable": {"thread_id": query.thread_id}}
)
return {"response": result.get("final_response")}
Deploy this on Railway, Fly.io, or any cloud provider. Cost: about ₹500–₹1000/month for a low-traffic agent.
LangGraph vs plain LangChain
If you’ve used LangChain, here’s the key difference: LangChain forces a linear pipeline. You use Chain objects that pass through a sequence of operations. Adding branching means nested chains or custom RunnableLambda logic.
LangGraph lets you express the workflow as a graph. Branching is just a conditional edge. Loops are just edges back to a previous node. State is explicit — you define exactly what flows through the graph.
The result: LangGraph agents are easier to debug. When something goes wrong, you inspect the state at each node. You can see exactly what happened, what the LLM returned, and which branch the graph took.
Related: Best AI agent frameworks for 2026 — how LangGraph compares to CrewAI, AutoGen, and custom builds.
Also: How to build your first AI agent from scratch — the fundamentals before frameworks.
Related: Building an AI code review agent: lessons from production — how to build a production code review agent with LangGraph, including architecture and failure modes.
Don't build a complex graph on your first try. Start with 3 nodes and a single conditional edge. Get that working. Then add the human-in-the-loop. Then add tools. Each layer adds complexity — make sure the base is solid before you stack more on top.