When implementing the subagents pattern, you’ll make several key design choices. This table summarizes the options—each is covered in detail in the sections below.
Subagent execution can be
synchronous
(blocking) or
asynchronous
(background). Your choice depends on whether the main agent needs the result to continue.
Mode
Main agent behavior
Best for
Tradeoff
Sync
Waits for subagent to complete
Main agent needs result to continue
Simple, but blocks the conversation
Async
Continues while subagent runs in background
Independent tasks, user shouldn’t wait
Responsive, but more complex
Not to be confused with Python’s
async
/
await
. Here, “async” means the main agent kicks off a background job (typically in a separate process or service) and continues without blocking.
By default, subagent calls are
synchronous
—the main agent waits for each subagent to complete before continuing. Use sync when the main agent’s next action depends on the subagent’s result.
When to use sync:
Main agent needs the subagent’s result to formulate its response
Tasks have order dependencies (e.g., fetch data → analyze → respond)
Subagent failures should block the main agent’s response
Tradeoffs:
Simple implementation—just call and wait
User sees no response until all subagents complete
Use
asynchronous execution
when the subagent’s work is independent—the main agent doesn’t need the result to continue conversing with the user. The main agent kicks off a background job and remains responsive.
When to use async:
Subagent work is independent of the main conversation flow
Users should be able to continue chatting while work happens
You want to run multiple independent tasks in parallel
Three-tool pattern:
Start job
: Kicks off the background task, returns a job ID
Check status
: Returns current state (pending, running, completed, failed)
Get result
: Retrieves the completed result
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.”
The key idea is wrapping subagents as tools that the main agent can call:
Copy
import { createAgent, tool } from "langchain";import * as z from "zod";// Create a sub-agentconst subagent = createAgent({...}); // Wrap it as a toolconst callSubagent = tool( async ({ query }) => { 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 toolconst mainAgent = createAgent({ model, tools: [callSubagent] });
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
for fine-grained control.
An alternative approach uses a single parameterized tool to invoke ephemeral sub-agents for independent tasks. Unlike the
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.
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.
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
(loading agent information on-demand via tools)
An interesting aspect of this approach is that sub-agents may have the exact same capabilities as the main agent. In such cases, invoking 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.
Agent registry with task dispatcher
Copy
import { tool, createAgent } from "langchain";import * as z from "zod";// Sub-agents developed by different teamsconst 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-agentsconst 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 invoke"), description: z .string() .describe("Task description"), }), });// Main coordinator agentconst 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." ),});
The
names
and
descriptions
associated with subagents are the primary way the main agent knows which subagents to invoke.
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.
For the
single dispatch tool
design, the main agent needs to call the
task
tool with the name of the subagent to invoke. The available tools can be provided to the main agent via one of the following methods:
System prompt enumeration
: List available agents in the system prompt.
Enum constraint on dispatch tool
: For small agent lists, add an enum to the
agent_name
field.
Tool-based discovery
: For large or dynamic agent registries, provide a separate tool (e.g.,
list_agents
or
search_agents
) that returns available agents.
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.
Subagent inputs example
Copy
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", });
Customize what the main agent receives back so it can make good decisions. Two strategies:
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.
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
.
Subagent outputs example
Copy
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") }) });