Skip to content

Chapter 16: Planning Agents - Plan-and-Execute

Theoretical Foundations

The Plan-and-Execute pattern represents a fundamental shift in how autonomous agents approach complex tasks. Unlike the ReAct (Reasoning and Acting) pattern introduced in earlier chapters—where an agent dynamically reasons and acts in a continuous, often unpredictable loop—Plan-and-Execute introduces a rigid separation of concerns. It decouples the strategy (the plan) from the tactics (the execution).

Imagine you are tasked with constructing a complex piece of furniture, like a modular bookshelf. You have two approaches:

  1. The ReAct Approach (The "As-You-Go" Builder): You pick up a piece of wood, look at the diagram, try to figure out where it goes, nail it in, and then repeat the process. You might realize halfway through that you missed a step or used the wrong screw. You adapt dynamically. This is flexible but prone to "reasoning loops" where the agent gets stuck re-evaluating the same step repeatedly.
  2. The Plan-and-Execute Approach (The Architect & The Contractor): First, you (the Planner) sit down, study the diagram, and create a numbered list of steps: 1. Assemble the base frame. 2. Attach the vertical supports. 3. Install the shelves. You hand this list to a Contractor (the Executor). The Contractor does not question the list; they simply execute Step 1, then Step 2, then Step 3. If a step fails (e.g., a screw is missing), the Contractor reports the error, but they do not rewrite the entire blueprint.

In the context of LangGraph.js, the Plan-and-Execute pattern ensures deterministic workflows. It prevents the agent from getting lost in infinite reasoning loops by forcing it to commit to a plan before acting. This is critical for production systems where predictability and auditability are paramount.

The "Why": Determinism, Efficiency, and Error Recovery

The primary motivation for choosing Plan-and-Execute over ReAct is control.

1. Mitigating Reasoning Hallucinations

In a ReAct agent, the LLM decides the next step based on the immediate context. If the context is noisy, the LLM might hallucinate a tool that doesn't exist or perform a redundant action. By generating a plan upfront, we constrain the LLM's future choices. The execution phase is restricted to the steps defined in the plan, significantly reducing the surface area for hallucination.

2. Optimization via Parallelization

While a basic Plan-and-Execute implementation executes steps sequentially, the planning phase allows us to identify independent steps. In a ReAct loop, the agent often cannot "see" the future to realize that Step 3 and Step 4 could happen simultaneously. With a plan in hand, a sophisticated orchestrator (or a future optimization layer) can analyze the dependency graph and dispatch independent tasks to a Worker Agent Pool concurrently.

3. State Management and Resilience

In a long-running ReAct loop, if the agent encounters an error, it often has to restart the reasoning process from the beginning or a recent checkpoint. In Plan-and-Execute, the state is explicitly tracked against a static plan. If Step 3 fails, the system knows exactly where it left off. The state is not just a conversation history; it is a structured object tracking plan_id, current_step_index, and completed_steps.

The Architecture: A Web Development Analogy

To understand the mechanics, let's map the Plan-and-Execute architecture to a modern Microservices Architecture using an API Gateway.

  • The Planner Node = The API Gateway / API Design Phase Before writing code, an architect defines the API endpoints (e.g., POST /users, GET /orders). This is the "Plan." It defines what needs to happen without defining how the internal services implement it.
  • The Executor Node = The Microservice The microservice is the "Worker." It receives a specific request (a step) and executes it. It is stateless regarding the overall workflow; it only cares about its specific input and output.
  • The Shared State = The Database / Redux Store The state tracks the current status of the workflow. Just as a Redux store tracks the state of a frontend application, the LangGraph state tracks which steps are pending, in_progress, or completed.

Under the Hood: State Management

In LangGraph.js, the state is typically defined using Annotation (as seen in previous chapters). For Plan-and-Execute, the state object is more complex than a simple chat history.

import { Annotation, StateGraph, START, END } from "@langchain/langgraph";

