AI Agents LangGraph

Nodes in LangGraph

Intermediate

Nodes in LangGraph
In LangGraph, a node is the fundamental building block of any workflow. It is a reusable unit of logic that receives the current state, performs an action, and returns updates to be merged back into the state. Nodes are extremely flexible, they can be simple Python functions, LangChain Runnables, classes, or even entire subgraphs. You add them to the graph using graph.add_node() , and connect them using edges to control the flow of execution. Nodes can receive the full state (or specific parts of it), perform a wide variety of tasks, from calling LLMs and tools to validation, retrieval, or human approval, and return either full state replacements, partial updates, or no changes at all. From basic function nodes to specialized ones like LLM Nodes, Tool Nodes, Router Nodes, Retrieval Nodes, Human-in-the-Loop Nodes, Parallel Execution Nodes, and Subgraph Nodes, LangGraph gives you powerful building blocks to create complex, reliable, and modular AI agents and workflows. Mastering nodes is the key to building scalable, maintainable, and production-ready applications with LangGraph.

Nodes in LangGraph: The Building Blocks of Graph

What is a Node?

A node is the fundamental unit of computation in LangGraph. Think of nodes as the cells of your graph network, they're where the actual processing and actions take place.

Nodes can be:

  • Functions that transform data
  • Tool calls that interact with external systems
  • LLM invocations that generate responses
  • Any executable unit that processes state

In essence, a node is where work happens in your graph. Later we will back to this.

What Does a Node Receive?

Like any function, a node receives input to process. In LangGraph, this input is called state .

def my_node(state: State) -> dict:
    # state contains all the data flowing through the graph
    user_input = state["messages"]
    # ... do something

State is the context for agents and workflows:

  • It carries data from one node to the next
  • It's shared across all nodes in the graph
  • It accumulates information as it flows through

When a node executes, it receives the current state and can read any field it needs. In State Management topic state is discussed in details.

What Does a Node Do?

A node performs actions on the state. This can include:

1. Calling Tools

A node executes external tools (e.g., search, calculator, or custom functions) selected by the LLM and adds the tool output back to the state.
def search_node(state: State) -> dict:
    query = state["query"]
    results = search_tool(query)  # External API call
    return {"search_results": results}

2. Performing Calculations

A node runs mathematical operations, data analysis, or custom business logic and updates the state with the computed results.
def calculate_node(state: State) -> dict:
    total = sum(state["values"])
    return {"total": total}

3. Database Operations

A node reads from or writes to databases (SQL, vector stores, etc.) to fetch or persist information needed during the workflow.
def save_to_db_node(state: State) -> dict:
    db.insert(state["data"])
    return {"saved": True}

4. Web Scraping/API Calls

A node fetches live data from external websites or APIs and brings that information into the graph’s state.
def fetch_data_node(state: State) -> dict:
    url = state["url"]
    data = requests.get(url).json()
    return {"fetched_data": data}

5. LLM Calls

A node sends a prompt or messages to a Large Language Model (GPT, Claude, Grok, etc.) to generate text, make decisions, or plan next steps.

def llm_node(state: State) -> dict:
    response = llm.invoke(state["messages"])
    return {"messages": state["messages"] + [response]}

6. State Transformation

A node modifies, filters, summarizes, or restructures the current state without calling any external service.

def transform_node(state: State) -> dict:
    processed = process_text(state["input"])
    return {"output": processed}

The key principle: Nodes read from state, perform some operation, and return updates to state.

What Does a Node Return?

Nodes in LangGraph return updates that get merged into the current state. You don’t need to return the entire state every time, you can return only what should change.
There are three main patterns :

1. Full State Update (Replace Everything)

Use this when you want to completely reset or overwrite the entire state.
def reset_node(state: State) -> State:
    return {
        "messages": [],
        "count": 0,
        "status": "reset",
        "documents": []
    }
Return only the fields you want to update. LangGraph will automatically merge these changes with the existing state.
def increment_node(state: State) -> dict:
    return {
        "count": state["count"] + 1,           # Only update this field
        "messages": [AIMessage(content="...")] # We can update multiple fields
    }
Key Benefit : Fields we don’t return remain unchanged.

