Chapter 211 min read

Chapter 2: The Three Pillars of LangGraph

The biggest mistake developers make when learning LangGraph is rushing straight into the API to build a multi-agent system. They copy-paste complex code with StateGraph, add_conditional_edges, and MemorySaver, run it, and immediately become confused when the graph gets stuck in an infinite loop.

Before we write complex AI systems, we must build a rock-solid mental model.

In this chapter, we are going to strip away the AI, strip away the advanced APIs, and focus entirely on the core paradigm: State → Node → Edge.

By the end of this chapter, you will understand exactly how LangGraph executes code, why it radically simplifies application complexity, and you will build your first functioning graph.


2.1 Thinking Like a State Machine

To understand LangGraph, start by looking at a traffic light.

  Red
   ↓
 Green
   ↓
 Yellow
   ↓
  Red

A traffic light is not an "Agent". It is a strict State Machine. It has three core components:

  1. State: The current status (e.g., {"current_color": "Red"}).
  2. Rules (Nodes): The physical lightbulb turning on or off.
  3. Transitions (Edges): The logic that says "If Red has been on for 60 seconds, transition to Green."

LangGraph works exactly the same way. It is a framework for defining States, Rules, and Transitions. The execution loop of LangGraph is always:

    Current State
          ↓
      Execute Node
          ↓
  Return State Update
          ↓
    Evaluate Edge
          ↓
      Next Node

2.2 The Execution Model of LangGraph

This brings us to a critical shift in how you must write code.

In traditional Python programming, functions call other functions directly:

def node_a(data):
    # Node A physically calls Node B
    result = node_b(data) 
    return result

In LangGraph, Nodes never call each other.

Nodes are completely isolated workers. They do not know what happened before them, and they do not know what will happen after them.

Execution actually looks like this:

                ┌──────────────────┐
                │ Graph Runtime    │
                └────────┬─────────┘
                         │
                         ▼
                   Current State
                         │
                         ▼
                      Node A
                         │
                         ▼
                   State Update
                         │
                         ▼
                 Conditional Edge
                         │
                         ▼
                      Node B

The runtime—not the nodes—controls execution.

  1. The Graph Runtime passes the State to Node A.
  2. Node A does some work, and returns an updated State to the Runtime.
  3. Node A goes to sleep.
  4. The Graph Runtime looks at the Edge rules.
  5. The Graph Runtime passes the updated State to Node B.

Because Nodes don't call each other directly, the runtime can pause execution, save the state to a database, and resume days later.


2.3 Understanding State

State is simply a shared memory object. In Python, it is typically represented as a TypedDict.

from typing import TypedDict

# This is the "blueprint" of our shared memory.
class ChatState(TypedDict):
    user_message: str
    intent: str
    response: str

Every node in your graph does exactly three things:

  1. Reads the incoming State.
  2. Performs work (like calling an LLM or a database).
  3. Returns a Python dictionary containing updates to the State.

Nothing more.


2.4 Why State Solves Complexity

Why go through the trouble of defining a global state? Look at how traditional applications handle data:

# Traditional complex script
user_intent = classify(user_message)

raw_data = fetch_db(user_message, user_intent)

response = generate_response(user_message, user_intent, raw_data)

save_to_db(user_message, user_intent, response)

As your application grows, you end up passing 20 different variables across 30 different nested functions. Dependencies become hidden. If generate_response suddenly needs access to the user's account_tier, you have to rewrite the function signatures of every function leading up to it.

With a centralized State, everything lives in one place. If a node deep in the graph needs the account_tier, it just reads state["account_tier"]. The complexity of data-passing drops to zero.


2.5 Nodes: Workers of the Graph

A Node is simply a Python function that receives the state and returns a state update.

Let's write a node. Notice that there is no LangGraph API here. It's pure Python.

# A simple Node
def classify_intent(state: ChatState):
    # 1. Read state
    message = state["user_message"]
    
    # 2. Perform work (mocked logic)
    found_intent = "support" if "help" in message.lower() else "sales"
    
    # 3. Return ONLY the update to the State
    return {"intent": found_intent}

When LangGraph executes this node, it takes the returned dictionary and automatically merges it into the global state.

Incoming State:

{
    "user_message": "I need help with my router",
    "intent": null,
    "response": null
}

Node Returns:

{
    "intent": "support"
}

Merged Result (New State):