// Defining the state for a Plan-and-Execute workflow
const PlanExecuteState = Annotation.Root({
  // The high-level objective provided by the user
  input: Annotation<string>(),

  // The generated plan: An array of step objects
  // Example: [{ step: 1, description: "Search for X", status: "pending" }]
  plan: Annotation<Array<{ step: number; description: string; status: string }>>({
    reducer: (state, update) => {
      // We replace the entire plan or update specific steps
      return update; 
    },
    default: () => [],
  }),

  // The current step index being executed
  currentStep: Annotation<number>({
    reducer: (state, update) => update,
    default: () => 0,
  }),

  // The final output after all steps are complete
  finalOutput: Annotation<string>({
    reducer: (state, update) => update,
    default: () => "",
  }),
});

The Workflow Breakdown

The execution flow can be visualized as a directed acyclic graph (DAG). Unlike ReAct, which is a cycle (Reason -> Act -> Observe -> Reason), Plan-and-Execute is a linear progression through the plan.

This diagram illustrates the Plan-and-Execute architecture as a linear Directed Acyclic Graph (DAG), where a user input generates a static plan that is executed step-by-step to produce a final output.
Hold "Ctrl" to enable pan & zoom

This diagram illustrates the Plan-and-Execute architecture as a linear Directed Acyclic Graph (DAG), where a user input generates a static plan that is executed step-by-step to produce a final output.

1. The Planner Node

This is the entry point. It takes the user's input (e.g., "Research the latest trends in WebGPU and write a summary report") and prompts an LLM to generate a sequence of discrete steps.

  • Input: The user query.
  • Process: The LLM is instructed to output a structured list (JSON) of steps. It does not execute any tools yet.
  • Output: A populated plan array in the state.

2. The Execution Loop

Once the plan is set, the graph enters the execution phase. This is handled by the Executor Node.

  • Step Selection: The node reads state.currentStep and retrieves the corresponding step description from state.plan.
  • Action: The node passes the step description to an LLM (often a more powerful model or a specialized model) which decides which tool to use (e.g., a Search Agent or a Code Generator).
  • Update: Upon completion, the node updates the status of that step in state.plan (e.g., changing status: "pending" to status: "completed") and increments state.currentStep.

3. The Conditional Edge (The Loop)

The magic of LangGraph lies in the conditional edge. After the Executor node runs, we define a function that checks if state.currentStep < state.plan.length.

  • If True: The graph routes back to the Executor node.
  • If False: The graph routes to the End node.

Comparison with ReAct Agents

It is vital to distinguish this from the ReAct pattern discussed in Book 3.

Feature ReAct Agent Plan-and-Execute
Decision Making Dynamic, step-by-step. Decisions are made during execution. Static, upfront. Decisions are made before execution begins.
Flexibility High. Can adapt to unexpected results immediately. Low. Sticks to the plan unless explicitly programmed to replan on failure.
Predictability Low. The path to the answer can vary. High. The path is defined in the plan.
Best For Open-ended exploration, creative tasks. Structured workflows, data extraction, reliable automation.

The Role of Specialized Agents (Worker Agent Pool)

In a sophisticated implementation, the Executor Node does not perform the work itself. Instead, it acts as a Supervisor that delegates to a Worker Agent Pool.

Returning to our microservice analogy: The Executor is the API Gateway. When it receives a request for "Search for WebGPU trends," it routes this request to the Search Agent microservice. The Search Agent is a specialized agent equipped with specific tools (like a browser tool or a search API tool). It returns the result to the Executor, which then updates the state.

This architecture allows for: 1. Specialization: A Search Agent can be optimized for retrieval, while a Code Agent is optimized for syntax and logic. 2. Separation of Context: The Search Agent doesn't need to know about the final report; it only needs to know how to search. 3. Scalability: You can swap out the underlying models or tools for specific agents without breaking the overall workflow.

Theoretical Foundations

The Plan-and-Execute pattern is the architectural bridge between simple LLM chains and complex autonomous systems. By enforcing a strict separation between planning and execution, we gain:

  1. Reliability: The system follows a predetermined path.
  2. Observability: We can inspect the plan to understand exactly what the system intends to do.
  3. State Integrity: The state object serves as a single source of truth for the workflow's progress.