3. Return None (No State Change)

Useful for logging, side effects, or nodes that only perform actions without modifying state.
def logging_node(state: State) -> None:
    logger.info(f"Processing state: {state}")
    # No return statement = no changes to state
    # (implicitly returns None)

How to Add a Node in LangGraph

The add_node() method is the fundamental way to add a new node to your LangGraph. A node can be a simple Python function, a LangChain Runnable, a class with __call__, or even another compiled graph (subgraph).

Two Main Ways to Add a Node

1. Let LangGraph automatically use the function name:
def research_node(state):
    # your logic here
    return {"documents": [...]}

graph.add_node(research_node)   # Node name becomes "research_node"
2. Explicitly give the node a custom name (Recommended for clarity):
graph.add_node("research", research_node)
# or
graph.add_node("agent", agent_llm_with_tools)
Full Example with Common Options:
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode

# Adding different types of nodes
graph = StateGraph(MessagesState)

graph.add_node("agent", agent_node)                    # LLM / Agent node
graph.add_node("tools", ToolNode(tools))               # Tool node
graph.add_node("retriever", retrieval_node)            # Custom node

# With metadata and retry (advanced)
graph.add_node(
    "web_search",
    web_search_node,
    metadata={"description": "Search the internet", "type": "tool"},
    retry=RetryPolicy(max_attempts=3)                  # Auto-retry on failure
)

Node Naming Best Practices

Good node names make your graph much easier to read, debug, and maintain, especially as your workflow grows.
Recommended Practices
  • Use clear, descriptive names that explain what the node does
  • Use snake_case (lowercase with underscores)
  • Keep names concise but meaningful
  • Use verbs when possible (fetch, validate, generate, summarize, etc.)
# Good Examples
builder.add_node("fetch_data", fetch_data)
builder.add_node("validate_input", validate_input)
builder.add_node("generate_response", generate_response)
builder.add_node("retrieve_documents", retrieval_node)
builder.add_node("route_decision", router_node)
builder.add_node("summarize_results", summarizer)
Avoid These
# Bad Examples
builder.add_node("node1", func1)           # Too vague
builder.add_node("step", func)             # Not descriptive
builder.add_node("my_node", process)       # Unclear purpose
builder.add_node("llm", llm_call)          # Too generic (what does it do?)
Pro Tip:
Use names that reflect the action or responsibility of the node. This becomes especially valuable when reading the graph structure or debugging execution flow.

Common Types of Nodes in LangGraph

below we review some common types of nodes. In practice, most LangGraph nodes fall into one of these categories:

Category Purpose
Processing Nodes Transform data
Reasoning Nodes LLM thinking/planning
Action Nodes Tool/API execution
Control Nodes Routing/decision-making
Memory Nodes Store/retrieve state
Integration Nodes External systems
Human Nodes Human approval/input

Technically, all LangGraph nodes are usually just Python callables . The “type” of node is more about:

  • its role
  • behavior
  • workflow purpose

rather than a strict LangGraph class type system.

So:

  • “router node”
  • “memory node”
  • “validation node”

are architectural patterns, not separate built-in LangGraph node classes. However, we review some of common type of them in more details. nevertheless, in other sections we elaborate most of them.

1. Function Nodes

Standard Python functions that process state.

Used for:

  • data transformation
  • routing preparation
  • calculations

This node can be define in several ways:

1.1  Plain Python Function (most common)

The simplest and most widely used way to create a node. Any regular Python function that accepts the graph state and returns updates to it.
from langgraph.graph import StateGraph

def my_node(state: dict):
    # Perform any logic
    result = some_calculation(state["input"])
    return {"output": result, "messages": [...]}

# Add to graph
graph.add_node("my_node", my_node)

1.2  Class with __call__

Useful when we want to maintain internal state, configuration, or reusable logic across multiple graph runs.
class MyNode:
    def __init__(self, model_name: str):
        self.llm = ChatOpenAI(model=model_name)
    
    def __call__(self, state: dict):
        response = self.llm.invoke(state["messages"])
        return {"messages": [response]}

# Usage
node = MyNode("gpt-4o")
graph.add_node("agent", node)

