Skip to content

Chapter 2: Introduction to LangGraph.js

Theoretical Foundations

In Book 3, we explored the fundamental building blocks of LangChain: chains. A chain is a linear sequence of steps, where the output of one component becomes the input of the next. It is a Directed Acyclic Graph (DAG) with a single, unbranching path. While powerful for straightforward tasks like summarizing a document or answering a single question, chains lack the ability to reflect or iterate. They execute once and terminate. To build autonomous agents capable of reasoning, planning, and self-correction, we require a more sophisticated computational structure.

This is where LangGraph.js introduces the cyclical graph. Unlike a linear chain, a graph allows for loops. An agent can perform an action, observe the result, and decide to return to a previous step to refine its approach—a process known as the ReAct (Reasoning and Acting) loop. LangGraph.js is a library built on top of LangChain.js that enables the construction of these stateful, cyclical computational graphs. It treats an agent not as a single monolithic LLM call, but as a flow of execution through nodes (operations) and edges (transitions), governed by a shared state.

The Web Development Analogy: Microservices vs. Serverless Functions

To understand the architectural shift from LangChain to LangGraph, consider a web application.

  • LangChain (The Serverless Function): A standard LangChain chain is analogous to a serverless function (e.g., AWS Lambda, Vercel Edge Function). It is stateless, ephemeral, and executes a single, predefined sequence of operations in response to a request. It receives input, processes it, and returns a response. It has no memory of previous invocations and cannot loop back on itself. It's perfect for a specific, isolated task.

  • LangGraph (The Microservice Architecture): LangGraph is akin to a microservice architecture orchestrated by a message bus or a workflow engine (like Temporal or AWS Step Functions). Each "node" in the graph is a distinct service (or microservice) with a specific responsibility: one service might call an LLM, another might query a database, and a third might parse the output. The "edges" are the communication pathways between these services. The "state" is a shared data store (like a database or a cache) that all services can read from and write to. The key difference is the ability to create feedback loops. A monitoring service (a node) can check the health of a request (the state) and, if conditions aren't met, route the message back to a previous service for a retry or a different processing path. This is the cyclical nature of LangGraph, enabling complex, long-running workflows that can adapt based on intermediate results.

Deconstructing the Graph: Nodes and Edges

At its heart, a LangGraph is defined by two primitive components: nodes and edges.

Nodes represent units of computation. In the context of autonomous agents, a node is typically a function that performs an action. This could be: 1. An LLM Call: Invoking a language model to generate text, reason about a problem, or make a decision. 2. A Tool Execution: Calling an external API, querying a database, or running a calculator. 3. A State Update: A simple function that modifies the shared state directly (e.g., incrementing a counter, logging an event). 4. A Conditional Branch: A node that doesn't perform computation itself but directs the flow based on the current state.

Edges represent the flow of control. They define which node executes next after a given node finishes. There are two primary types of edges: 1. Direct Edges: A simple A -> B connection. Node A always transitions to Node B. 2. Conditional Edges: A A -> B or A -> C connection, where the path is determined by a function that inspects the shared state. This is the mechanism that allows an agent to make decisions, such as "If the answer is satisfactory, go to the END node; otherwise, go back to the TOOL_NODE."

Visualizing the Structure

Let's visualize a simple ReAct agent loop, which is the foundational pattern for autonomous agents. The agent starts, reasons about the input, decides whether to use a tool or provide a final answer, and loops until it has enough information.

The diagram illustrates the cyclical flow of a ReAct agent, where reasoning and action are interleaved in a loop that continues until the agent determines it has sufficient information to provide a final answer.
Hold "Ctrl" to enable pan & zoom

The diagram illustrates the cyclical flow of a ReAct agent, where reasoning and action are interleaved in a loop that continues until the agent determines it has sufficient information to provide a final answer.

This diagram illustrates the cyclical nature. The path reason -> decision -> tool -> observe -> reason forms a loop. The agent will continue cycling through this loop until the decision node determines that a final answer can be provided, at which point it transitions to the end node.

The Shared State: The Single Source of Truth

