---
title: "LangGraph tutorial for beginners: build your first workflow"
canonical: "https://agenticup.dev/posts/langgraph-tutorial-beginners/"
pubDate: "2026-06-01T00:00:00.000Z"
description: "LangGraph keeps getting recommended but every tutorial assumes you already know LangChain. Here's a beginner-friendly walkthrough — from state graphs to a working agent — without the chain-of-thought abstraction."
tags: [langgraph, tutorial, beginner guide, agent workflow, langchain]
---

The [LangGraph documentation](https://langchain-ai.github.io/langgraph/) 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.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "LangGraph tutorial for beginners: build your first agent workflow",
  "description": "A hands-on tutorial for building your first LangGraph agent — state graphs, nodes, edges, and how to deploy it.",
  "step": [
    {"@type": "HowToStep", "position": 1, "name": "Understand graphs vs chains", "text": "Learn why LangGraph uses directed graphs instead of linear chains for agent workflows."},
    {"@type": "HowToStep", "position": 2, "name": "Define your state schema", "text": "Create a TypedDict or dataclass that defines what data flows through your graph."},
    {"@type": "HowToStep", "position": 3, "name": "Add nodes", "text": "Add function nodes that process state — one for LLM calls, one for tool execution."},
    {"@type": "HowToStep", "position": 4, "name": "Add edges and conditional routing", "text": "Connect nodes with edges and add conditional routing to handle branching logic."},
    {"@type": "HowToStep", "position": 5, "name": "Add human-in-the-loop", "text": "Add interrupt nodes that pause execution for user approval before continuing."},
    {"@type": "HowToStep", "position": 6, "name": "Compile and run", "text": "Compile the graph and run it with your input state."}
  ]
}
</script>

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

```bash
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:

```python
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:

```python
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:

```python
# 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

```python
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:

```python
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:

```python
# 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:

```python
# 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:

```python
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:

```python
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](/posts/best-ai-agent-frameworks-2026/) — how LangGraph compares to CrewAI, AutoGen, and custom builds.*

*Also: [How to build your first AI agent from scratch](/posts/how-to-build-first-ai-agent-2026/) — the fundamentals before frameworks.*

*Related: [Building an AI code review agent: lessons from production](/posts/building-ai-code-review-agent/) — how to build a production code review agent with LangGraph, including architecture and failure modes.*

<div class="callout">
  <div class="callout-title">Start small</div>
  <p>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.</p>
</div>