1.3 Async Function

For I/O-heavy operations (LLM calls, API requests, database queries) to enable concurrent and faster execution.

async def async_node(state: dict):
    response = await llm.ainvoke(state["messages"])
    return {"messages": [response]}

# Add normally — LangGraph handles async automatically
graph.add_node("async_agent", async_node)

1.4 LangChain Runnable (chain, LLM, etc.)

You can directly use any LangChain Runnable (LLM, chain, prompt + LLM, etc.) as a node.

from langchain_core.runnables import Runnable

# LLM as node
llm = ChatOpenAI(model="gpt-4o")

# Chain as node
chain = prompt | llm | parser

graph.add_node("llm_node", llm)      # direct LLM
graph.add_node("chain_node", chain)  # full chain
LangChain Runnables are one of the easiest and most popular ways to use LangChain components inside LangGraph, but they’re not the only way. We can actually integrate LangChain in many different parts of LangGraph. For example, we can create tools using LangChain’s @tool decorator and run them easily with ToolNode . Managing state is simple too, we can use LangChain’s Annotated types and reducers to update the data cleanly without conflicts. Features like human-in-the-loop and interrupts also work smoothly with LangChain. Plus, we can even take a whole LangGraph or a prebuilt LangChain agent and use it as a subgraph inside our main graph.
In short: While Runnables are the main bridge (especially inside nodes), LangGraph is built to work hand-in-hand with the entire LangChain ecosystem. We'll naturally end up using LangChain tools, prompts, models, memory, and more almost everywhere in your graph.

2. LLM Nodes

An LLM Node is a node specifically designed to call a Large Language Model. It acts as the decision-making brain of most agents in LangGraph. Commonly used for:
  • Text generation
  • Reasoning & decision making
  • Summarization
  • Planning & tool selection
  • Response formatting
def llm_node(state: MessagesState):
    response = llm.invoke(state["messages"])   # or .ainvoke()
    return {"messages": [response]}

# Common pattern with tool calling
tools = [search_web, get_weather, run_sql]
llm_with_tools = llm.bind_tools(tools)

def agent_node(state: MessagesState):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

The LLM can then respond in two ways:

Case 1 — Normal text response:
──────────────────────────────
AIMessage(
    content="Paris is the capital of France."
    tool_calls=[]                              ← empty, no tool needed
)

Case 2 — Tool call response:
─────────────────────────────
AIMessage(
    content=""                                 ← empty, no text
    tool_calls=[{
        "name": "get_weather",
        "args": {"city": "London"},
        "id": "call_abc123"
    }]
)
How Tool Calling Fits in a Graph
The LLM node itself never runs the tool, it just decides which tool to call. A separate tool node does the actual execution:

                    │  agent_node │  ← LLM decides
                    └──────┬──────┘
                           │
              ┌────────────▼─────────────┐
              │  tool_calls in response? │
              └────────────┬─────────────┘
                    │               │
                   YES              NO
                    │               │
           ┌────────▼──────┐   ┌───▼────┐
           │  tool_node    │   │  END   │
           │ (runs tool)   │   └────────┘
           └────────┬──────┘
                    │
           (result added to state)
                    │
                    └──── back to agent_node ──► loops until no tool calls

3. Tool Nodes

Tool Nodes are responsible for executing external tools or APIs. They take tool calls from the LLM and run them, then return the results back into the graph’s state. Common Examples:
  • Web search
  • Database queries
  • Calculator tools
  • Weather APIs
  • Custom business logic tools
Most Common Implementation: ToolNode (from LangGraph)
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain_community.tools import TavilySearchResults

# 1. Define your tools using LangChain
@tool
def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b

search_tool = TavilySearchResults(max_results=2)

tools = [multiply, search_tool]

# 2. Create ToolNode
tool_node = ToolNode(tools=tools)

# 3. Add to your graph
graph.add_node("tools", tool_node)
How it works in a typical agent flow:
# LLM decides which tool to call
graph.add_node("agent", agent_llm_node)

# ToolNode executes the chosen tool
graph.add_node("tools", ToolNode(tools))

# Conditional edge: LLM decides next step
graph.add_conditional_edges("agent", route_tools)