The most critical concept in LangGraph is the shared state. In a linear chain, data flows from one component to the next, but there is no central repository that persists across the entire execution. In a cyclical graph, this is insufficient. We need a way to accumulate knowledge, track progress, and maintain context across multiple steps and potential loops.

LangGraph.js uses a state schema, typically defined with Zod (a TypeScript validation library), to enforce the structure of this shared state. This schema acts as a contract, ensuring that every node knows what data is available and what shape it must be in. The state is a JavaScript object that is passed to every node during execution. A node can read the current state and is responsible for returning a new object that represents the updated state.

Why is this so powerful? 1. Memory: The state holds the conversation history, the results of tool calls, and the agent's internal thoughts. Without it, the agent would be amnesiac with every step. 2. Coordination: It acts as a communication channel between nodes. A tool node writes its result to the state, and the next LLM node reads that result to inform its next thought. 3. Persistence: By saving the state after each node execution, we can pause, resume, or even rewind the entire workflow. This is the role of the Checkpointer.

Example State Schema (Conceptual)

import { z } from "zod";

// This is the blueprint for our agent's memory.
// It defines what data the agent must track throughout its execution.
const AgentStateSchema = z.object({
  // The original input from the user.
  input: z.string(),

  // An array to store the sequence of thoughts and actions.
  // This is the agent's "chain of thought" or "trace".
  messages: z.array(
    z.object({
      role: z.enum(["agent", "tool", "system"]),
      content: z.string(),
    })
  ),

  // The final, formatted answer to be returned to the user.
  finalAnswer: z.string().optional(),

  // A flag to control the graph's execution flow.
  // This is read by the conditional edge to decide the next step.
  shouldContinue: z.boolean(),

  // A counter to prevent infinite loops.
  iterationCount: z.number().default(0),
});

// In a real implementation, we would use this schema to define the graph's state.
// For now, we just conceptualize it as the "shape" of the data flowing through the graph.
type AgentState = z.infer<typeof AgentStateSchema>;

The Checkpointer: Enabling Stateful Workflows

The Checkpointer is the abstraction that makes long-running, stateful workflows practical. It is a persistence layer responsible for saving the complete Graph State after each node execution. Think of it as a "save game" feature for a complex process.

Why is a Checkpointer necessary? An agent's reasoning process can be time-consuming and expensive. It might involve multiple LLM calls and API queries. If the process is interrupted (e.g., a server crash, a network timeout, or simply wanting to pause and resume later), all progress would be lost without a checkpointer.

How it works under the hood: 1. Transaction ID: When a run is initiated, the checkpointer creates a unique ID for that execution (a thread_id or run_id). 2. Snapshotting: After each node successfully completes, the checkpointer serializes the entire current state (the AgentState object) and saves it to a persistent store (like Redis, a SQL database, or even a local file) associated with the transaction ID. 3. Resumption: When resuming a run, the graph engine asks the checkpointer for the last saved state corresponding to the transaction ID. It then "hydrates" the in-memory state with this data and continues execution from the last known point, rather than from the beginning.

This capability is what transforms a simple script into a robust, production-ready agent system. It provides fault tolerance, allows for human-in-the-loop interventions (pausing to approve an action), and enables debugging by inspecting the state at any point in the graph's execution.

Entry Point and Graph Compilation

Finally, to bring all these components together, we must define the Entry Point Node. This is the designated starting node in a StateGraph definition. All execution runs begin by invoking the logic associated with this node, which initializes the workflow. It's the "main" function of our graph.

The process of building a LangGraph involves: 1. Defining the State Schema: Using Zod to create the blueprint for our shared state. 2. Creating Nodes: Defining functions (or Runnable objects) that will perform the work of each node. 3. Adding Nodes and Edges: Using the graph builder API to register nodes and define the connections (both direct and conditional) between them. 4. Setting the Entry Point: Specifying which node should be executed first. 5. Compiling the Graph: The final step where the graph definition is validated and compiled into an executable Runnable object. This compilation step often involves optimizations and checks for graph integrity (e.g., ensuring there are no unreachable nodes).