This pattern is the foundation upon which more complex multi-agent systems are built, allowing us to orchestrate multiple specialized agents (like the Worker Agent Pool) under a unified, deterministic strategy.

Basic Code Example

In this example, we will build a minimal Plan-and-Execute agent system within a TypeScript context. This architecture is designed for a SaaS application where a user requests a complex task that requires a structured sequence of steps.

The Scenario: A user wants to "Plan a weekend trip to Paris". The system will: 1. Plan: Break the request down into logical, sequential steps (e.g., "Book flight", "Reserve hotel"). 2. Execute: Perform the specific action for each step using a "tool" (simulated function). 3. Finalize: Aggregate the results into a coherent response.

This pattern is distinct from ReAct (Reasoning and Acting) agents because the planning phase is decoupled from the execution phase, ensuring the workflow follows a deterministic path defined by the planner.

The LangGraph Architecture

We will use LangGraph.js to define the state, nodes, and edges. The graph will look like this:

A diagram illustrating the LangGraph architecture, where nodes (represented as circles) are connected by directed edges (arrows) to define the flow of state transitions.
Hold "Ctrl" to enable pan & zoom

A diagram illustrating the LangGraph architecture, where nodes (represented as circles) are connected by directed edges (arrows) to define the flow of state transitions.

TypeScript Implementation

/**
 * @fileoverview A minimal Plan-and-Execute agent implementation using LangGraph.js.
 * This example simulates a SaaS backend workflow for planning a trip.
 * 
 * Dependencies: @langchain/core, langgraph
 */

// ============================================================================
// 1. IMPORTS & SETUP
// ============================================================================

import { StateGraph, Annotation, END, START } from "@langchain/core/graphs";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";

// ============================================================================
// 2. STATE DEFINITION
// ============================================================================

/**
 * Defines the structure of the graph's state.
 * 
 * @property plan - An array of strings representing the steps to execute.
 * @property currentStepIndex - The index of the step currently being executed.
 * @property pastSteps - An array of tuples [step, result] capturing execution history.
 * @property response - The final aggregated output string.
 */
const GraphState = Annotation.Root({
  plan: Annotation<string[]>({
    reducer: (state, update) => update, // Overwrite the plan
    default: () => [],
  }),
  currentStepIndex: Annotation<number>({
    reducer: (state, update) => update, // Overwrite index
    default: () => 0,
  }),
  pastSteps: Annotation<Array<[string, string]>>({
    reducer: (state, update) => [...state, update], // Append history
    default: () => [],
  }),
  response: Annotation<string>({
    reducer: (state, update) => update, // Overwrite response
    default: () => "",
  }),
});

// ============================================================================
// 3. TOOL SIMULATION (EXECUTION LOGIC)
// ============================================================================

/**
 * Simulates an external tool call (e.g., booking API, database query).
 * In a real app, this would be a Server Action or external API fetch.
 * 
 * @param step - The description of the step to perform.
 * @returns A promise resolving to the result string.
 */
async function executeTool(step: string): Promise<string> {
  // Simulate network latency
  await new Promise((resolve) => setTimeout(resolve, 100));

  // Simple keyword matching to simulate different outcomes
  if (step.toLowerCase().includes("flight")) {
    return "Flight to Paris booked successfully (Ref: AF1234).";
  }
  if (step.toLowerCase().includes("hotel")) {
    return "Hotel reservation confirmed at 'Le Grand Hotel' (Ref: H-5678).";
  }
  if (step.toLowerCase().includes("museum")) {
    return "Louvre Museum tickets purchased online.";
  }
  return `Executed step: ${step}`;
}

// ============================================================================
// 4. NODES (LOGICAL BLOCKS)
// ============================================================================

/**
 * Node 1: Planner
 * Generates the high-level plan based on the user's initial request.
 * 
 * In a real scenario, this would call an LLM. Here, we hardcode a response
 * for determinism in this "Hello World" example.
 */
const plannerNode = async (state: typeof GraphState.State) => {
  console.log("🤖 [Planner] Generating plan...");

  // Simulated LLM output
  const steps = [
    "Book flight to Paris",
    "Reserve hotel accommodation",
    "Purchase Louvre Museum tickets"
  ];

  return {
    plan: steps,
  };
};