The core difference between LLM Node and Tool Node

LLM Node LLM Node

DECIDES which tool to call

Outputs tool_calls in AIMessage

Uses the LLM (costs tokens)

Returns AIMessage

ACTUALLY RUNS the tool

Reads tool_calls, executes them

No LLM involved, pure Python

Returns ToolMessage

LLM Node — just thinks:

def agent_node(state: MessagesState):
    response = llm_with_tools.invoke(state["messages"])
    # LLM looks at messages and says:
    # "I think I should call get_weather with city=London"
    # but it does NOT call it — just expresses the intent
    return {"messages": [response]}

# response looks like:
# AIMessage(
#     content="",
#     tool_calls=[{"name": "get_weather", "args": {"city": "London"}}]
# )

Tool Node — just executes:

from langgraph.prebuilt import ToolNode

tools = [get_weather, search_web]
tool_node = ToolNode(tools)

# Internally it does roughly this:
# 1. reads tool_calls from last AIMessage
# 2. finds the matching function
# 3. calls get_weather(city="London")
# 4. wraps result in ToolMessage
# No LLM involved at all

Message Flow Makes it Clear

User: "What's the weather in London?"
        │
        ▼
┌──────────────┐
│  agent_node  │  llm_with_tools.invoke(messages)
│   (LLM)      │  
│              │  → AIMessage(tool_calls=[get_weather(London)])
└──────┬───────┘         ← INTENT only, not executed yet
       │
       ▼
┌──────────────┐
│  tool_node   │  sees tool_calls in AIMessage
│  (no LLM)   │  actually calls get_weather("London")
│              │  → ToolMessage(content="15°C, cloudy")
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  agent_node  │  llm sees ToolMessage result in history
│   (LLM)      │  → AIMessage(content="It's 15°C and cloudy in London")
└──────────────┘         ← final answer, no tool_calls this time

4. Router Nodes

Router Nodes decide the next step in the workflow. They inspect the current state and return the name of the node (or nodes) that should execute next. These nodes are typically used together with conditional edges. Commonly used for:
  • Classifying user requests
  • Choosing different workflow paths
  • Deciding whether to call tools or end the process
  • Routing based on LLM output or conditions
def route_tools(state):
    """Router that decides the next node based on the last message."""
    messages = state["messages"]
    last_message = messages[-1]
    
    # If the LLM called a tool, go to tools node
    if last_message.tool_calls:
        return "tools"
    
    # Otherwise, end the graph
    return "END"


# Add to graph
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)

# Conditional routing
graph.add_conditional_edges(
    "agent",          # Start from agent node
    route_tools,      # Router function
    {
        "tools": "tools",
        "END": END
    }
)
As an alternative (using LLM for routing), m any people let the LLM itself decide the route by using .bind_tools() and checking for tool calls.

5. Validation Nodes

Validation Nodes check and ensure that the output from previous nodes is correct, safe, and properly formatted before continuing the workflow. Commonly used for:
  • JSON validation
  • Schema checking
  • Response quality checks
  • Safety / moderation filters
  • Data consistency verification
from pydantic import BaseModel, ValidationError

class ResponseSchema(BaseModel):
    answer: str
    confidence: float

def validation_node(state):
    try:
        validated = ResponseSchema.model_validate(state["output"])
        return {"validated_output": validated.model_dump()}
    except ValidationError as e:
        return {"error": str(e), "next": "fix_output"}  # Send back for retry

6. Memory Nodes

Memory Nodes manage short-term and long-term memory. They store, retrieve, and update conversation history or relevant context so the agent can remember previous interactions. Responsible for:
  • Storing memory
  • Retrieving memory
  • Updating conversation history
Common Examples:
  • Vector database retrieval (Chroma, Pinecone, etc.)
  • Persistent memory updates
  • Message summarization
  • Conversation history management
def memory_node(state):
    # Retrieve relevant past memories
    query = state["messages"][-1].content
    relevant_memory = vector_retriever.invoke(query)
    
    # Update state with memory
    return {
        "messages": state["messages"] + relevant_memory,
        "memory": "updated"
    }

7. Retrieval Nodes