Once compiled, the graph is executed by calling its .stream() or .invoke() method with an initial input. The checkpointer (if configured) is passed in at runtime, managing the state's persistence throughout the entire lifecycle of the run. This structured approach provides a deterministic, observable, and resilient framework for orchestrating complex multi-agent systems and workflows.

Basic Code Example

In this example, we will simulate a simple SaaS application where a user submits a request, and two specialized agents collaborate to fulfill it. We will build a cyclical graph that implements a basic ReAct (Reasoning and Acting) loop. The first agent will reason about the request, and the second will act on it. The graph will continue looping between these agents until a specific termination condition is met (i.e., the task is complete).

This demonstrates the fundamental LangGraph components: 1. State: A shared memory object (GraphState) accessible by all nodes. 2. Nodes: JavaScript functions (representing LLM calls or tools) that read/update the state. 3. Edges: Direct connections between nodes. 4. Conditional Edges: Logic that determines the next step based on the state's content.

The Graph Visualization

The following Graphviz DOT code illustrates the cyclical structure of our ReAct loop.

This diagram visualizes the cyclical ReAct loop, where conditional edges dynamically route the agent's flow between reasoning and action steps based on the current state's content.
Hold "Ctrl" to enable pan & zoom

This diagram visualizes the cyclical ReAct loop, where conditional edges dynamically route the agent's flow between reasoning and action steps based on the current state's content.

Complete TypeScript Code

This code is fully self-contained. It uses @langchain/core for the base state management and zod for schema validation. To run this, you would typically install these dependencies (npm install @langchain/core zod).

/**
 * SaaS Multi-Agent Workflow: "Hello World" Example
 * 
 * Context: A user submits a request to a SaaS dashboard.
 * Goal: Demonstrate a cyclical ReAct loop using LangGraph.js.
 * 
 * Dependencies: @langchain/core, zod
 */

import { z } from "zod";
import { StateGraph, START, END } from "@langchain/core/graphs/state_graph";
import { Annotation } from "@langchain/core/langgraph";

// ==========================================
// 1. Define the Shared State Schema
// ==========================================

/**
 * We use Zod to define the structure of our Graph State.
 * This ensures type safety across our nodes.
 * 
 * @property {string} input - The original user request.
 * @property {string[]} steps - A log of actions taken by the agents.
 * @property {string} status - Current state of the workflow (e.g., "thinking", "acting", "finished").
 * @property {boolean} shouldContinue - A flag used by conditional edges to decide the next node.
 */
const GraphState = Annotation.Root({
    input: Annotation<string>({
        reducer: (curr, update) => update ?? curr, // Keep existing value if update is undefined
        default: () => "",
    }),
    steps: Annotation<string[]>({
        reducer: (curr, update) => [...curr, update], // Append new steps to the array
        default: () => [],
    }),
    status: Annotation<string>({
        reducer: (curr, update) => update,
        default: () => "idle",
    }),
    shouldContinue: Annotation<boolean>({
        reducer: (curr, update) => update,
        default: () => false,
    }),
});

// ==========================================
// 2. Define Node Logic (Agents)
// ==========================================

/**
 * NODE: Reasoner Agent
 * 
 * This node simulates an LLM analyzing the input and deciding if an action is needed.
 * In a real app, this would call an LLM (e.g., GPT-4).
 * 
 * @param state - The current GraphState
 * @returns Partial<GraphState> - The updated state
 */
const reasonerNode = (state: typeof GraphState.State): Partial<typeof GraphState.State> => {
    console.log("🤖 [Reasoner] Thinking...");

    // Simulate LLM logic:
    // If the input contains "search", we need an action.
    // Otherwise, we might be done.
    const needsAction = state.input.toLowerCase().includes("search");
    const actionDescription = needsAction 
        ? `I need to perform a web search for: "${state.input}"` 
        : `I understand the request: "${state.input}". No external action needed.`;

    return {
        steps: [...state.steps, `Reasoning: ${actionDescription}`],
        status: needsAction ? "acting" : "finished",
        shouldContinue: needsAction, // This flag tells the edge what to do
    };
};

/**
 * NODE: Actor Agent
 * 
 * This node simulates an LLM performing an external action (like a tool call).
 * 
 * @param state - The current GraphState
 * @returns Partial<GraphState> - The updated state
 */