/**
 * Node 2: Executor
 * Executes the specific step at `currentStepIndex`.
 * 
 * This node retrieves the step from the plan, calls the tool,
 * and updates the state with the result.
 */
const executorNode = async (state: typeof GraphState.State) => {
  const { plan, currentStepIndex } = state;

  // Safety check: ensure we have a plan and valid index
  if (currentStepIndex >= plan.length) {
    return { response: "No more steps to execute." };
  }

  const stepToExecute = plan[currentStepIndex];
  console.log(`⚡ [Executor] Executing step ${currentStepIndex + 1}: "${stepToExecute}"`);

  // Call the simulated tool
  const result = await executeTool(stepToExecute);

  return {
    pastSteps: [stepToExecute, result] as [string, string],
  };
};

/**
 * Node 3: Reflector (Router)
 * Determines if the execution is complete or if we need to loop back.
 * 
 * This node acts as a conditional router. It checks the index against
 * the plan length. While we handle the conditional logic in the edges
 * for this specific example, this node prepares the data for the next step.
 */
const reflectNode = async (state: typeof GraphState.State) => {
  const { plan, currentStepIndex, pastSteps } = state;

  console.log("🔍 [Reflector] Checking progress...");

  // If we have completed all steps, format the final response
  if (currentStepIndex >= plan.length - 1) {
    const summary = pastSteps
      .map(([step, result]) => `- ${step}: ${result}`)
      .join("\n");

    return {
      response: `Trip Planning Complete!\n\nSummary:\n${summary}`,
    };
  }

  // Otherwise, increment the step index for the next loop
  return {
    currentStepIndex: currentStepIndex + 1,
  };
};

// ============================================================================
// 5. GRAPH CONSTRUCTION
// ============================================================================

/**
 * Constructs the LangGraph workflow.
 */
function createWorkflow() {
  const workflow = new StateGraph(GraphState)
    // Define Nodes
    .addNode("planner", plannerNode)
    .addNode("executor", executorNode)
    .addNode("reflector", reflectNode)
    // Define Edges
    .addEdge(START, "planner") // Start -> Planner
    .addEdge("planner", "executor") // Planner -> Executor

    // Conditional Edge: Reflector -> Executor OR End
    .addConditionalEdges(
      "reflector",
      (state: typeof GraphState.State) => {
        // Logic: If we have a response (meaning we finished), go to END.
        // Otherwise, loop back to executor.
        if (state.response && state.response.length > 0) {
          return END;
        }
        return "executor";
      }
    );

  return workflow.compile();
}

// ============================================================================
// 6. EXECUTION (SIMULATED SERVER ACTION)
// ============================================================================

/**
 * Main entry point.
 * Simulates a Server Action handling a request from a Next.js frontend.
 */
async function runTripPlanner() {
  const app = createWorkflow();

  // Initial user input
  const initialInput = {
    // Note: In a real app, we might pass the user query here and let the planner parse it.
    // For this simple example, the planner ignores input and returns a fixed plan.
  };

  console.log("🚀 Starting Plan-and-Execute Workflow...\n");

  // Stream execution events
  const stream = await app.stream(initialInput);

  // Process stream for visualization
  for await (const step of stream) {
    const nodeName = Object.keys(step)[0];
    const state = step[nodeName];

    // Log current state snapshot
    if (state.plan?.length > 0) {
      console.log(`   -> Plan updated: [${state.plan.join(", ")}]`);
    }
    if (state.pastSteps?.length > 0) {
      const lastStep = state.pastSteps[state.pastSteps.length - 1];
      console.log(`   -> Result: ${lastStep[1]}`);
    }
    if (state.response) {
      console.log(`   -> Final Response: ${state.response}`);
    }
    console.log(""); // Empty line for readability
  }
}

// Execute the function
runTripPlanner().catch(console.error);

Detailed Line-by-Line Explanation

