Subagents
Added in: @mastra/core@1.8.0
Subagents are specialized agents that another agent can delegate tasks to. Add them to the parent agent's agents property, then call Agent.stream() or Agent.generate(). The parent agent uses its instructions and each subagent's description to decide when and how to delegate tasks.
When to use subagentsDirect link to When to use subagents
Use subagents when a task requires agents with different specializations to work together. The parent agent decides when to delegate and passes context to each subagent. It then synthesizes their results.
Common use cases:
- Research and writing workflows where one agent gathers data and another produces content
- Multi-step tasks that need different expertise at each stage
- Tasks where you need fine-grained control over delegation behavior
A parent agent that coordinates subagents is often called a supervisor. The supervisor pattern is one approach to building multi-agent systems in Mastra. For other patterns, read the conceptual overview.
QuickstartDirect link to Quickstart
Define subagents with clear descriptions, then add them to a parent agent:
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'
const researchAgent = new Agent({
id: 'research-agent',
description: 'Gathers factual information and returns bullet-point summaries.',
model: 'openai/gpt-5-mini',
})
const writingAgent = new Agent({
id: 'writing-agent',
description: 'Transforms research into well-structured articles.',
model: 'openai/gpt-5-mini',
})
const parentAgent = new Agent({
id: 'parent-agent',
instructions: `You coordinate research and writing using specialized agents.
Delegate to research-agent for facts, then writing-agent for content.`,
model: 'openai/gpt-5.6-sol',
agents: { researchAgent, writingAgent },
memory: new Memory({
storage: new LibSQLStore({ id: 'storage', url: 'file:mastra.db' }),
}),
})
const stream = await parentAgent.stream('Research AI in education and write an article', {
maxSteps: 10,
})
for await (const chunk of stream.textStream) {
process.stdout.write(chunk)
}
Delegation hooksDirect link to Delegation hooks
Delegation hooks let you intercept, modify, or reject delegations as they happen. Configure them under the delegation option, either in the agent's defaultOptions or per-call.
onDelegationStartDirect link to ondelegationstart
Called before the parent agent delegates to a subagent. Return an object to control the delegation:
proceed: true: Allow the delegation (default behavior)proceed: false: Reject the delegation with arejectionReasonmodifiedPrompt: Rewrite the prompt sent to the subagentmodifiedMaxSteps: Limit the subagent's iteration count
const stream = await parentAgent.stream('Research AI trends', {
maxSteps: 10,
delegation: {
onDelegationStart: async context => {
console.log(`Delegating to: ${context.primitiveId}`)
// Modify the prompt for a specific agent
if (context.primitiveId === 'research-agent') {
return {
proceed: true,
modifiedPrompt: `${context.prompt}\n\nFocus on 2024-2025 data.`,
modifiedMaxSteps: 5,
}
}
// Reject delegation after too many iterations
if (context.iteration > 8) {
return {
proceed: false,
rejectionReason: 'Max iterations reached. Synthesize current findings.',
}
}
return { proceed: true }
},
},
})
The context object includes:
| Property | Description |
|---|---|
primitiveId | The ID of the subagent being delegated to |
prompt | The prompt the parent agent is sending |
iteration | Current iteration number |
onDelegationCompleteDirect link to ondelegationcomplete
Called after a delegation finishes. Use it to inspect results or provide feedback, or alternatively stop execution:
context.bail(): Stop the parent agent's loop immediately- Return
{ feedback: '...' }: Add feedback that gets saved to the parent agent's memory and is visible to subsequent iterations
const stream = await parentAgent.stream('Research AI trends', {
maxSteps: 10,
delegation: {
onDelegationComplete: async context => {
console.log(`Completed: ${context.primitiveId}`)
// Bail on errors
if (context.error) {
context.bail()
return {
feedback: `Delegation to ${context.primitiveId} failed: ${context.error}. Try a different approach.`,
}
}
},
},
})
The context object includes:
| Property | Description |
|---|---|
primitiveId | The ID of the subagent that ran |
result | The subagent's response |
error | Error if the delegation failed |
bail() | Function to stop the parent agent's loop |
Message filteringDirect link to Message filtering
By default, subagents receive the full conversation context from the parent agent. Use messageFilter to control what messages are shared, for example, to remove sensitive data or limit context size.
const stream = await parentAgent.stream('Research AI trends', {
maxSteps: 10,
delegation: {
messageFilter: ({ messages, primitiveId, prompt }) => {
// Remove messages containing sensitive data
return messages
.filter(msg => {
const content =
typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
return !content.includes('confidential')
})
.slice(-10) // Only pass the last 10 messages
},
},
})
The callback receives messages (the full conversation history), primitiveId (the subagent ID), and prompt (the delegation prompt). Return the filtered array of messages.
Subagent result contextDirect link to Subagent result context
When a subagent completes, the parent agent's model receives the subagent's text response in later iterations. Nested tool calls and subagent metadata, such as thread and resource IDs, aren't added to the parent agent's model context.
Application code and UI integrations can still inspect subAgentToolResults and the rest of the raw delegation result in the tool result payload.
This keeps debugging and display data available without sending nested tool arguments or outputs back into the parent agent's next model call.
Set includeSubAgentToolResultsInModelContext to include the full subagent result, including nested tool results and subagent metadata, in the parent agent's model context.
await parentAgent.generate('Research AI trends', {
delegation: {
includeSubAgentToolResultsInModelContext: true,
},
})
Iteration monitoringDirect link to Iteration monitoring
onIterationComplete is called after each iteration of the parent agent's loop. Use it to monitor execution or guide the next iteration. You can also stop execution early.
const stream = await parentAgent.stream('Research AI trends', {
maxSteps: 10,
onIterationComplete: async context => {
console.log(`Iteration ${context.iteration}/${context.maxIterations}`)
console.log(`Finish reason: ${context.finishReason}`)
// Inject feedback to guide the agent
if (!context.text.includes('recommendations')) {
return {
continue: true,
feedback: 'Please include specific recommendations in your analysis.',
}
}
// Stop early when the response is sufficient
if (context.text.length > 1000 && context.finishReason === 'stop') {
return { continue: false }
}
return { continue: true }
},
})
Return { continue: true } to keep iterating, or { continue: false } to stop. Include optional feedback to inject guidance into the conversation. When feedback is combined with continue: false, the model may get one final turn to produce a text response incorporating the feedback, but only if the current iteration is still active (e.g., after tool calls), otherwise no extra turn is granted.
Memory isolationDirect link to Memory isolation
Mastra isolates subagent memory during delegation. Subagents receive the full conversation context for better decision-making, but only their specific delegation prompt and response are saved to their memory.
How it works:
- Full context forwarded: When the parent agent delegates, the subagent receives all messages from the parent agent's conversation
- Scoped memory saves: Only the delegation prompt and the subagent's response are saved to the subagent's memory
- Fresh thread per invocation: Each delegation uses a unique thread ID, ensuring clean separation
As a result, subagents have the context they need without cluttering their memory with the parent agent's entire conversation. Visit memory in multi-agent systems for more details.
Tool approval propagationDirect link to Tool approval propagation
Tool approvals propagate through the delegation chain. When a subagent uses a tool with requireApproval: true or calls suspend(), the approval request surfaces in the parent agent's stream.
const sensitiveDataTool = createTool({
id: 'get-user-data',
requireApproval: true,
execute: async input => {
return await database.getUserData(input.userId)
},
})
const dataAgent = new Agent({
id: 'data-agent',
tools: { sensitiveDataTool },
})
const parentAgent = new Agent({
id: 'parent-agent',
agents: { dataAgent },
memory: new Memory(),
})
const stream = await parentAgent.stream('Get data for user 123')
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
console.log('Tool requires approval:', chunk.payload.toolName)
}
}
CancellationDirect link to Cancellation
When you pass an abortSignal to the parent agent's stream() or generate() call, Mastra forwards that same signal to delegated subagents. Calling AbortController.abort() cancels in-flight subagent runs at their next step instead of letting them run to completion.
const controller = new AbortController()
const stream = await parentAgent.stream('Research AI trends', {
abortSignal: controller.signal,
})
// Cancel the parent agent and any in-flight subagents
controller.abort()
Task completion scoringDirect link to Task completion scoring
Agents don't always produce a complete, correct output on the first try. Task completion scorers can help by validating whether the task is complete after each iteration. If validation fails, the parent agent continues iterating. Feedback from failed scorers is included in the conversation context so subagents can see what was missing.
import { createScorer } from '@mastra/core/evals'
const taskCompleteScorer = createScorer({
id: 'task-complete',
name: 'Task Completeness',
}).generateScore(async context => {
const text = (context.run.output || '').toString()
const hasAnalysis = text.includes('analysis')
const hasRecommendations = text.includes('recommendation')
return hasAnalysis && hasRecommendations ? 1 : 0
})
const stream = await parentAgent.stream('Research AI in education', {
maxSteps: 10,
isTaskComplete: {
scorers: [taskCompleteScorer],
strategy: 'all',
onComplete: async result => {
console.log('Task complete:', result.complete)
},
},
})
Rubric scorerDirect link to Rubric scorer
The built-in rubric scorer lets you define what "correct" looks like as a checklist and have the agent self-evaluate and iterate until every criterion is satisfied or maxSteps is reached.
It works as an LLM-as-judge scorer. After each iteration, a separate grader model reviews the agent's output against the rubric. The loop ends when every required criterion passes. A failed criterion adds its feedback to the conversation so the agent can try again.
This is most effective for tasks with clear, verifiable success criteria. You can use it like so:
import { Agent } from '@mastra/core/agent'
import { createRubricScorer } from '@mastra/evals/scorers/prebuilt'
const parentAgent = new Agent({
id: 'parent-agent',
instructions: 'You coordinate research and writing using specialized agents.',
model: 'openai/gpt-5.6-sol',
agents: { researchAgent, writingAgent },
})
const rubricScorer = createRubricScorer({
model: 'openai/gpt-5-mini',
criteria: [
{ description: 'The response includes an analysis section' },
{ description: 'The response includes concrete recommendations' },
],
})
const stream = await parentAgent.stream('Research AI in education', {
maxSteps: 10,
isTaskComplete: {
scorers: [rubricScorer],
strategy: 'all',
},
})
For full API details, see the rubric scorer reference.
Writing effective instructionsDirect link to Writing effective instructions
Clear instructions are essential for effective delegation.
The parent agent's instructions should specify the available resources and when to use each one. They should also define coordination behavior and success criteria.
Each subagent should have a clear description that explains its purpose and return format, including when the parent agent should use it.
The parent agent uses these descriptions to make delegation decisions.
const parentAgent = new Agent({
id: 'parent-agent',
instructions: `You coordinate research and writing tasks.
Available resources:
- researchAgent: Gathers factual data and sources (returns bullet points)
- writingAgent: Transforms research into narrative content (returns full paragraphs)
Delegation strategy:
1. For research requests: Delegate to researchAgent first
2. For writing requests: Delegate to writingAgent
3. For complex requests: Delegate to researchAgent first, then writingAgent
Success criteria:
- All user questions are fully answered
- Response is well-formatted and complete`,
agents: { researchAgent, writingAgent },
})
Running subagents in the backgroundDirect link to Running subagents in the background
Subagent invocations are dispatched as tool calls, so they can run as background tasks. This is useful when one or more delegations are long-running and you don't want them to block the parent agent's response.
Enable the backgroundTasks manager on the Mastra instance, then opt subagents in on the parent agent:
const parentAgent = new Agent({
id: 'parent-agent',
instructions: 'Coordinate research and writing using the available agents.',
model: 'openai/gpt-5.6-sol',
agents: { researchAgent, writingAgent },
backgroundTasks: {
tools: {
researchAgent: { enabled: true, timeoutMs: 900_000 },
writingAgent: { enabled: true, timeoutMs: 900_000 },
},
},
})
const stream = await parentAgent.streamUntilIdle('Research AI in education and write an article', {
memory: { thread: 't1', resource: 'u1' },
})
Use streamUntilIdle() instead of stream() so the stream stays open until the subagents complete and the parent agent has had a chance to respond to their results.
If a subagent isn't listed under the parent agent's backgroundTasks.tools but has its own background-eligible tools, the parent agent still dispatches the subagent as a background task and inherits its config. See Inheriting from the subagent for details.
Subagent versioningDirect link to Subagent versioning
When using the editor, you can control which stored version of each subagent the parent agent uses at runtime. Set version overrides on the Mastra instance or per invocation:
const result = await parentAgent.generate('Research and write about AI safety', {
versions: {
agents: {
'research-agent': { status: 'published' },
'writing-agent': { versionId: 'draft-456' },
},
},
})
Version overrides propagate automatically through delegation. See Subagent versioning for details on resolution order and server API usage.