Skip to main content

Signals

Added in: @mastra/core@1.39.0

alpha

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.

When to use signals
Direct link to When to use signals

Use signals when an agent thread needs new input or context outside the original stream() call. Signals are useful when users send follow-up messages while a run is active, when background systems need to add context to a thread, or when external events should wake, update, or notify the agent.

Use sendMessage() and queueMessage() for user-authored input. Use sendSignal() for lower-level system context. Use sendStateSignal() for durable state lanes, and use sendNotificationSignal() when an external event should create a durable notification inbox record.

Quickstart
Direct link to Quickstart

Create an agent, subscribe to a thread, then send a message to that thread. The subscription receives the active stream when the message wakes the agent or enters a running loop.

src/mastra/signals.ts
import { Agent } from '@mastra/core/agent'

const agent = new Agent({
id: 'support-agent',
name: 'Support Agent',
instructions: 'Help the user compare options.',
model: 'openai/gpt-5.5',
})

const thread = {
resourceId: 'user_123',
threadId: 'thread_456',
}

const subscription = await agent.subscribeToThread(thread)

await agent.sendMessage('Compare that with the previous option.', thread)

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.

Message input
Direct link to Message input

Send a message now
Direct link to Send a message now

Use sendMessage() when the user expects the active agent to see the message immediately.

src/mastra/signals.ts
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 turn
Direct 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.

src/mastra/signals.ts
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.

Signal context
Direct link to Signal context

Control low-level signal behavior
Direct 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.

src/mastra/signals.ts
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

Pass ifIdle.streamOptions when the idle wake-up stream needs options such as model settings, tools, or runtime context.

note

Visit Agent.sendSignal() reference for ifActive, ifIdle, branch attributes, and streamOptions.

Send notification context
Direct 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.

src/mastra/signals.ts
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.

Storage support
Direct link to Storage support

Notification inbox storage is available in the storage adapters that support richer memory and signal workflows: libSQL, PostgreSQL, and MongoDB. These adapters expose notification records through getStore('notifications').

Send processor context
Direct 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.

src/mastra/processors/agents-md-reminder.ts
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 attributes
Direct 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. Top-level attributes always apply, and Mastra merges the selected branch's attributes into them when the input is accepted.

note

Visit Agent.sendMessage() reference and Agent.sendSignal() reference for branch-specific attributes.

State and notification signals
Direct link to State and notification signals

State signals
Direct link to State signals

State signals expose named, thread-scoped context lanes. Use them for durable context that changes over time, such as browser state, editor state, or a background watcher result.

Use sendStateSignal() when an external producer detects a state change. Each state signal identifies a state lane, a producer-owned cache key, and whether the update is a snapshot or delta.

src/mastra/browser-watcher.ts
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_456',
},
)

When Mastra accepts a state signal, it stores compact tracking metadata on the thread. If a producer sends the same cacheKey and mode again while that state is still current, Mastra skips the duplicate.

Use computeStateSignal() when a processor owns a state lane. Mastra calls it once per model input step after processInputStep(). Visit Agent.sendStateSignal() reference for state signal fields and return values.

src/mastra/processors/browser-state.ts
import type { ComputeStateSignalArgs, Processor } from '@mastra/core/processors'

export const browserStateProcessor: Processor = {
id: 'browser-state',
stateId: 'browser',
computeStateSignal(args: ComputeStateSignalArgs) {
const browser = readCurrentBrowserState()
const previous = readMostRecentBrowserState(args.activeStateSignals)
const changed = previous ? diffBrowserState(previous, browser) : browser
const shouldRefreshSnapshot = Boolean(args.lastSnapshot && !args.contextWindow.hasSnapshot)

if (previous && Object.keys(changed).length === 0 && !shouldRefreshSnapshot) {
return
}

const isDelta = Boolean(previous && !shouldRefreshSnapshot)

return {
mode: isDelta ? 'delta' : 'snapshot',
cacheKey: stableBrowserStateCacheKey(browser),
contents: isDelta ? describeBrowserDelta(changed) : describeBrowserSnapshot(browser),
value: browser,
...(isDelta ? { delta: changed } : {}),
}
},
}