Retrieval Nodes fetch relevant information from external data sources (documents, vector databases, knowledge bases, etc.). They are the core component in RAG (Retrieval-Augmented Generation) systems, allowing agents to access up-to-date or domain-specific knowledge. Commonly used for:
  • Retrieving relevant documents
  • Searching embeddings (vector similarity search)
  • Fetching additional context
  • Knowledge base lookups
  • Semantic search
from langchain_core.vectorstores import VectorStoreRetriever

# Usually created from a vector store
retriever = vector_store.as_retriever(search_kwargs={"k": 4})

def retrieval_node(state):
    """Retrieve relevant documents based on the user's query."""
    query = state["messages"][-1].content
    
    retrieved_docs = retriever.invoke(query)
    
    return {
        "documents": retrieved_docs,           # Add documents to state
        "context": "\n\n".join([doc.page_content for doc in retrieved_docs])
    }

# Add to graph
graph.add_node("retrieve", retrieval_node)

8. Human-in-the-Loop Nodes (HITL)

These nodes pause the graph execution and wait for human input before continuing. They are essential for building reliable, controllable AI systems that involve human oversight. Commonly used for:
  • Approval workflows
  • Manual review and editing
  • Feedback collection
  • Content moderation
  • Decision confirmation
Usually implemented with:
  • interrupt_before / interrupt_after
  • Checkpoints & persistence (SQLite, PostgreSQL, etc.)
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END

# Enable checkpointing
checkpointer = MemorySaver()

# Build graph with interrupt
graph = StateGraph(MessagesState)

# ... add other nodes ...

# Interrupt before a sensitive node
graph.add_node("human_approval", human_approval_node)

# Compile with interrupt
app = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["human_approval"]   # Pause before this node
)

# Run until interrupt
config = {"configurable": {"thread_id": "123"}}
result = app.invoke(inputs, config)

# Later, after human input:
app.invoke(None, config)   # Resume with human feedback
Simple way to resume with human input:
# Human provides feedback
human_feedback = {"messages": [HumanMessage(content="Approved. Proceed.")]}

app.invoke(human_feedback, config)

9. Parallel Execution Nodes

Parallel Execution Nodes allow the graph to run multiple operations simultaneously (in parallel), improving speed and efficiency. LangGraph automatically handles concurrent execution when multiple nodes are triggered at the same time. Commonly used for:
  • Parallel searches (e.g., web + vector search)
  • Concurrent tool calls
  • Batch processing
  • Multi-document analysis
  • Gathering data from multiple sources
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages

# Example: Run multiple retrievers in parallel
def web_search(state):
    return {"documents": web_retriever.invoke(state["query"])}

def vector_search(state):
    return {"documents": vector_retriever.invoke(state["query"])}

def combine_results(state):
    # Merge documents from all parallel branches
    return {"documents": state["documents"]}

graph = StateGraph(MessagesState)

graph.add_node("web_search", web_search)
graph.add_node("vector_search", vector_search)
graph.add_node("combine", combine_results)

# Fan-out: Start both searches in parallel
graph.add_edge(START, "web_search")
graph.add_edge(START, "vector_search")

# Fan-in: Wait for both to finish, then combine
graph.add_edge("web_search", "combine")
graph.add_edge("vector_search", "combine")

graph.add_edge("combine", END)
Modern way using SEND (Recommended):
from langgraph.types import Send

def route_to_parallel(state):
    return [
        Send("web_search", state),
        Send("vector_search", state)
    ]

graph.add_conditional_edges(START, route_to_parallel)

10. Aggregation Nodes

Aggregation Nodes combine, merge, or process outputs from multiple parallel branches. They act as the “fan-in” point in your graph, collecting results from different paths before continuing. Commonly used for:
  • Merging search results from multiple sources
  • Summarizing outputs from parallel operations
  • Combining tool responses
  • Reducer-based state updates
  • Final result synthesis
from langgraph.graph.message import add_messages
from typing import Annotated

# Using built-in reducers (Recommended)
class State(TypedDict):
    messages: Annotated[list, add_messages]
    documents: Annotated[list, lambda x, y: x + y]   # Custom reducer

