Signals
This feature is in alpha. Breaking changes may occur without a major version bump until the API is stable.
Signals are a way to interact with an agent through a thread. Instead of starting every interaction with agent.stream(), subscribe to a thread and send messages or signals. Mastra either wakes the agent when the thread is idle, drops input into the running agent loop, or queues input for the next turn.
Use message APIs for user-authored input. Use sendSignal() for lower-level system context, such as background task notifications, policy reminders, or processor-generated context.
QuickstartDirect link to Quickstart
Subscribe to the thread before sending messages. The subscription receives the active stream when the message wakes the agent or enters a running loop.
const subscription = await agent.subscribeToThread({
resourceId: 'user_123',
threadId: 'thread_456',
})
agent.sendMessage('Compare that with the previous option.', {
resourceId: 'user_123',
threadId: 'thread_456',
})
for await (const chunk of subscription.stream) {
console.log(chunk)
}
When the thread has a running agent stream, sendMessage() becomes new input inside that agent loop. When the thread is idle, Mastra starts a stream with the message as the first input.
Send a message nowDirect link to Send a message now
Use sendMessage() when the user expects the active agent to see the message immediately.
agent.sendMessage(
{
contents: 'Use the latest customer note too.',
attributes: { name: 'Jane', sentFrom: 'slack' },
},
{
resourceId: 'user_123',
threadId: 'thread_456',
},
)
The model receives attributed messages as XML-wrapped user input:
<user name="Jane" sentFrom="slack">Use the latest customer note too.</user>
Messages without attributes are sent as plain user input.
Queue a message for the next turnDirect link to Queue a message for the next turn
Use queueMessage() when a user sends a follow-up but the active model call should finish first. Mastra waits for the active run to complete, then starts a new run on the same thread.
agent.queueMessage('Also check whether the tests need updates.', {
resourceId: 'user_123',
threadId: 'thread_456',
})
When the thread is idle, queueMessage() starts a run immediately. When the thread is active, it preserves turn order by starting a new run after the active run completes.
Control low-level signal behaviorDirect link to Control low-level signal behavior
Use sendSignal() when you need to send system-generated context instead of user-authored input. For external events, use type: 'notification'. By default, Mastra delivers signals to active runs and wakes idle threads. Use ifActive.behavior and ifIdle.behavior to change that behavior.
const result = agent.sendSignal(
{
type: 'notification',
contents: 'GitHub CI failed on PR #123: 3 tests failed.',
},
{
resourceId: 'user_123',
threadId: 'thread_456',
ifIdle: {
behavior: 'persist',
},
},
)
await result.persisted
The behavior options are:
ifActive.behavior: 'deliver': Add the signal or message to the running agent loop. This is the default.ifActive.behavior: 'persist': Save the signal or message to memory without adding it to the running loop.ifActive.behavior: 'discard': Ignore the signal or message while the thread is active.ifIdle.behavior: 'wake': Start a stream with the signal or message as the first input. This is the default.ifIdle.behavior: 'persist': Save the signal or message to memory without starting a stream.ifIdle.behavior: 'discard': Ignore the signal or message while the thread is idle.
Pass ifIdle.streamOptions when the idle wake-up stream needs options such as model settings, tools, or runtime context. You do not need to repeat memory.resource or memory.thread; Mastra uses the top-level resourceId and threadId for the thread.
agent.sendMessage('Continue with the next step.', {
resourceId: 'user_123',
threadId: 'thread_456',
ifIdle: {
behavior: 'wake',
streamOptions: {
maxSteps: 3,
},
},
})
Send notification contextDirect link to Send notification context
Signals have a semantic type and an LLM-facing tagName. Use type to describe the signal category. Use tagName to control the XML tag the model sees.
For external events, use type: 'notification'. Reactive signals are reserved for processor- or runtime-generated context, such as policy guidance, background task results, and auto-loaded instructions.
agent.sendSignal(
{
type: 'notification',
contents: 'PR #123 has a new review comment from User X about the API surface.',
attributes: {
source: 'github',
pr: '123',
},
},
{
resourceId: 'user_123',
threadId: 'thread_456',
},
)
The model receives the signal as context like this:
<notification source="github" pr="123">PR #123 has a new review comment from User X about the API surface.</notification>
Use XML-safe tagName and attribute names. They can contain letters, numbers, underscores, periods, and hyphens. They must start with a letter or underscore.
Send processor contextDirect link to Send processor context
Processors can send reactive signals during a run. A processor should inspect the chat history, react to a specific trigger, and avoid sending the same context more than once.
The following example demonstrates a processor that injects AGENTS.md instructions after a tool call reads an AGENTS.md file.
import type { Processor, ProcessInputStepArgs } from '@mastra/core/processors'
export const agentsMdReminderProcessor: Processor = {
id: 'agents-md-reminder',
async processInputStep({ messageList, sendSignal }: ProcessInputStepArgs) {
const messages = messageList.get.all.db()
const agentsMdPath = findAgentsMdPathFromToolCalls(messages)
if (!agentsMdPath || hasAlreadySentAgentsMdReminder(messages, agentsMdPath)) {
return messageList
}
await sendSignal?.({
type: 'reactive',
contents: readAgentsMdInstructions(agentsMdPath),
attributes: {
type: 'dynamic-agents-md',
path: agentsMdPath,
},
metadata: {
path: agentsMdPath,
},
})
return messageList
},
}
Reactive signals default to tagName: 'system-reminder', so the model receives this context as
<system-reminder type="dynamic-agents-md" path="packages/ui/AGENTS.md">
$agentsMdFileContents
</system-reminder>
Awaiting sendSignal() preserves stream echo ordering when a subscribed thread is active.
Conditional attributesDirect link to Conditional attributes
Use ifActive.attributes and ifIdle.attributes to tag input with context that depends on whether the agent is active or idle at delivery time. Mastra resolves the correct branch when the input is accepted.
agent.sendMessage(
{
contents: 'Also cover the edge cases.',
attributes: { source: 'chat' },
},
{
resourceId: 'user_123',
threadId: 'thread_456',
ifActive: { attributes: { delivery: 'while-active' } },
ifIdle: { attributes: { delivery: 'new-message' } },
},
)
When the agent is working, the model sees:
<user source="chat" delivery="while-active">Also cover the edge cases.</user>
When the agent is idle:
<user source="chat" delivery="new-message">Also cover the edge cases.</user>
Top-level attributes always apply. The selected branch's attributes are merged into them at delivery time. The delivery name shown above is not a special Mastra API field. It is a custom attribute name used for this example, you can add any attribute names that suit your use case.
CompatibilityDirect link to Compatibility
Mastra still accepts legacy signal payloads such as type: 'user-message' and type: 'system-reminder'. It normalizes them internally to the new category and tag shape:
type: 'user-message': Normalizes totype: 'user'andtagName: 'user'type: 'system-reminder': Normalizes totype: 'reactive'andtagName: 'system-reminder'
Existing stored signal rows and older clients continue to load through the compatibility layer.
Visit Agent signals reference for the full message, signal, and subscription types.
Approve tool callsDirect link to Approve tool calls
When a subscribed run pauses for tool approval, approve or decline the tool call with the subscription-native methods. The call returns a JSON acknowledgement. The resumed chunks arrive through the existing thread subscription.
await agent.sendToolApproval({
resourceId: 'user_123',
threadId: 'thread_456',
toolCallId: 'tool-call_456',
approved: true,
})
Pass approved: false to decline the same pending tool call. Use the older approveToolCall() and declineToolCall() methods only when you are rendering the separate continuation stream directly.
Use HTTP routesDirect link to Use HTTP routes
If you call Mastra over HTTP directly, use POST /api/agents/:agentId/send-message for immediate messages and POST /api/agents/:agentId/queue-message for next-turn messages. For subscription-native tool approval, use POST /api/agents/:agentId/send-tool-approval. See Server routes reference for request and response schemas.
Use the client SDKDirect link to Use the client SDK
The JavaScript client exposes thread signal APIs. Use subscribeToThread() before sending thread input so the client can render the stream that wakes from, or receives, the input.
const agent = client.getAgent('supportAgent')
const subscription = await agent.subscribeToThread({
resourceId: 'user_123',
threadId: 'thread_456',
})
await agent.sendSignal({
signal: {
type: 'user-message',
contents: 'Show the shorter version.',
},
resourceId: 'user_123',
threadId: 'thread_456',
})
await subscription.processDataStream({
onChunk: chunk => {
console.log(chunk)
},
reconnect: true,
})
Use reconnect: true for long-lived subscriptions. The client resubscribes when the stream closes or a reconnect request fails, such as after a proxy idle timeout or a dropped network connection.
Keep custom SSE subscriptions aliveDirect link to Keep custom SSE subscriptions alive
If you expose your own Server-Sent Events (SSE) endpoint for thread subscriptions, send periodic heartbeat frames while the stream is idle. This keeps browsers, proxies, and load balancers from closing the connection before the next signal or model chunk arrives.
The following example sends an SSE comment every 25 seconds:
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(': keep-alive\n\n'))
}, 25_000)
request.signal.addEventListener('abort', () => {
clearInterval(heartbeat)
})
Use heartbeats together with client-side reconnect logic. Heartbeats reduce idle disconnects, while reconnects recover when the network or runtime still closes the stream.