{
    "user_message": "I need help with my router",
    "intent": "support",
    "response": null
}

2.6 Node Types

A massive "aha!" moment for beginners is realizing that nodes are not always LLMs. In a robust enterprise graph, you will have many different types of nodes working together:

The LLM Node

Uses an LLM to generate text or reasoning. State (Question) → LLM → State Update (Answer)

The Tool Node

Executes external side-effects like hitting APIs. State (SQL Query) → Database → State Update (Rows)

The Business Logic Node

Pure, deterministic Python code. No AI involved. State (Price) → Discount Logic → State Update (Final Price)

The Human Node

Pauses the graph to wait for a user to click a button. State (Draft Email) → Human → State Update (Approved/Rejected)


2.6.5 Stateless vs Stateful Nodes

A critical concept for designing maintainable graphs is understanding where data lives.

Stateless Node (Good):

def calculate_discount(state: ProductState):
    # Reads from state, returns an update. Perfect.
    discount = state["price"] * 0.10
    return {"discounted_price": state["price"] - discount}

Stateful Node (Bad):

# BAD! Do not do this!
global_discount_cache = {}

def calculate_discount_bad(state: ProductState):
    # This node is holding its own secret state.
    # When the server restarts, this data is permanently lost!
    global_discount_cache[state["id"]] = True
    return {"discounted_price": ...}

LangGraph nodes should behave as stateless workers. Persistent data belongs in the State dictionary, not inside the node logic or in global variables.


2.7 Edges: The Decision Makers

If Nodes perform the work, Edges decide where to go next.

Edges do not modify the state. They simply read the state and return the name of the next Node to execute.


2.8 Simple Edges

The most basic edge is a straight line. Once Node A finishes, unconditionally go to Node B.

 START
   ↓
Extract Data Node
   ↓
Save to DB Node
   ↓
  END

2.9 Conditional Edges (Branching)

Real applications require branching. Because Edges are just Python functions, we can write complex routing logic using simple if/else statements.

                  [Approved] ↗ Publish Node
                 /
Review Product Node
                 \
                  [Rejected] ↘ Reject Node

Code example:

def route_review(state: ProductState):
    # Edge reads the state and returns the string name of the next node
    if state["approved"] == True:
        return "publish_node"
    else:
        return "reject_node"

Warning: The returned string must match the exact node name registered in the graph. If you return "Publish_Node" but registered the node as "publish_node", the graph will crash immediately.

Notice that the routing is 100% deterministic. We are not asking an LLM to choose the route; we are evaluating hard Python logic.


2.10 Building the First Graph

Let's combine these concepts and build the smallest possible functioning graph using the actual LangGraph API.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

# 1. Define State
class GraphState(TypedDict):
    name: str
    greeting: str

# 2. Define Node
def generate_greeting(state: GraphState):
    user_name = state["name"]
    return {"greeting": f"Hello, {user_name}!"}

# 3. Build Graph
builder = StateGraph(GraphState)
builder.add_node("greeting_node", generate_greeting)

# 4. Define Edges (A straight line)
builder.add_edge(START, "greeting_node")
builder.add_edge("greeting_node", END)

# 5. Compile into a runnable application!
app = builder.compile()

# Execute
result = app.invoke({"name": "Krishna"})
print(result["greeting"]) # Output: Hello, Krishna!

2.10.1 START and END

In the code above, you saw two special variables imported from langgraph.graph: START and END.

 START
   ↓
 Node A
   ↓
 Node B
   ↓
  END

These are special virtual nodes provided by the framework.

  • START is the graph entrypoint. It represents the moment the user calls .invoke(). You must map an edge from START to your first operational node to kick off the execution.
  • END is the termination point. When a node routes to END, the graph execution halts entirely, and the final state dictionary is returned to the user.

2.11 Tracing Graph Execution

To truly master LangGraph, you must visualize how the State mutates at every single step. Let's trace the execution of the graph we just built.

Step 0: Invocation (START) The user calls .invoke({"name": "Krishna"}). The Graph Runtime initializes the state:

{
    "name": "Krishna",
    "greeting": null
}

Step 1: Edge Evaluation The runtime checks the edges. The rule is START -> greeting_node. The runtime passes the state to greeting_node.

Step 2: Node Execution generate_greeting executes. It returns the update {"greeting": "Hello, Krishna!"}. The Graph Runtime merges this update into the global state:

{
    "name": "Krishna",
    "greeting": "Hello, Krishna!"
}

Step 3: Edge Evaluation The runtime checks the edges from greeting_node. The rule is greeting_node -> END.

Step 4: Termination (END) The runtime hits the END node, stops execution, and returns the final state to the user.


2.12 A Real AI Example: The Intent Router

Let's build a truly useful AI workflow: an Intent Router. The user sends a message. We use an LLM to classify it as "Support" or "Sales", and then route the state to the appropriate department node.

         User Message
              ↓
      Intent Classifier Node
              ↓
              ? (Conditional Edge)
             / \
     Support/   \Sales
           /     \
  Support Node   Sales Node

To ensure perfect routing, we will force the LLM to output a strict Pydantic structure instead of brittle string parsing.

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

# 1. State Definition
class RouterState(TypedDict):
    message: str
    intent: str
    final_reply: str

# 2. Strict LLM Output Schema
class Intent(BaseModel):
    intent: Literal["sales", "support"] = Field(description="The user's intended department")

# --- NODES ---
def classify_intent(state: RouterState):
    """Uses an LLM and structured output to perfectly classify the intent."""
    structured_llm = llm.with_structured_output(Intent)
    
    prompt = f"Classify this message: {state['message']}"
    
    # Returns a valid Pydantic object
    result = structured_llm.invoke(prompt)
    
    # We return the extracted string to update our Graph State!
    return {"intent": result.intent}

def handle_support(state: RouterState):
    """Business logic for support."""
    return {"final_reply": "Connecting you to a technical support agent..."}

def handle_sales(state: RouterState):
    """Business logic for sales."""
    return {"final_reply": "Here is our pricing page: acme.com/pricing"}

# --- CONDITIONAL EDGE ---
def route_department(state: RouterState):
    """Reads the state and returns the exact string name of the next node."""
    if state["intent"] == "sales":
        return "sales_node"
    return "support_node"

# --- BUILD GRAPH ---
builder = StateGraph(RouterState)

# Add Nodes
builder.add_node("classifier", classify_intent)
builder.add_node("support_node", handle_support)
builder.add_node("sales_node", handle_sales)

# Add Edges
builder.add_edge(START, "classifier")

# Add the Conditional Edge
# We say: "After the classifier runs, use the 'route_department' function to decide where to go."
builder.add_conditional_edges("classifier", route_department)

builder.add_edge("support_node", END)
builder.add_edge("sales_node", END)

app = builder.compile()

# Test the router!
sales_test = app.invoke({"message": "How much does the enterprise plan cost?"})
print(sales_test["final_reply"]) # "Here is our pricing page: acme.com/pricing"

support_test = app.invoke({"message": "My password reset link is broken."})
print(support_test["final_reply"]) # "Connecting you to a technical support agent..."

2.13 Common Beginner Mistakes

As you begin designing graphs, avoid these fatal architectural mistakes:

  1. Treating Nodes like Classes: Nodes are functions, not objects. Do not attempt to store persistent variables inside the node (e.g., self.counter = 1). All data must live in the State.
  2. Creating Giant State Objects: Do not put 50 variables in your state just in case you need them. Only put data in the state that must be passed between nodes. Local variables should stay inside the node function.
  3. Giant Logic Nodes: If your node has 3 nested if/else statements and makes 4 different API calls, you are missing the point of LangGraph. Break that giant node into 4 smaller, isolated nodes connected by edges.
  4. Overusing LLM Routing: Beginners try to use LLMs to decide every single edge transition. LLMs are slow, expensive, and unpredictable. Use explicit, deterministic Python if/else logic for routing whenever possible.
  5. Thinking LangGraph is only for Agents: As we saw in our Intent Router, we didn't build an "Agent". We built an intelligent, durable workflow. LangGraph is for any application that requires state management.

2.14 Summary

The core architectural pattern of LangGraph can be summarized in one loop:

State  →  Node  →  State Update  →  Edge  →  Next Node

Everything in LangGraph—from simple routers to massive, multi-agent swarms—is just this exact pattern repeated over and over again.

Before we can build sophisticated agents that write code and correct their own errors, we must deeply understand how state is represented, merged, and modified. In the next chapter, we will dive deeply into the absolute core of the framework: LangGraph State, TypedDicts, Reducers, and State Updates.