def aggregate_results(state):
    """Merge and summarize results from parallel branches."""
    all_docs = state.get("documents", [])
    
    # Optional: Summarize using LLM
    summary = llm.invoke(f"Summarize these documents:\n{all_docs}")
    
    return {
        "documents": all_docs,           # Keep all raw docs
        "summary": summary.content       # Add final combined result
    }

# Add to graph
graph.add_node("aggregate", aggregate_results)

# Connect parallel branches to aggregator
graph.add_edge("web_search", "aggregate")
graph.add_edge("vector_search", "aggregate")
graph.add_edge("tool_search", "aggregate")

11. Decision Nodes

Decision Nodes evaluate conditions, scores, or logic and determine the next step in the workflow. They help the graph make smart, rule-based choices without always needing an LLM. Commonly used for:
  • Confidence scoring
  • Retry decisions
  • Fallback selection
  • Threshold-based routing
  • Quality checks and branching logic
def decision_node(state):
    """Evaluate confidence and decide next action."""
    last_message = state["messages"][-1]
    confidence = getattr(last_message, "confidence", 0.0)
    
    if confidence >= 0.85:
        return "end"                    # High confidence → finish
    elif confidence >= 0.6:
        return "retry"                  # Medium → try again
    else:
        return "fallback"               # Low → use fallback path


# Add to graph
graph.add_node("decide", decision_node)

# Conditional routing based on decision
graph.add_conditional_edges(
    "decide",
    lambda state: decision_node(state),   # or pass the function directly
    {
        "end": END,
        "retry": "agent",
        "fallback": "fallback_node"
    }
)

12. Subgraph Nodes

A Subgraph Node is a node that contains an entire LangGraph inside it. This allows you to treat complex workflows as reusable building blocks, making your graphs more organized and scalable. This enables:
  • Modular workflows
  • Reusable components
  • Hierarchical agents (agent teams)
  • Multi-stage pipelines
  • Easier testing and maintenance
from langgraph.graph import StateGraph, START, END

# 1. Create a reusable subgraph
def create_research_subgraph():
    subgraph = StateGraph(MessagesState)
    
    subgraph.add_node("retrieve", retrieval_node)
    subgraph.add_node("analyze", analysis_node)
    subgraph.add_node("summarize", summarizer_node)
    
    subgraph.add_edge(START, "retrieve")
    subgraph.add_edge("retrieve", "analyze")
    subgraph.add_edge("analyze", "summarize")
    subgraph.add_edge("summarize", END)
    
    return subgraph.compile()

# 2. Add the subgraph as a normal node in the main graph
research_subgraph = create_research_subgraph()

main_graph = StateGraph(MessagesState)
main_graph.add_node("research", research_subgraph)   # ← Subgraph as node

main_graph.add_node("final_answer", final_answer_node)

main_graph.add_edge(START, "research")
main_graph.add_edge("research", "final_answer")
main_graph.add_edge("final_answer", END)
Pro Tip: Subgraphs can have their own state, checkpointer, and even interrupts — making them powerful for building agent teams or complex multi-agent systems.

13. Node with RunnableConfig (Accessing Metadata & Thread Info)

You can optionally accept a second argument config: RunnableConfig in your node function. This gives you access to useful runtime information such as thread_id , tags , metadata , and more.
from langchain_core.runnables import RunnableConfig
from langgraph.graph import MessagesState

def my_node(state: MessagesState, config: RunnableConfig):
    """Node that can access thread metadata and runtime config."""
    
    # Access configurable fields (very common)
    thread_id = config["configurable"].get("thread_id")
    user_id = config["configurable"].get("user_id")
    
    # Access metadata and tags
    metadata = config.get("metadata", {})
    tags = config.get("tags", [])
    
    print(f"Running on thread: {thread_id} | User: {user_id}")
    
    # Your logic here...
    return {"messages": ["Node executed successfully"]}
Common Use Cases:
  • Accessing thread_id for logging or tracing
  • Reading custom metadata passed at runtime
  • Implementing user-specific or session-specific behavior
  • Conditional logic based on tags or metadata
Tip: This is especially useful in production-grade agents for logging, debugging, and multi-tenancy.

AI agent LangChain LangGraph Python

← All training