1. Imports and State Definition

  • @langchain/core/graphs: We import the necessary classes to build the state graph. StateGraph is the main container, Annotation defines the state shape, and END/START are special symbols defining entry and exit points.
  • GraphState: This is the "memory" of our agent.
    • plan: Stores the array of steps. The reducer is set to update (overwrite) because the planner generates the entire plan at once.
    • pastSteps: Uses a reducer that appends ([...state, update]). This creates an immutable history of what has been executed, which is crucial for generating the final summary.
    • currentStepIndex: Tracks where we are in the plan. We increment this manually to drive the loop.

2. The Tool Simulation

  • executeTool: In a real SaaS app, this function would likely be a Server Action (in Next.js) or an API call. It accepts a step description and returns a result. We simulate asynchronous behavior using setTimeout to mimic network latency.

3. The Nodes

  • plannerNode:
    • This is the first logical block.
    • It takes the current state (though in this simple version, it doesn't strictly need the input state).
    • It returns an object { plan: [...] }. LangGraph automatically merges this into the global state.
  • executorNode:
    • It reads currentStepIndex and plan from the state.
    • It performs a bounds check (if (currentStepIndex >= plan.length)). This is a critical safety mechanism to prevent the agent from hallucinating steps out of bounds.
    • It calls executeTool and returns the result, which gets appended to pastSteps.
  • reflectNode:
    • This node checks the progress.
    • If currentStepIndex is the last item in the plan, it generates the final response string.
    • If not, it simply updates currentStepIndex (incrementing it by 1) to prepare for the next loop iteration.

4. Graph Construction

  • new StateGraph(GraphState): Initializes the graph with our defined state schema.
  • .addNode("name", func): Registers the functions we defined above as callable nodes.
  • .addEdge(START, "planner"): Defines the entry point. When the graph starts, it immediately jumps to the planner node.
  • .addEdge("planner", "executor"): A deterministic edge. After planning, we always execute.
  • .addConditionalEdges("reflector", ...): This is the core of the control flow.
    • The reflector node returns the state.
    • The predicate function inspects state.response.
    • Logic: If a final response exists (meaning we finished the loop), return END. Otherwise, return "executor" to loop back.

5. Execution

  • app.stream(initialInput): Instead of just running app.invoke(), we use stream. This allows us to observe the state changes at every node transition, which is excellent for debugging and UI updates in a web app.
  • The Loop: We iterate over the stream. Each step yields an object where the key is the node name and the value is the state after that node ran.

Common Pitfalls

1. State Mutation (The "Reference" Trap)

Issue: In JavaScript, objects are passed by reference. If you modify the state directly inside a node (e.g., state.plan.push("new step")), you will corrupt the graph's history and potentially cause infinite loops or unpredictable behavior. Solution: Always return a new object or rely on LangGraph's reducers. In the example, we use spread operators (...state) or return new objects entirely. This ensures immutability.

2. Async/Await Loops in Conditional Edges

Issue: LangGraph's addConditionalEdges predicate function must be synchronous. You cannot make the predicate function async. Wrong:

.addConditionalEdges("node", async (state) => { 
  const check = await someAsyncCheck(state); // ERROR
  return check ? "next" : END;
});
Correct: Perform all asynchronous logic inside the Node itself. The node should update the state with the result of the async operation, and the conditional edge should simply read that synchronous state property.

3. Vercel/AWS Lambda Timeouts

Issue: In a Serverless environment (like Vercel), execution has strict timeouts (e.g., 10 seconds on the Hobby plan). If your executeTool function involves heavy computation or long network requests, the workflow will fail. Solution: * Offload Heavy Work: For long-running tasks (e.g., video processing), trigger a background job in the executor node and return immediately with a "Job Started" status, rather than waiting for completion. * Optimize Streaming: Use stream instead of invoke to flush partial responses to the client before the full execution finishes, keeping the connection alive.

4. Hallucinated JSON in Tool Calls

Issue: If you were using an LLM inside the plannerNode or executorNode to generate structured data (like arguments for a tool), models can sometimes return malformed JSON or text. Solution: Always use Structured Output (JSON mode) or tool calling schemas provided by the model provider. Do not rely on parsing raw text strings with regex, as this is brittle. In our example, we bypassed this by hardcoding the plan, but in a real LLM integration, you must validate the schema before proceeding.

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.