Agent class
The Agent class is the foundation for creating AI agents in Mastra. It provides methods for generating responses, streaming interactions, and handling voice capabilities.
Usage examplesDirect link to Usage examples
Basic string instructionsDirect link to Basic string instructions
Passing instructions as a string or array of strings is the simplest way to set up an agent. This is useful for straightforward use cases where you need to provide a prompt without additional configuration.
import { Agent } from '@mastra/core/agent'
// String instructions
export const agent = new Agent({
id: 'test-agent',
name: 'Test Agent',
instructions: 'You are a helpful assistant that provides concise answers.',
model: 'openai/gpt-5.5',
})
// System message object
export const agent2 = new Agent({
id: 'test-agent-2',
name: 'Test Agent 2',
instructions: {
role: 'system',
content: 'You are an expert programmer',
},
model: 'openai/gpt-5.5',
})
// Array of system messages
export const agent3 = new Agent({
id: 'test-agent-3',
name: 'Test Agent 3',
instructions: [
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'system', content: 'You have expertise in TypeScript' },
],
model: 'openai/gpt-5.5',
})
Provider-specific configurationsDirect link to Provider-specific configurations
Each model provider also enables a few different options, including prompt caching and configuring reasoning. You can set providerOptions on the instruction level to set different caching strategy per system instruction/prompt.
import { Agent } from '@mastra/core/agent'
export const agent = new Agent({
id: 'core-message-agent',
name: 'Core Message Agent',
instructions: {
role: 'system',
content: 'You are a helpful assistant specialized in technical documentation.',
providerOptions: {
openai: {
reasoningEffort: 'low',
},
},
},
model: 'openai/gpt-5.5',
})
Mixed instruction formatsDirect link to Mixed instruction formats
import { Agent } from '@mastra/core/agent'
// This could be customizable based on the user
const preferredTone = {
role: 'system',
content: 'Always maintain a professional and empathetic tone.',
}
export const agent = new Agent({
id: 'multi-message-agent',
name: 'Multi Message Agent',
instructions: [
{ role: 'system', content: 'You are a customer service representative.' },
preferredTone,
{
role: 'system',
content: 'Escalate complex issues to human agents when needed.',
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
},
],
model: 'anthropic/claude-sonnet-4-6',
})
Thread signalsDirect link to Thread signals
Use Agent signals to send real-time input and context into a memory thread. Message APIs are for user-authored input. sendSignal() is the lower-level API for system-generated context.
When the target thread is running, sendMessage() delivers the message into the active agent loop. When the thread is idle, Mastra starts a stream with the message as the first input by default.
const subscription = await agent.subscribeToThread({
resourceId: 'user-123',
threadId: 'thread-abc',
})
void (async () => {
for await (const chunk of subscription.stream) {
console.log(chunk)
}
})()
agent.sendMessage('Use the latest customer note too.', {
resourceId: 'user-123',
threadId: 'thread-abc',
ifIdle: {
streamOptions: {
maxSteps: 3,
},
},
})
Use attributes to identify different users in a shared thread. The attributes are rendered as XML so the model can distinguish who said what:
agent.sendMessage(
{
contents: 'Can we simplify the API surface?',
attributes: { name: 'Devin', from: 'slack' },
},
{ resourceId: 'user-123', threadId: 'thread-abc' },
)
The model receives this as:
<user name="Devin" from="slack">Can we simplify the API surface?</user>
Use ifActive.attributes and ifIdle.attributes when the message should carry different context depending on whether the thread is currently running:
agent.sendMessage(
{
contents: 'Also cover the edge cases.',
attributes: { source: 'chat' },
},
{
resourceId: 'user-123',
threadId: 'thread-abc',
ifActive: { attributes: { delivery: 'while-active' } },
ifIdle: { attributes: { delivery: 'new-message' } },
},
)
When the thread is active, the model sees:
<user source="chat" delivery="while-active">Also cover the edge cases.</user>
When the thread is idle, the model sees:
<user source="chat" delivery="new-message">Also cover the edge cases.</user>
The UI sees the message contents and can also read attributes and metadata off the signal message for custom rendering (e.g. showing user names, avatars, or platform badges).
sendMessage(message, options)Direct link to sendmessagemessage-options
Sends a user message to an active run or memory thread. Use this when the active agent should receive the message immediately.
message:
attributes are present, Mastra renders the message as a <user> XML element with the attributes included.options?:
runId?:
resourceId?:
threadId for thread-targeted messages.threadId?:
resourceId for thread-targeted messages.ifActive?:
behavior?:
deliver.attributes?:
ifIdle?:
behavior?:
wake.streamOptions?:
ifIdle.behavior is wake. Mastra uses the top-level resourceId and threadId for memory context.attributes?:
Set ifIdle.behavior to wake and pass ifIdle.streamOptions when an idle thread should start a new stream with custom execution options:
agent.sendMessage('Continue with the next step.', {
resourceId: 'user-123',
threadId: 'thread-abc',
ifIdle: {
behavior: 'wake',
streamOptions: {
maxSteps: 3,
},
},
})
Returns { accepted: Promise<SendAgentSignalAccepted>, signal: CreatedAgentSignal, persisted?: Promise<void> }. accepted resolves at decision-time, once Mastra decides what to do with the message: { action: 'wake', runId, output } when this process runs the agent (it started or won the lease to start the run), { action: 'deliver', runId } when the message is forwarded onto an existing run (including when this process loses a cross-process wake race), or { action: 'persist' } / { action: 'discard' } when nothing ran. runId is the authoritative id of the run that handled the message and is present only on wake and deliver; for persist/discard use result.signal.id to correlate the stored message. accepted resolves for routing — a generation error on a wake run surfaces through output.consumeStream() — and rejects only when the message couldn't be routed or started at all (e.g. a misconfigured agent). persisted is only present for persist behavior and resolves when Mastra finishes writing the message to memory. On the wake action, output is the agent stream for in-process consumption.
queueMessage(message, options)Direct link to queuemessagemessage-options
Queues a user message for the next turn on a thread. If the thread is active, Mastra waits for the active run to finish, then starts a new run with the queued message. If the thread is idle, Mastra starts a run immediately.
agent.queueMessage('Also check whether the tests need updates.', {
resourceId: 'user-123',
threadId: 'thread-abc',
})
queueMessage() accepts the same message and options shape as sendMessage() and returns { accepted: Promise<SendAgentSignalAccepted>, signal: CreatedAgentSignal, persisted?: Promise<void> }, with the same accepted semantics as sendMessage().
sendSignal(signal, options)Direct link to sendsignalsignal-options
Sends a signal to an active run or memory thread.
signal:
type is the semantic signal category. tagName controls the XML tag the model sees. For example, { type: 'notification', tagName: 'github-review' } renders as <github-review>...</github-review>. Legacy user-message and system-reminder payloads are still accepted and normalized. Unknown type values are rejected; use tagName for custom XML tags.options?:
runId?:
resourceId?:
threadId for thread-targeted signals.threadId?:
resourceId for thread-targeted signals.ifActive?:
behavior?:
deliver.attributes?:
ifIdle?:
behavior?:
wake.streamOptions?:
ifIdle.behavior is wake. Mastra uses the top-level resourceId and threadId for memory context.attributes?:
Returns { accepted: Promise<SendAgentSignalAccepted>, signal: CreatedAgentSignal, persisted?: Promise<void> }. accepted resolves at decision-time, once Mastra decides what to do with the signal: { action: 'wake', runId, output } when this process runs the agent (it started or won the lease to start the run), { action: 'deliver', runId } when the signal is forwarded onto an existing run (including when this process loses a cross-process wake race), or { action: 'persist' } / { action: 'discard' } when nothing ran. action mirrors the winning behavior from ifActive/ifIdle. runId is the authoritative id of the run that handled the signal and is present only on wake and deliver; for persist/discard use result.signal.id to correlate the stored signal. accepted resolves for routing — a generation error on a wake run surfaces through output.consumeStream() — and rejects only when the signal couldn't be routed or started at all (e.g. a misconfigured agent). persisted is only present for persist behavior and resolves when Mastra finishes writing the signal to memory. On the wake action, output is the agent stream for in-process consumption.
In serverless handlers, await accepted and pass the wake output to your platform's waitUntil equivalent so the winning process can drain the stream after the HTTP response returns.
const result = agent.sendSignal(signal, { resourceId, threadId })
ctx.waitUntil(
result.accepted.then(async accepted => {
if (accepted.action === 'wake') {
await accepted.output.consumeStream()
}
}),
)
sendStateSignal(state, options)Direct link to sendstatesignalstate-options
Sends named, thread-scoped state context to an active run or memory thread. Use this when an external producer owns durable context that changes over time, such as browser state, editor state, or watcher output.
const result = await agent.sendStateSignal(
{
id: 'browser',
mode: 'snapshot',
cacheKey: 'browser:https://example.com:3-tabs',
contents: 'Browser is open. Active tab URL: https://example.com. 3 open tabs.',
value: {
activeUrl: 'https://example.com',
tabCount: 3,
open: true,
},
},
{
resourceId: 'user-123',
threadId: 'thread-abc',
},
)
state:
id:
browser or editor.cacheKey:
contents:
mode?:
snapshot.value?:
mode: 'snapshot'.delta?:
mode: 'delta'.attributes?:
metadata?:
tagName?:
state.options:
sendSignal().Returns { accepted: Promise<SendAgentSignalAccepted>, signal: CreatedAgentSignal, persisted?: Promise<void>, skipped?: false } when Mastra accepts new state. Returns { skipped: true, reason: 'unchanged' } when the same cacheKey and mode are already current for the state lane. accepted resolves at decision-time, once Mastra decides what to do with the signal: { action: 'wake', runId, output } when this process runs the agent (it started or won the lease to start the run), { action: 'deliver', runId } when the signal is forwarded onto an existing run (including when this process loses a cross-process wake race), or { action: 'persist' } / { action: 'discard' } when nothing ran. runId is the authoritative id of the run that handled the signal and is present only on wake and deliver; for persist/discard use result.signal.id to correlate the stored signal. On the wake action, output is the agent stream for in-process consumption.
sendNotificationSignal(notification, options)Direct link to sendnotificationsignalnotification-options
Creates or coalesces a notification inbox record, resolves the notification delivery policy, and sends a notification signal when the decision is immediate.
const result = await agent.sendNotificationSignal(
{
source: 'github',
kind: 'ci-status',
priority: 'high',
summary: 'CI failed on main: 3 tests failed.',
dedupeKey: 'github:acme/app:main:ci',
},
{
resourceId: 'user-123',
threadId: 'thread-abc',
},
)
notification:
source:
github, slack, or email.kind:
ci-status, mention, or direct-message.summary:
priority?:
medium.payload?:
dedupeKey?:
coalesceKey?:
attributes?:
metadata?:
options:
resourceId:
threadId:
ifIdle?:
streamOptions?:
Returns { record: NotificationRecord, decision: NotificationDeliveryDecision, runId?: string, signal?: CreatedAgentSignal, persisted?: Promise<void>, accepted?: Promise<SendAgentSignalAccepted> }. record is the stored inbox record. decision is the delivery-policy result. signal and runId are present when ingress emits a signal immediately, including the immediate summary emitted for active high-priority notifications. persisted is present when the emitted signal is persisted without waking an idle thread. accepted is present when a signal is emitted and resolves at decision-time, once Mastra decides what to do with it: { action: 'wake', runId, output } when this process runs the agent (it started or won the lease to start the run), { action: 'deliver', runId } when the signal is forwarded onto an existing run, or { action: 'persist' } / { action: 'discard' } when nothing ran. runId on the accepted result is present only on wake and deliver. On the wake action, output is the agent stream for in-process consumption.
Default delivery is priority-aware. urgent notifications deliver immediately. high notifications deliver immediately when the thread is idle; when the thread is active, Mastra emits a summary immediately and keeps deliverAt for later full delivery when the thread is idle. medium notifications deliver immediately when idle and batch into summaries when active. low notifications batch into summaries in both active and idle threads; idle low-priority summaries reach subscribers without waking the model loop. For the full flow, visit Signals.
Configure notifications.deliveryPolicy on the agent when some notifications should wait for a different dispatch window or summary rollup:
export const supportAgent = new Agent({
id: 'support-agent',
name: 'Support Agent',
instructions: 'Help the user triage updates.',
model: 'openai/gpt-5.5',
notifications: {
deliveryPolicy: {
priorities: {
urgent: 'deliver',
},
decide: ({ record }) => {
if (record.priority === 'low') {
return {
action: 'summarize',
summaryAt: new Date(Date.now() + 30 * 60 * 1000),
}
}
},
},
},
})
subscribeToThread(options)Direct link to subscribetothreadoptions
Subscribes to raw stream chunks for a memory thread. Use this before calling sendMessage(), queueMessage(), or sendSignal() when you need to render stream output, observe signal echoes, or abort the active run.
options:
resourceId?:
threadId:
Returns an AgentThreadSubscription object with these members:
stream:
activeRunId:
null when no run is active.abort:
true when a run was aborted.unsubscribe:
Constructor parametersDirect link to Constructor parameters
id:
name:
description?:
metadata?:
instructions:
model:
agents?:
tools?:
hooks?:
generate() or stream() override matching hooks set here. See Tool hooks below.beforeToolCall?:
{ toolName, input, context, metadata }. Return { proceed: false, output } to skip the tool call and use output as its result.afterToolCall?:
{ toolName, input, context, metadata, output, error }. output is undefined when the tool throws, and error is set instead.transform?:
transform on createTool() for tool-local rules.workflows?:
defaultOptions?:
stream() and generate().defaultGenerateOptionsLegacy?:
generateLegacy().defaultStreamOptionsLegacy?:
streamLegacy().mastra?:
scorers?:
memory?:
notifications?:
deliveryPolicy?:
decide() function.voice?:
inputProcessors?:
createWorkflow() using ProcessorStepSchema.outputProcessors?:
maxProcessorRetries?:
requestContextSchema?:
editor?:
generate() memory optionsDirect link to generate-memory-options
Pass memory when you call agent.generate() to choose which conversation thread the run should read from and write to. The common shape is memory: { resource: string, thread: string }, where resource identifies the owner and thread identifies the conversation. See Threads and resources for the concept model.
const response = await agent.generate('What did we decide about retries?', {
memory: {
resource: 'user-123',
thread: 'support-thread-456',
},
})
Use a thread object when you need to create or update thread metadata during the call:
const response = await agent.generate('Continue the support conversation.', {
memory: {
resource: 'user-123',
thread: {
id: 'support-thread-456',
title: 'Billing support',
metadata: { category: 'billing' },
},
},
})
Tool hooksDirect link to Tool hooks
Use hooks to run logic around every tool call the agent makes, including assigned tools, memory tools, toolsets, client tools, and workspace tools.
import { Agent } from '@mastra/core/agent'
export const agent = new Agent({
id: 'support-agent',
name: 'support-agent',
instructions: 'Help users with their questions.',
model: 'openai/gpt-5.5',
hooks: {
beforeToolCall: ({ toolName, input }) => {
console.log(`Running ${toolName}`, input)
},
afterToolCall: ({ toolName, output, error }) => {
console.log(`Finished ${toolName}`, { output, error })
},
},
})
beforeToolCall can short-circuit the tool call by returning { proceed: false, output }. The agent skips execution and uses output as the tool result:
const result = await agent.generate('Clean up old records', {
hooks: {
beforeToolCall: ({ toolName }) => {
if (toolName === 'deleteRecord') {
return { proceed: false, output: { blocked: true } }
}
},
},
})
The hook context metadata includes agentId and agentName. Per-execution hooks passed to generate() or stream() override matching agent-level hooks. When a workspace also defines tools.hooks, workspace hooks run inside the agent hook wrapper.
Editor overridesDirect link to Editor overrides
When you register the MastraEditor, the editor field controls which parts of a code-defined agent can be changed through the editor. Fields owned by code are read-only in Studio and are stripped from saved overrides.
editor?:
false to lock the agent. Set instructions: true to allow instruction edits. Set tools: true to allow tool membership and description edits, or tools: { description: true } to allow only description edits.The agent's id, name, and model always come from code and can't be overridden through the editor. See the Editor overview for usage.