Mastra passes lastSnapshot and deltasSinceSnapshot into computeStateSignal(). It resolves them from message history when the current message list doesn't contain the latest snapshot. The processor still owns merge and diff logic.

contextWindow.hasSnapshot tells the processor whether the active message window already contains a snapshot for this state lane. If it's false, return a fresh snapshot so the model sees the current state even after older state messages are trimmed from the context window.

The built-in browser context processor emits state under the browser id with snapshot and delta modes.

Notification signals
Direct link to Notification signals

Notification signals represent external events such as GitHub activity, email, Slack mentions, CI status, incidents, recordings, or direct messages. Use agent.sendNotificationSignal() when the event should create a durable inbox record.

Notification delivery has two phases. During ingress, agent.sendNotificationSignal() stores a notification record and resolves the agent's delivery policy. During dispatch, Mastra consumes due records and emits full notification or summary signals.

The default delivery policy is priority-aware. Urgent notifications deliver immediately, while lower-priority notifications may be batched into summaries or wait until the thread is idle.

note

Visit Agent.sendNotificationSignal() reference for notification fields, Agent constructor reference for notifications.deliveryPolicy configuration, and createNotificationInboxTool() reference for inbox tool actions.

src/mastra/notifications.ts
await agent.sendNotificationSignal(
{
source: 'github',
kind: 'ci-status',
priority: 'high',
summary: 'CI failed on main: 3 tests failed.',
payload: {
repository: 'acme/app',
branch: 'main',
},
dedupeKey: 'github:acme/app:main:ci',
},
{
resourceId: 'user_123',
threadId: 'thread_456',
},
)

The model receives full notifications as context:

<notification source="github" type="ci-status" priority="high" status="delivered">CI failed on main: 3 tests failed.</notification>

Notification summaries tell the model that inbox records are waiting:

<notification-summary pending="10">github: 3, email: 5, slack: 2</notification-summary>

When Mastra emits a summary, it clears summaryAt and sets summarySignalId on each summarized record. The records stay pending and readable. When Mastra emits a full notification, it sets deliveredSignalId and marks the record delivered. If the inbox tool reads a notification first, it can inject the full notification signal and mark the record seen, which prevents duplicate full delivery.

Configure a delivery policy on the agent when some notifications should wait for a different dispatch window or summary rollup. Enable scheduled dispatch at the Mastra level when deferred notifications and summary rollups should be delivered automatically.

note

Visit Agent constructor reference for notifications.deliveryPolicy and Mastra class reference for runtime notification dispatch configuration.

Notification inbox tool
Direct link to Notification inbox tool

Use createNotificationInboxTool() to give agents one tool for inbox actions instead of many CRUD tools. Use read after a <notification-summary> signal when the agent needs the full records behind the summary. The notification contents are delivered as signals, not as normal tool output.

note

Visit createNotificationInboxTool() reference for the setup example, input schema, and action behavior.

sendNotificationSignal() requires a storage domain with notifications support. Use sendSignal({ type: 'notification' }) only for lower-level notification-shaped context that should bypass inbox storage.

Compatibility and APIs
Direct link to Compatibility and APIs

Compatibility
Direct 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 to type: 'user' and tagName: 'user'
  • type: 'system-reminder': Normalizes to type: 'reactive' and tagName: 'system-reminder'

Existing stored signal rows and older clients continue to load through the compatibility layer. New clients call the message routes when the server supports them; React's thread signal path falls back to the legacy /signals route when it detects an older server.

note

Visit Agent signals reference for the full message, signal, and subscription types.

Approve tool calls
Direct 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 resumed chunks arrive through the existing thread subscription.

note

Visit client.getAgent().sendToolApproval() reference and server agent routes for request and response shapes.

Use HTTP routes
Direct 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 SDK
Direct 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.

src/app/chat.ts
const agent = client.getAgent('supportAgent')

const subscription = await agent.subscribeToThread({
resourceId: 'user_123',
threadId: 'thread_456',
})

await agent.sendMessage({
message: '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. Visit client.getAgent().subscribeToThread() reference for reconnect options.

Keep custom SSE subscriptions alive
Direct 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:

src/api/subscribe.ts
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.