const actorNode = (state: typeof GraphState.State): Partial<typeof GraphState.State> => {
    console.log("⚡ [Actor] Executing action...");

    // Simulate a tool execution (e.g., fetching data from an API)
    // In a real app, this might fetch data from a database or call a 3rd party API.
    const mockApiResult = "Found result: 'LangGraph.js Documentation'";

    return {
        steps: [...state.steps, `Action: ${mockApiResult}`],
        status: "reasoning", // Go back to reasoning to process the result
        shouldContinue: true, // Keep the loop going
    };
};

// ==========================================
// 3. Define Conditional Logic
// ==========================================

/**
 * EDGE CONDITION: decideNextStep
 * 
 * This function inspects the state to determine which node to run next.
 * It enables the cyclical behavior.
 * 
 * @param state - The current GraphState
 * @returns string - The name of the next node (or END)
 */
const decideNextStep = (state: typeof GraphState.State): string => {
    // If the status is "finished", we terminate the graph
    if (state.status === "finished") {
        return END;
    }

    // If the status is "acting", go to the Actor node
    if (state.status === "acting") {
        return "actor_node";
    }

    // Default: Go back to the Reasoner node
    return "reasoner_node";
};

// ==========================================
// 4. Build and Compile the Graph
// ==========================================

/**
 * Main Graph Construction
 */
const workflow = new StateGraph(GraphState);

// Add nodes to the graph
workflow.addNode("reasoner_node", reasonerNode);
workflow.addNode("actor_node", actorNode);

// Define the entry point
workflow.addEdge(START, "reasoner_node");

// Define the conditional edge
// The graph calls 'decideNextStep' after 'actor_node' finishes
workflow.addConditionalEdges(
    "actor_node",
    decideNextStep
);

// Add a standard edge from the reasoner back to the conditional check
// (In this simple loop, we actually just want to check the condition after the actor runs,
// but for a robust ReAct loop, we might check after the reasoner too if it finishes immediately).
// For this specific example, we will route reasoner -> actor directly if action is needed,
// but the conditional edge handles the branching.
// To keep it simple and cyclical:
workflow.addConditionalEdges(
    "reasoner_node",
    (state) => {
        // If reasoner says we should continue (needs action), go to actor.
        // If reasoner says finished (no action needed), go to END.
        return state.shouldContinue ? "actor_node" : END;
    }
);

// Compile the graph into an executable runtime
const app = workflow.compile();

// ==========================================
// 5. Execution
// ==========================================

/**
 * Main function to run the SaaS Workflow
 */
const runSaaSWorkflow = async () => {
    console.log("🚀 SaaS Dashboard: Initializing Agent Workflow...\n");

    // Initial State
    const initialState = {
        input: "Search for latest LangGraph updates",
        steps: [],
        status: "idle",
        shouldContinue: false,
    };

    // Execute the graph stream
    // We use stream to see the step-by-step execution
    const stream = await app.stream({ input: initialState.input });

    for await (const output of stream) {
        // Access the node name from the output keys
        const nodeName = Object.keys(output)[0];
        const nodeState = output[nodeName];

        console.log(`--- Step Executed: ${nodeName} ---`);
        console.log(`Status: ${nodeState.status}`);
        console.log(`Log: ${nodeState.steps[nodeState.steps.length - 1]}`);
        console.log(""); // Empty line for readability
    }

    console.log("✅ Workflow Complete.");
};

// Run the example
runSaaSWorkflow().catch(console.error);

Detailed Line-by-Line Explanation

1. State Definition (GraphState)

  • zod Integration: We define a schema using zod. This is crucial for TypeScript projects to prevent runtime errors. If a node tries to push a number into steps (which expects strings), TypeScript will flag it immediately.
  • Annotation.Root: This is the LangGraph mechanism for defining shared state. Unlike standard Redux or React state, this is designed to be distributed across a graph of nodes.
  • Reducers: Notice the reducer property (e.g., (curr, update) => [...curr, update]). In a cyclical graph, state accumulates. If we didn't use a reducer to append to the steps array, the previous step's data would be overwritten every time the graph loops.

