> ## Documentation Index
> Fetch the complete documentation index at: https://langchain-5e9cc07a-preview-srtues-1765892940-6d4394f.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-agent

Multi-agent systems break complex applications into coordinated components. **Importantly, "multi-agent" doesn't necessarily mean multiple distinct agents** — a single agent with dynamic behavior can achieve similar capabilities.

## Why multi-agent?

When developers say they need "multi-agent," they're usually looking for one or more of these capabilities:

<Columns cols={2}>
  <Card title="Context management" icon="brain">
    Surface relevant knowledge without overwhelming the context window. Different tasks (agents) need different context.
  </Card>

  <Card title="Distributed development" icon="users">
    Let different teams develop and maintain capabilities independently with clear boundaries.
  </Card>

  <Card title="Parallelization" icon="code-branch">
    Spawn specialized workers for subtasks and execute them concurrently for faster results.
  </Card>

  <Card title="Sequential constraints" icon="list-ol">
    Enforce step-by-step workflows. Unlock tools and actions only after preconditions are met.
  </Card>
</Columns>

Multi-agent patterns are particularly valuable when a single agent has too many [tools](/oss/javascript/langchain/tools) and makes poor decisions about which to use, when tasks require specialized knowledge with extensive context (long prompts and domain-specific tools), or when you need to enforce sequential constraints that unlock capabilities only after certain conditions are met.

<Tip>
  At the center of multi-agent design is **[context engineering](/oss/javascript/langchain/context-engineering)**—deciding what information each agent sees. The quality of your system depends on ensuring each agent has access to the right data for its task.
</Tip>

## Patterns

Here are the main patterns for building multi-agent systems, each suited to different use cases:

| Pattern                                 | How it works                                                                                                                                                                                                    |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [**Subagents**](#subagents)             | A main agent coordinates subagents and background jobs as tools. Centralized control — all routing passes through the main agent. Multiple coordination approaches available.                                   |
| [**Handoffs**](#handoffs)               | Behavior changes dynamically based on state. Tool calls update a state variable, and the system adjusts behavior accordingly. Supports both handoffs between distinct agents and dynamic configuration changes. |
| [**Skills**](#skills)                   | Specialized prompts loaded on-demand. The main agent stays in control and gains additional context as needed.                                                                                                   |
| [**Router**](#router)                   | A routing step classifies input and directs it to one or more specialized agents. Results are collected and returned to the user.                                                                               |
| [**Custom workflow**](#custom-workflow) | Build bespoke logic with LangGraph, mixing deterministic and agentic steps. Reuse or customize agents as needed.                                                                                                |

**Tool calling** is the primary coordination mechanism across all patterns. Tools can:

* Invoke sub-agents ([subagents](#subagents))
* Update state to trigger routing or configuration changes ([handoffs](#handoffs))
* Load context on-demand ([skills](#skills))
* Invoke entire multi-agent systems (wrapping a [router](#router) as a tool)

### Choosing a pattern

Use this table to match your requirements to the right pattern:

| Pattern                     | Distributed development | Parallelization | Multi-hop | Direct user interaction |
| --------------------------- | :---------------------: | :-------------: | :-------: | :---------------------: |
| [**Subagents**](#subagents) |           ⭐⭐⭐           |       ⭐⭐⭐       |    ⭐⭐⭐    |            —            |
| [**Handoffs**](#handoffs)   |            —            |        —        |    ⭐⭐⭐    |           ⭐⭐⭐           |
| [**Skills**](#skills)       |           ⭐⭐⭐           |        ⭐⭐       |    ⭐⭐⭐    |           ⭐⭐⭐           |
| [**Router**](#router)       |            ⭐⭐           |       ⭐⭐⭐       |     —     |            ⭐⭐           |

* **Distributed development**: Can different teams maintain components independently?
* **Parallelization**: Can multiple agents execute concurrently?
* **Multi-hop**: Does the pattern support multiple hops between agents?
* **Direct user interaction**: Can subagents converse directly with the user?

<Tip>
  You can mix patterns! For example, a **subagents** pattern can manage **workflow** sub-graphs or use the **router** pattern as a tool (querying multiple knowledge bases in parallel, then synthesizing results). A **state machine** can invoke **skills** at specific stages (loading specialized context only when reaching certain steps). The **[one tool for all agents](#one-tool-for-all-agents-spawning)** approach can work within a **custom workflow** to parallelize independent tasks while maintaining deterministic overall structure.
</Tip>

## Subagents

In the **subagents** architecture, a central main agent (often referred to as a **supervisor**) coordinates subagents by calling them as tools. The main agent decides which subagent to invoke, what input to provide, and how to combine results. Subagents are stateless—they don't remember past interactions, with all conversation memory maintained by the main agent. This provides context isolation: each subagent invocation works in a clean context window, preventing context bloat in the main conversation.

**Key characteristics:**

* Centralized control: All routing passes through the main agent
* No direct user interaction: Subagents return results to the main agent, not the user
* Subagents via tools: Subagents are invoked via tools
* Parallel execution: The main agent can invoke multiple subagents in a single turn

Use the subagents pattern when you have multiple distinct domains (e.g., calendar, email, CRM, database), subagents don't need to converse directly with users, or you want centralized workflow control. For simpler cases with just a few tools, use a single agent.

<Card title="Tutorial: Build an agent with subagents" icon="sitemap" href="/oss/javascript/langchain/supervisor" arrow cta="Learn more">
  Learn how to build a personal assistant using the subagents pattern, where a central main agent (supervisor) coordinates specialized worker agents.
</Card>

### Sync vs async

By default, subagent calls are **synchronous**—the main agent waits for each subagent to complete before continuing. This is simple and works well for most cases.

For **long-running tasks** (reviewing contracts, conducting research, auditing code), use **asynchronous execution**. The main agent kicks off a background job and continues conversing with the user while the work completes.

<Accordion title="Background job pattern">
  ```mermaid theme={null}
  sequenceDiagram
      participant User
      participant Main Agent
      participant Job System
      participant Contract Reviewer

      User->>Main Agent: "Review this M&A contract"
      Main Agent->>Job System: run_agent("legal_reviewer", task)
      Job System->>Contract Reviewer: Start agent
      Job System-->>Main Agent: job_id: "job_123"
      Main Agent-->>User: "Started review (job_123)"

      Note over Contract Reviewer: Reviewing 150+ pages<br/>(may take minutes)

      User->>Main Agent: "What's the status?"
      Main Agent->>Job System: check_status(job_id)
      Job System-->>Main Agent: "running"
      Main Agent-->>User: "Still reviewing contract..."

      Note over Contract Reviewer: Review completes

      User->>Main Agent: "Is it done yet?"
      Main Agent->>Job System: check_status(job_id)
      Job System-->>Main Agent: "completed"
      Main Agent->>Job System: get_result(job_id)
      Job System-->>Main Agent: Contract analysis
      Main Agent-->>User: "Review complete: [findings]"
  ```

  **Key characteristics:**

  * Three-tool pattern: Kick off job (returns job ID), check status, get results
  * Asynchronous execution: Work proceeds in the background while main agent remains responsive
  * User-initiated checks: Main agent checks job status when the user asks, not on a polling schedule

  **Handling job completion:** When a job finishes, your application needs to notify the user. One approach: surface a notification that, when clicked, sends a `HumanMessage` like "Check job\_123 and summarize the results."
</Accordion>

### Tool patterns

There are two main ways to expose subagents as tools:

| Pattern                                           | Best for                                                      | Trade-off                                         |
| ------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------- |
| [**Tool per agent**](#tool-per-agent)             | Fine-grained control over each subagent's input/output        | More setup, but more customization                |
| [**Single dispatch tool**](#single-dispatch-tool) | Many agents, distributed teams, convention over configuration | Simpler composition, less per-agent customization |

#### Tool per agent

```mermaid theme={null}
graph LR
    A[User] --> B[Main Agent]
    B --> C[Subagent A]
    B --> D[Subagent B]
    B --> E[Subagent C]
    C --> B
    D --> B
    E --> B
    B --> F[User response]
```

The key idea is wrapping subagents as tools that the main agent can call:

```typescript theme={null}
import { createAgent, tool } from "langchain";
import * as z from "zod";

// Create a sub-agent
const subagent = createAgent({...});  // [!code highlight]

// Wrap it as a tool  // [!code highlight]
const callSubagent = tool(  // [!code highlight]
  async ({ query }) => {  // [!code highlight]
    const result = await subagent.invoke({
      messages: [{ role: "user", content: query }]
    });
    return result.messages.at(-1)?.text;
  },
  {
    name: "subagent_name",
    description: "subagent_description",
    schema: z.object({
      query: z.string().describe("The query to send to subagent")
    })
  }
);

// Main agent with subagent as a tool  // [!code highlight]
const mainAgent = createAgent({ model, tools: [callSubagent] });  // [!code highlight]
```

The main agent invokes the subagent tool when it decides the task matches the subagent's description, receives the result, and continues orchestration. See [Context engineering](#context-engineering) for fine-grained control.

#### Single dispatch tool

An alternative approach uses a single parameterized tool to spawn ephemeral sub-agents for independent tasks. Unlike the [tool per agent](#tool-per-agent) approach where each sub-agent is wrapped as a separate tool, this uses a convention-based approach with a single `task` tool: the task description is passed as a human message to the sub-agent, and the sub-agent's final message is returned as the tool result.

```mermaid theme={null}
graph LR
    A[User] --> B[Main Agent]
    B --> C{task<br/>agent_name, description}
    C -->|research| D[Research Agent]
    C -->|writer| E[Writer Agent]
    C -->|reviewer| F[Reviewer Agent]
    D --> C
    E --> C
    F --> C
    C --> B
    B --> G[User response]
```

**Key characteristics:**

* Single task tool: One parameterized tool that can invoke any registered sub-agent by name
* Convention-based invocation: Agent selected by name, task passed as human message, final message returned as tool result
* Team distribution: Different teams can develop and deploy agents independently
* Agent discovery: Sub-agents can be discovered via system prompt (listing available agents) or through [progressive disclosure](/oss/javascript/langchain/progressive-disclosure-skills) (loading agent information on-demand via tools)

Use this approach when you want to distribute agent development across multiple teams, need to isolate complex tasks into separate context windows, need a scalable way to add new agents without modifying the coordinator, or prefer convention over customization. This approach trades flexibility in context engineering for simplicity in agent composition and strong context isolation.

<Tip>
  An interesting aspect of this approach is that sub-agents may have the exact same capabilities as the main agent. In such cases, spawning a sub-agent is **really about context isolation** as the primary reason—allowing complex, multi-step tasks to run in isolated context windows without bloating the main agent's conversation history. The sub-agent completes its work autonomously and returns only a concise summary, keeping the main thread focused and efficient.
</Tip>

<Accordion title="Agent registry with task dispatcher">
  ```typescript theme={null}
  import { tool, createAgent } from "langchain";
  import * as z from "zod";

  // Sub-agents developed by different teams
  const researchAgent = createAgent({
    model: "gpt-4o",
    prompt: "You are a research specialist...",
  });

  const writerAgent = createAgent({
    model: "gpt-4o",
    prompt: "You are a writing specialist...",
  });

  // Registry of available sub-agents
  const SUBAGENTS = {
    research: researchAgent,
    writer: writerAgent,
  };

  const task = tool(
    async ({ agentName, description }) => {
      const agent = SUBAGENTS[agentName];
      const result = await agent.invoke({
        messages: [
          { role: "user", content: description }
        ],
      });
      return result.messages.at(-1)?.content;
    },
    {
      name: "task",
      description: `Launch an ephemeral subagent.

  Available agents:
  - research: Research and fact-finding
  - writer: Content creation and editing`,
      schema: z.object({
        agentName: z
          .string()
          .describe("Name of agent to spawn"),
        description: z
          .string()
          .describe("Task description"),
      }),
    }
  );

  // Main coordinator agent
  const mainAgent = createAgent({
    model: "gpt-4o",
    tools: [task],
    prompt: (
      "You coordinate specialized sub-agents. " +
      "Available: research (fact-finding), " +
      "writer (content creation). " +
      "Use the task tool to delegate work."
    ),
  });
  ```
</Accordion>

### Context engineering

Control how context flows between the main agent and its subagents:

| Category                                  | Purpose                                                  | Impacts                      |
| ----------------------------------------- | -------------------------------------------------------- | ---------------------------- |
| [**Subagent specs**](#subagent-specs)     | Ensure subagents are invoked when they should be         | Main agent routing decisions |
| [**Subagent inputs**](#subagent-inputs)   | Ensure subagents can execute well with optimized context | Subagent performance         |
| [**Subagent outputs**](#subagent-outputs) | Ensure the supervisor can act on subagent results        | Main agent performance       |

#### Subagent specs

The **name** and **description** you give a subagent tool determine when the main agent decides to invoke it. These are prompting levers—choose them carefully.

* **Name**: How the main agent refers to the sub-agent. Keep it clear and action-oriented (e.g., `research_agent`, `code_reviewer`).
* **Description**: What the main agent knows about the sub-agent's capabilities. Be specific about what tasks it handles and when to use it.

#### Subagent inputs

Customize what context the subagent receives to execute its task. Add input that isn't practical to capture in a static prompt—full message history, prior results, or task metadata—by pulling from the agent's state.

<Accordion title="Subagent inputs">
  ```typescript theme={null}
  import { createAgent, tool, AgentState, ToolMessage } from "langchain";
  import { Command } from "@langchain/langgraph";
  import * as z from "zod";

  // Example of passing the full conversation history to the sub agent via the state.
  const callSubagent1 = tool(
    async ({query}) => {
      const state = getCurrentTaskInput<AgentState>();
      // Apply any logic needed to transform the messages into a suitable input
      const subAgentInput = someLogic(query, state.messages);
      const result = await subagent1.invoke({
        messages: subAgentInput,
        // You could also pass other state keys here as needed.
        // Make sure to define these in both the main and subagent's
        // state schemas.
        exampleStateKey: state.exampleStateKey
      });
      return result.messages.at(-1)?.content;
    },
    {
      name: "subagent1_name",
      description: "subagent1_description",
    }
  );
  ```
</Accordion>

#### Subagent outputs

Customize what the main agent receives back so it can make good decisions. Two strategies:

1. **Prompt the sub-agent**: Specify exactly what should be returned. A common failure mode is that the sub-agent performs tool calls or reasoning but doesn't include results in its final message—remind it that the supervisor only sees the final output.
2. **Format in code**: Adjust or enrich the response before returning it. For example, pass specific state keys back in addition to the final text using a `Command`.

<Accordion title="Subagent outputs">
  ```typescript theme={null}
  import { tool, ToolMessage } from "langchain";
  import { Command } from "@langchain/langgraph";
  import * as z from "zod";

  const callSubagent1 = tool(
    async ({ query }, config) => {
      const result = await subagent1.invoke({
        messages: [{ role: "user", content: query }]
      });

      // Return a Command to update multiple state keys
      return new Command({
        update: {
          // Pass back additional state from the subagent
          exampleStateKey: result.exampleStateKey,
          messages: [
            new ToolMessage({
              content: result.messages.at(-1)?.text,
              tool_call_id: config.toolCall?.id!
            })
          ]
        }
      });
    },
    {
      name: "subagent1_name",
      description: "subagent1_description",
      schema: z.object({
        query: z.string().describe("The query to send to subagent1")
      })
    }
  );
  ```
</Accordion>

## Handoffs

In the **handoffs** architecture, behavior changes dynamically based on state. The core mechanism: tools update a state variable (e.g., `current_step` or `active_agent`) that persists across turns, and the system reads this variable to adjust behavior—either applying different configuration (system prompt, tools) or routing to a different agent. This pattern supports both handoffs between distinct agents and dynamic configuration changes within a single agent.

<Tip>
  The term **handoffs** was coined by [OpenAI](https://openai.github.io/openai-agents-python/handoffs/) for using tool calls (e.g., `transfer_to_sales_agent`) to transfer control between agents or states.
</Tip>

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Agent
    participant Workflow State

    User->>Agent: "My phone is broken"
    Note over Agent,Workflow State: Step: Get warranty status<br/>Tools: record_warranty_status
    Agent-->>User: "Is your device under warranty?"

    User->>Agent: "Yes, it's still under warranty"
    Agent->>Workflow State: record_warranty_status("in_warranty")
    Note over Agent,Workflow State: Step: Classify issue<br/>Tools: record_issue_type
    Agent-->>User: "Can you describe the issue?"

    User->>Agent: "The screen is cracked"
    Agent->>Workflow State: record_issue_type("hardware")
    Note over Agent,Workflow State: Step: Provide resolution<br/>Tools: provide_solution, escalate_to_human
    Agent-->>User: "Here's the warranty repair process..."
```

**Key characteristics:**

* State-driven behavior: Behavior changes based on a state variable (e.g., `current_step` or `active_agent`)
* Tool-based transitions: Tools update the state variable to move between states
* Direct user interaction: Each state's configuration handles user messages directly
* Persistent state: State survives across conversation turns

Use the handoffs pattern when you need to enforce sequential constraints (unlock capabilities only after preconditions are met), the agent needs to converse directly with the user across different states, or you're building multi-stage conversational flows. This pattern is particularly valuable for customer support scenarios where you need to collect information in a specific sequence — for example, collecting a warranty ID before processing a refund.

<Card title="Tutorial: Build a customer support agent using handoffs" icon="people-arrows" href="/oss/javascript/langchain/customer-support-handoffs" arrow cta="Learn more">
  Learn how to build a customer support agent using the handoffs pattern, where a single agent transitions between different configurations.
</Card>

There are two ways to implement handoffs: **[single agent with middleware](#single-agent-with-middleware)** (one agent with dynamic configuration) or **[multiple agent subgraphs](#multiple-agent-subgraphs)** (distinct agents as graph nodes).

### Single agent with middleware

A single agent changes its behavior based on state. Middleware intercepts each model call and dynamically adjusts the system prompt and available tools. Tools update the state variable to trigger transitions:

```typescript theme={null}
import { tool } from "@langchain/core/tools";
import { Command } from "@langchain/langgraph";
import { ToolMessage } from "@langchain/core/messages";
import { z } from "zod";

const recordWarrantyStatus = tool(
  async ({ status }, config) => {
    return new Command({
      update: {
        messages: [
          new ToolMessage({
            content: `Warranty status recorded: ${status}`,
            tool_call_id: config.toolCall?.id!
          })
        ],
        warrantyStatus: status,
        currentStep: "specialist"  // Update state to trigger transition
      }
    });
  },
  {
    name: "record_warranty_status",
    description: "Record warranty status and transition to next step.",
    schema: z.object({
      status: z.string()
    })
  }
);
```

<Accordion title="Complete example: Customer support with middleware">
  ```typescript theme={null}
  import { tool, createAgent, AgentState } from "langchain";
  import { wrapModelCall, ModelRequest, ModelResponse } from "langchain/agents/middleware";
  import { Command } from "@langchain/langgraph";
  import { ToolMessage } from "langchain/messages";
  import * as z from "zod";

  // 1. Define state with current_step tracker
  const SupportState = z.object({  // [!code highlight]
    currentStep: z.string().default("triage"),  // [!code highlight]
    warrantyStatus: z.string().optional()
  });

  // 2. Tools update currentStep via Command
  const recordWarrantyStatus = tool(
    async ({ status }, config) => {
      return new Command({  // [!code highlight]
        update: {  // [!code highlight]
          messages: [  // [!code highlight]
            new ToolMessage({
              content: `Warranty status recorded: ${status}`,
              tool_call_id: config.toolCall?.id!
            })
          ],
          warrantyStatus: status,
          currentStep: "specialist"  // Transition to next step  // [!code highlight]
        }
      });
    },
    {
      name: "record_warranty_status",
      description: "Record warranty status and transition",
      schema: z.object({ status: z.string() })
    }
  );

  // 3. Middleware applies dynamic configuration based on currentStep
  const applyStepConfig = wrapModelCall(  // [!code highlight]
    async (request: ModelRequest, handler) => {
      const step = request.state.currentStep || "triage";  // [!code highlight]

      // Map steps to their configurations
      const configs = {
        triage: {
          prompt: "Collect warranty information...",
          tools: [recordWarrantyStatus]
        },
        specialist: {
          prompt: `Provide solutions based on warranty: ${request.state.warrantyStatus}`,
          tools: [provideSolution, escalate]
        }
      };

      const config = configs[step];
      request = request.override({  // [!code highlight]
        systemPrompt: config.prompt,  // [!code highlight]
        tools: config.tools  // [!code highlight]
      });
      return handler(request);
    }
  );

  // 4. Create agent with middleware
  const agent = createAgent({
    model,
    tools: [recordWarrantyStatus, provideSolution, escalate],
    stateSchema: SupportState,
    middleware: [applyStepConfig],  // [!code highlight]
    checkpointer: new InMemorySaver()  // Persist state across turns  // [!code highlight]
  });
  ```
</Accordion>

### Multiple agent subgraphs

Multiple distinct agents exist as separate nodes in a graph. Handoff tools navigate between agent nodes using `Command.PARENT` to specify which node to execute next:

```typescript theme={null}
const transferToSales = tool(
  async () => {
    return new Command({
      goto: "sales_agent",  // Navigate to the sales agent node
      update: { activeAgent: "sales_agent" },
      graph: Command.PARENT  // Navigate in parent graph
    });
  },
  {
    name: "transfer_to_sales",
    description: "Transfer to the sales agent.",
    schema: z.object({})
  }
);
```

<Accordion title="Complete example: Sales and support with handoffs">
  This example shows a multi-agent system with separate sales and support agents. Each agent is a separate graph node, and handoff tools allow agents to transfer conversations to each other.

  ```typescript theme={null}
  import { StateGraph, START, MessagesAnnotation, Annotation } from "@langchain/langgraph";
  import { createAgent } from "langchain";
  import { tool } from "@langchain/core/tools";
  import { Command } from "@langchain/langgraph";
  import { z } from "zod";

  // 1. Define state with active_agent tracker
  const MultiAgentState = Annotation.Root({
    ...MessagesAnnotation.spec,
    activeAgent: Annotation<string>({
      reducer: (x, y) => y ?? x,
      default: () => "sales_agent"
    })
  });

  // 2. Create handoff tools
  const transferToSales = tool(
    async () => {
      return new Command({
        goto: "sales_agent",
        update: { activeAgent: "sales_agent" },
        graph: Command.PARENT
      });
    },
    {
      name: "transfer_to_sales",
      description: "Transfer to the sales agent.",
      schema: z.object({})
    }
  );

  const transferToSupport = tool(
    async () => {
      return new Command({
        goto: "support_agent",
        update: { activeAgent: "support_agent" },
        graph: Command.PARENT
      });
    },
    {
      name: "transfer_to_support",
      description: "Transfer to the support agent.",
      schema: z.object({})
    }
  );

  // 3. Create agents with handoff tools
  const salesAgent = createAgent({
    llm: model,
    tools: [transferToSupport],
    prompt: "You are a sales agent. Help with sales inquiries."
  });

  const supportAgent = createAgent({
    llm: model,
    tools: [transferToSales],
    prompt: "You are a support agent. Help with technical issues."
  });

  // 4. Create agent nodes that invoke the agents
  const callSalesAgent = async (state: typeof MultiAgentState.State) => {
    const response = await salesAgent.invoke(state);
    return response;
  };

  const callSupportAgent = async (state: typeof MultiAgentState.State) => {
    const response = await supportAgent.invoke(state);
    return response;
  };

  // 5. Create router function
  const routeToAgent = (state: typeof MultiAgentState.State): string => {
    return state.activeAgent;
  };

  // 6. Build the graph
  const builder = new StateGraph(MultiAgentState)
    .addNode("sales_agent", callSalesAgent)
    .addNode("support_agent", callSupportAgent);

  // Start with conditional routing based on initial activeAgent
  builder.addConditionalEdges(
    START,
    routeToAgent,
    ["sales_agent", "support_agent"]
  );

  // After each agent, route to the active agent (enables handoffs)
  builder.addConditionalEdges(
    "sales_agent",
    routeToAgent,
    ["sales_agent", "support_agent"]
  );
  builder.addConditionalEdges(
    "support_agent",
    routeToAgent,
    ["sales_agent", "support_agent"]
  );

  const graph = builder.compile();
  ```
</Accordion>

<Tip>
  Use **single agent with middleware** for most handoffs use cases—it's simpler. Only use **multiple agent subgraphs** when you need bespoke agent implementations (e.g., a node that's itself a complex graph with reflection or retrieval steps).
</Tip>

**Implementation considerations:**

* **Conversation history**: Decide what conversation history each agent/state receives—full history, filtered portions, or summaries.
* **Tool semantics**: Clarify whether handoff tools only update routing state or also perform actions (e.g., should `transfer_to_sales()` also create a ticket?).

## Skills

In the **skills** architecture, specialized capabilities are packaged as invokable "skills" that augment an agent's behavior. Skills are primarily prompt-driven specializations that an agent can invoke on-demand.

```mermaid theme={null}
graph LR
    A[User] --> B[Agent]
    B --> C[Python skill]
    B --> D[SQL skill]
    B --> E[Docs skill]
    B --> A
```

**Key characteristics:**

* Prompt-driven specialization: Skills are primarily defined by specialized prompts
* Progressive disclosure: Skills become available based on context or user needs
* Team distribution: Different teams can develop and maintain skills independently
* Lightweight composition: Skills are simpler than full sub-agents

Use the skills pattern when you want a single agent with many possible specializations, you don't need to enforce specific constraints between skills, or different teams need to develop capabilities independently. Common examples include coding assistants (skills for different languages or tasks), knowledge bases (skills for different domains), and creative assistants (skills for different formats).

<Tip>
  This pattern is conceptually identical to [llms.txt](https://llmstxt.org/) (introduced by Jeremy Howard), which uses tool calling for progressive disclosure of documentation. The skills pattern applies the same approach to specialized prompts and domain knowledge rather than just documentation pages.
</Tip>

<Accordion title="Loading skills on-demand">
  ```typescript theme={null}
  import { tool, createAgent } from "langchain";
  import * as z from "zod";

  const loadSkill = tool(
    async ({ skillName }) => {
      // Load skill content from file/database
      return "";
    },
    {
      name: "load_skill",
      description: `Load a specialized skill.

  Available skills:
  - write_sql: SQL query writing expert
  - review_legal_doc: Legal document reviewer

  Returns the skill's prompt and context.`,
      schema: z.object({
        skillName: z
          .string()
          .describe("Name of skill to load")
      })
    }
  );

  const agent = createAgent({
    model: "gpt-4o",
    tools: [loadSkill],
    prompt: (
      "You are a helpful assistant. " +
      "You have access to two skills: " +
      "write_sql and review_legal_doc. " +
      "Use load_skill to access them."
    ),
  });
  ```
</Accordion>

### Extending the pattern

When writing custom implementations, you can extend the basic skills pattern in several ways:

**Dynamic tool registration**: Combine progressive disclosure with state management to register new tools as skills load. For example, loading a "database\_admin" skill could both add specialized context and register database-specific tools (backup, restore, migrate). This uses the same tool-and-state mechanisms used across multi-agent patterns—tools updating state to dynamically change agent capabilities.

**Hierarchical skills**: Skills can define other skills in a tree structure, creating nested specializations. For instance, loading a "data\_science" skill might make available sub-skills like "pandas\_expert", "visualization", and "statistical\_analysis". Each sub-skill can be loaded independently as needed, allowing for fine-grained progressive disclosure of domain knowledge. This hierarchical approach helps manage large knowledge bases by organizing capabilities into logical groupings that can be discovered and loaded on-demand.

<Card title="Tutorial: Build an agent with on-demand skill loading" icon="wand-magic-sparkles" href="/oss/javascript/langchain/progressive-disclosure-skills" arrow cta="Learn more">
  Learn how to implement skills with progressive disclosure, where the agent loads specialized prompts and schemas on-demand rather than upfront.
</Card>

## Router

In the **router** architecture, a routing step classifies input and directs it to specialized agents. This is useful when you have distinct **verticals**—separate knowledge domains that each require their own agent.

```mermaid theme={null}
graph LR
    A([Query]) --> B[Router]
    B --> C[Agent A]
    B --> D[Agent B]
    B --> E[Agent C]
    C --> F[Synthesize]
    D --> F
    E --> F
    F --> G([Combined answer])
```

Key characteristics:

* Router decomposes the query
* Zero or more specialized agents are invoked in parallel
* Results are synthesized into a coherent response

Two approaches:

* [**Stateless routers**](#stateless) address each request independently
* [**Stateful routers**](#stateful) maintain conversation history across requests

### Stateless

Each request is routed independently—no memory between calls. For multi-turn conversations, see [Stateful routers](#stateful).

<Tip>
  **Stateless router vs Subagents**: The [subagents pattern](#subagents) can also route to multiple agents. Use the stateless router when you need specialized preprocessing or custom routing logic. Use the subagents pattern when you want the LLM to decide which agents to call dynamically.
</Tip>

<Accordion title="Building a multi-source knowledge base router">
  Your organization's knowledge lives in multiple places: GitHub repositories, Notion wikis, and Slack conversations. These are three distinct verticals, each requiring specialized tools and context. When users ask questions like "How do I authenticate API requests?", the answer may require information from multiple sources. This example builds a router that decomposes queries, identifies which verticals to consult, queries them in parallel, and synthesizes results.

  ```mermaid theme={null}
  graph LR
      A([Query]) --> B[Router]
      B --> C[GitHub agent]
      B --> D[Notion agent]
      B --> E[Slack agent]
      C --> F[Synthesize]
      D --> F
      E --> F
      F --> G([Combined answer])
  ```

  ```typescript theme={null}
  import { StateGraph, Annotation, START, END, Send } from "@langchain/langgraph";
  import { createAgent } from "langchain";
  import { ChatOpenAI } from "@langchain/openai";

  const RouterState = Annotation.Root({
    query: Annotation<string>(),
    routes: Annotation<string[]>(),
    githubResult: Annotation<string | null>(),
    notionResult: Annotation<string | null>(),
    slackResult: Annotation<string | null>(),
    finalAnswer: Annotation<string>()
  });

  // Specialized agents for each vertical
  const githubAgent = createAgent({
    model: "openai:gpt-4o",
    tools: [searchCode, searchIssues, searchPrs],
    prompt: "You are a GitHub expert. Answer questions about code, API references, and implementation details.",
    name: "github_agent"
  });

  const notionAgent = createAgent({
    model: "openai:gpt-4o",
    tools: [searchNotion, getPage],
    prompt: "You are a Notion expert. Answer questions about internal processes, policies, and team documentation.",
    name: "notion_agent"
  });

  const slackAgent = createAgent({
    model: "openai:gpt-4o",
    tools: [searchSlack, getThread],
    prompt: "You are a Slack expert. Answer questions by searching relevant threads and discussions.",
    name: "slack_agent"
  });

  const routerLlm = new ChatOpenAI({ model: "gpt-4o-mini" });

  async function decomposeQuery(state: typeof RouterState.State) {  // [!code highlight]
    // Decompose query and determine which knowledge bases to consult  // [!code highlight]
    const response = await routerLlm.invoke([
      {
        role: "system",
        content: "Analyze this query and determine which knowledge bases to consult. Return a JSON list with one or more of: 'github', 'notion', 'slack'."
      },
      { role: "user", content: state.query }
    ]);
    // Parse LLM response to get routes (simplified for example)
    const routes = ["github", "notion"];
    return { routes };
  }

  // Route to multiple agents in parallel  // [!code highlight]
  function routeToAgents(state: typeof RouterState.State): Send[] {  // [!code highlight]
    // Fan out to multiple agents in parallel  // [!code highlight]
    return state.routes.map(route => new Send(route, state));  // [!code highlight]
  }

  async function queryGithub(state: typeof RouterState.State) {
    const result = await githubAgent.invoke({
      messages: [{ role: "user", content: state.query }]
    });
    return { githubResult: result.messages.at(-1)?.content };
  }

  async function queryNotion(state: typeof RouterState.State) {
    const result = await notionAgent.invoke({
      messages: [{ role: "user", content: state.query }]
    });
    return { notionResult: result.messages.at(-1)?.content };
  }

  async function querySlack(state: typeof RouterState.State) {
    const result = await slackAgent.invoke({
      messages: [{ role: "user", content: state.query }]
    });
    return { slackResult: result.messages.at(-1)?.content };
  }

  async function synthesizeResults(state: typeof RouterState.State) {  // [!code highlight]
    // Combine results from multiple agents into a coherent answer  // [!code highlight]
    const results: string[] = [];
    if (state.githubResult) results.push(`GitHub: ${state.githubResult}`);
    if (state.notionResult) results.push(`Notion: ${state.notionResult}`);
    if (state.slackResult) results.push(`Slack: ${state.slackResult}`);

    // Use LLM to synthesize
    const synthesisResponse = await routerLlm.invoke([
      { role: "system", content: "Synthesize these search results into a coherent answer." },
      { role: "user", content: results.join("\n\n") }
    ]);
    return { finalAnswer: synthesisResponse.content };
  }

  // Build workflow with parallel execution
  const workflow = new StateGraph(RouterState)
    .addNode("decompose", decomposeQuery)
    .addNode("github", queryGithub)
    .addNode("notion", queryNotion)
    .addNode("slack", querySlack)
    .addNode("synthesize", synthesizeResults)
    .addEdge(START, "decompose")
    .addConditionalEdges("decompose", routeToAgents, ["github", "notion", "slack"])  // [!code highlight]
    .addEdge("github", "synthesize")
    .addEdge("notion", "synthesize")
    .addEdge("slack", "synthesize")
    .addEdge("synthesize", END)
    .compile();

  const result = await workflow.invoke({ query: "How do I authenticate API requests?" });
  console.log(result.finalAnswer);
  ```
</Accordion>

### Stateful

For multi-turn conversations, you need to maintain context across invocations.

#### Tool wrapper

The simplest approach: wrap the stateless router as a tool that a conversational agent can call. The conversational agent handles memory and context; the router stays stateless. This avoids the complexity of managing conversation history across multiple parallel agents.

```typescript theme={null}
const searchDocs = tool(
  async ({ query }) => {
    const result = await workflow.invoke({ query });
    return result.finalAnswer;
  },
  {
    name: "search_docs",
    description: "Search across multiple documentation sources",
    schema: z.object({
      query: z.string().describe("The search query")
    })
  }
);

// Conversational agent uses the router as a tool
const conversationalAgent = createAgent({
  model,
  tools: [searchDocs],
  prompt: "You are a helpful assistant. Use search_docs to answer questions."
});
```

#### Full persistence

If you need the router itself to maintain state, use [persistence](/oss/javascript/langchain/short-term-memory) to store message history. When routing to an agent, fetch previous messages from state and selectively include them in the agent's context—this is a lever for [context engineering](/oss/javascript/langchain/context-engineering).

<Warning>
  **Stateful routers require custom history management.** If the router switches between agents across turns, conversations may not feel fluid to end users when agents have different tones or prompts. With parallel invocation, you'll need to maintain history at the router level (inputs and synthesized outputs) and leverage this history in routing logic. Consider the [handoffs pattern](#handoffs) or [subagents pattern](#subagents) instead—both provide clearer semantics for multi-turn conversations.
</Warning>

## Custom workflow

In the **custom workflow** architecture, you define your own bespoke execution flow using LangGraph. You have complete control over the graph structure—including sequential steps, conditional branches, loops, and parallel execution.

Use custom workflows when:

* Standard patterns (subagents, skills, etc.) don't fit your requirements
* You need to mix deterministic logic with agentic behavior
* Your use case requires complex routing or multi-stage processing

Each node in your workflow can be a simple function, an LLM call, or an entire agent with tools. You can also compose other architectures within a custom workflow—for example, embedding a multi-agent system as a single node.

```mermaid theme={null}
graph LR
    A([Input]) --> B{{Conditional}}
    B -->|path_a| C[Deterministic step]
    B -->|path_b| D((Agentic step))
    C --> G([Output])
    D --> G([Output])
```

The [router pattern](#router) is an example of a custom workflow.

<Tip>
  **Calling a LangChain agent from a LangGraph node**: The main insight when mixing LangChain and LangGraph is that you can call a LangChain agent directly inside any LangGraph node. This lets you combine the flexibility of custom workflows with the convenience of pre-built agents:

  ```typescript theme={null}
  import { createAgent } from "langchain";

  const agent = createAgent({ model: "openai:gpt-4o", tools: [...] });

  async function agentNode(state: typeof State.State) {
    // A LangGraph node that invokes a LangChain agent
    const result = await agent.invoke({
      messages: [{ role: "user", content: state.query }]
    });
    return { answer: result.messages.at(-1)?.content };
  }
  ```
</Tip>

**Example: RAG pipeline** — A common use case is combining retrieval with an agent. This example builds a WNBA stats assistant that retrieves from a knowledge base and can fetch live news.

<Accordion title="Custom RAG workflow">
  The workflow demonstrates three types of nodes:

  * **Model node** (Rewrite): Rewrites the user query for better retrieval using structured output.
  * **Deterministic node** (Retrieve): Performs vector similarity search — no LLM involved.
  * **Agent node** (Agent): Reasons over retrieved context and can fetch additional information via tools.

  ```mermaid theme={null}
  graph LR
      A([Query]) --> B{{Rewrite}}
      B --> C[(Retrieve)]
      C --> D((Agent))
      D --> E([Response])
  ```

  <Tip>
    You can use LangGraph state to pass information between workflow steps. This allows each part of your workflow to read and update structured fields, making it easy to share data and context across nodes.
  </Tip>

  ```typescript theme={null}
  import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
  import { createAgent, tool } from "langchain";
  import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
  import { MemoryVectorStore } from "langchain/vectorstores/memory";
  import * as z from "zod";

  const State = Annotation.Root({
    question: Annotation<string>(),
    rewrittenQuery: Annotation<string>(),
    documents: Annotation<string[]>(),
    answer: Annotation<string>(),
  });

  // WNBA knowledge base with rosters, game results, and player stats
  const embeddings = new OpenAIEmbeddings();
  const vectorStore = await MemoryVectorStore.fromTexts(
    [
      // Rosters
      "New York Liberty 2024 roster: Breanna Stewart, Sabrina Ionescu, Jonquel Jones, Courtney Vandersloot.",
      "Las Vegas Aces 2024 roster: A'ja Wilson, Kelsey Plum, Jackie Young, Chelsea Gray.",
      "Indiana Fever 2024 roster: Caitlin Clark, Aliyah Boston, Kelsey Mitchell, NaLyssa Smith.",
      // Game results
      "2024 WNBA Finals: New York Liberty defeated Minnesota Lynx 3-2 to win the championship.",
      "June 15, 2024: Indiana Fever 85, Chicago Sky 79. Caitlin Clark had 23 points and 8 assists.",
      "August 20, 2024: Las Vegas Aces 92, Phoenix Mercury 84. A'ja Wilson scored 35 points.",
      // Player stats
      "A'ja Wilson 2024 season stats: 26.9 PPG, 11.9 RPG, 2.6 BPG. Won MVP award.",
      "Caitlin Clark 2024 rookie stats: 19.2 PPG, 8.4 APG, 5.7 RPG. Won Rookie of the Year.",
      "Breanna Stewart 2024 stats: 20.4 PPG, 8.5 RPG, 3.5 APG.",
    ],
    [{}, {}, {}, {}, {}, {}, {}, {}, {}],
    embeddings
  );
  const retriever = vectorStore.asRetriever({ k: 5 });

  const getLatestNews = tool(
    async ({ query }) => {
      // Your news API here
      return "Latest: The WNBA announced expanded playoff format for 2025...";
    },
    {
      name: "get_latest_news",
      description: "Get the latest WNBA news and updates",
      schema: z.object({ query: z.string() })
    }
  );

  const agent = createAgent({
    model: "openai:gpt-4o",
    tools: [getLatestNews],
  });

  const model = new ChatOpenAI({ model: "gpt-4o" });

  const RewrittenQuery = z.object({ query: z.string() });

  async function rewriteQuery(state: typeof State.State) {
    const systemPrompt = `Rewrite this query to retrieve relevant WNBA information.
  The knowledge base contains: team rosters, game results with scores, and player statistics (PPG, RPG, APG).
  Focus on specific player names, team names, or stat categories mentioned.`;
    const response = await model.withStructuredOutput(RewrittenQuery).invoke([
      { role: "system", content: systemPrompt },
      { role: "user", content: state.question }
    ]);
    return { rewrittenQuery: response.query };
  }

  async function retrieve(state: typeof State.State) {
    const docs = await retriever.invoke(state.rewrittenQuery);
    return { documents: docs.map(doc => doc.pageContent) };
  }

  async function callAgent(state: typeof State.State) {
    const context = state.documents.join("\n\n");
    const prompt = `Context:\n${context}\n\nQuestion: ${state.question}`;
    const response = await agent.invoke({ messages: [{ role: "user", content: prompt }] });
    return { answer: response.messages.at(-1)?.contentBlocks };
  }

  const workflow = new StateGraph(State)
    .addNode("rewrite", rewriteQuery)
    .addNode("retrieve", retrieve)
    .addNode("agent", callAgent)
    .addEdge(START, "rewrite")
    .addEdge("rewrite", "retrieve")
    .addEdge("retrieve", "agent")
    .addEdge("agent", END)
    .compile();

  const result = await workflow.invoke({ question: "Who won the 2024 WNBA Championship?" });
  console.log(result.answer);
  ```
</Accordion>

***

<Callout icon="pen-to-square" iconType="regular">
  [Edit the source of this page on GitHub.](https://github.com/langchain-ai/docs/edit/main/src/oss/langchain/multi-agent.mdx)
</Callout>

<Tip icon="terminal" iconType="regular">
  [Connect these docs programmatically](/use-these-docs) to Claude, VSCode, and more via MCP for real-time answers.
</Tip>