2. Node Logic

  • reasonerNode: This simulates the "Reason" phase of ReAct. It inspects the input string. In a production SaaS app, this node would invoke an LLM (like GPT-4) with a system prompt. Here, we use a simple string check (includes("search")) to keep the code executable without API keys.
  • actorNode: This simulates the "Act" phase. It takes the reasoning from the previous step and performs an action. In a real scenario, this would be a fetch call to an external API or a database query.
  • Return Values: Both nodes return a Partial<GraphState.State>. LangGraph automatically merges these updates into the global state using the reducers we defined.

3. Conditional Edges (decideNextStep)

  • The Brain of the Loop: This function is the most critical part of a cyclical graph. It does not execute code; it simply returns the name of the next node to execute.
  • Logic Flow:
    1. Check state.status. If "finished", return END (a special LangGraph constant).
    2. If "acting", return "actor_node".
    3. Otherwise, return "reasoner_node".
  • Why this matters: This allows the graph to be dynamic. If the Actor returns data that requires more reasoning, the graph loops back. If the data is sufficient, the graph terminates.

4. Graph Construction

  • new StateGraph(GraphState): Initializes the graph with our defined state schema.
  • addNode: Registers the functions we defined earlier as nodes. The key is the node name (string), and the value is the function.
  • addEdge(START, "reasoner_node"): Defines the entry point. When the graph starts, it immediately jumps to the Reasoner.
  • addConditionalEdges: This attaches the decideNextStep function to the actor_node. After the actor runs, the graph pauses to evaluate this function before proceeding.

5. Execution

  • app.stream(): This is the asynchronous generator that powers the graph. It yields the state after every node execution. This is essential for SaaS apps because it allows the frontend to display real-time updates (e.g., a loading spinner on "Reasoning", then "Acting").
  • The Loop: The for await loop captures these updates. In a web server (like Next.js API Route), you might write these chunks to a ReadableStream for the client.

Common Pitfalls

1. The "Undefined Update" Trap (TypeScript/State Merging)

LangGraph merges node outputs into the state. If you return { status: undefined } from a node, LangGraph might overwrite the existing status with undefined depending on the reducer configuration. * Fix: Always ensure your node returns explicit values or use the spread operator carefully: return { ...state, status: "new_status" }. * Specific JS Issue: JavaScript's loose typing means you might accidentally return a Promise inside the state object if you forget await inside a node. This will serialize the Promise object as text [object Promise] in your state logs, breaking the logic.

2. Vercel/AWS Lambda Timeouts (Async/Await Loops)

In a Serverless environment (like Vercel), execution is time-boxed (e.g., 10 seconds on Hobby plans). A cyclical graph with 50 iterations might hit this limit. * The Pitfall: Using app.invoke() (which waits for the entire graph to finish) in a serverless function will cause a timeout error if the loop runs too long. * The Fix: Use app.stream() instead. It yields control back to the event loop after every node. In a Next.js App Router, you can pipe this stream directly to the HTTP response, bypassing the function timeout (as the connection remains open).

3. Hallucinated JSON in LLM Nodes

If your reasonerNode uses an actual LLM to generate JSON for the state update, LLMs often hallucinate extra text (e.g., "Here is the JSON you asked for: { ... }"). * The Pitfall: The LLM returns a string like "The answer is 42." instead of the expected JSON object. When LangGraph tries to parse this into the state, it crashes. * The Fix: Always use a Output Parser (like JsonOutputParser from LangChain) inside your node. Do not trust the raw string output of an LLM to match your Zod schema without parsing and validation.

4. Mutable State References

In JavaScript, objects are passed by reference. If you modify state.steps.push('new step') directly inside a node, you are mutating the global state reference. * The Pitfall: This causes unpredictable behavior in cyclical graphs because the history is being rewritten in place rather than creating a new history branch. It breaks the ability to time-travel (rollback) the graph state. * The Fix: Always return new objects. Use immutable patterns: return { steps: [...state.steps, 'new step'] }. This is why the code uses the spread operator (...) in the reducer and return statements.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon


Loading knowledge check...



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.