Skip to main content

Notifications

A notification records an event for an agent to act on, such as a failed build, a pull request comment, or a completed background job. Mastra stores it in a durable inbox, separate from the conversation's messages. The event remains available even if the agent is busy when it arrives.

A delivery policy decides whether to send the event immediately, summarize it with other events, or leave it for later. The agent uses the inbox to read, dismiss, or archive records. Delivery can fail, and policies can discard events, so storing a record doesn't guarantee that the agent will act on it. The Notifications reference describes the record lifecycle and policy options.

Use notifications for events that need this inbox and delivery behavior. Use sendMessage() for user input, a state signal for changing application context, or a transient reactive signal for context needed by one model request. sendSignal({ type: 'notification' }) sends event context without creating an inbox record or providing its retries.

Quickstart
Direct link to Quickstart

Call agent.sendNotificationSignal() to create the record and apply its delivery policy. To let the agent read pending records, add createNotificationInboxTool() as shown in Work with the inbox.

Records need a storage adapter with the notifications domain. This example uses libSQL, subscribes to the thread first, then sends a high-priority CI failure:

src/mastra/notifications.ts
import { Mastra } from '@mastra/core'
import { Agent } from '@mastra/core/agent'
import { LibSQLStore } from '@mastra/libsql'
import { Memory } from '@mastra/memory'

const storage = new LibSQLStore({
id: 'mastra',
url: 'file:./mastra.db',
})

const ciAgent = new Agent({
id: 'ci-agent',
name: 'CI Agent',
instructions: 'Triage CI failures and explain what broke.',
model: 'openai/gpt-6.1-sol',
memory: new Memory({ storage }),
})

const mastra = new Mastra({
agents: { ciAgent },
storage,
})

const agent = mastra.getAgent('ciAgent')
const thread = {
resourceId: 'user_123',
threadId: 'thread_456',
}

const subscription = await agent.subscribeToThread(thread)

const { record, decision } = await agent.sendNotificationSignal(
{
source: 'github',
kind: 'ci-status',
priority: 'high',
summary: 'CI failed on main: 3 tests failed in the API suite.',
payload: {
repository: 'acme/app',
branch: 'main',
runId: '123456',
},
dedupeKey: 'github:acme/app:main:ci',
},
thread,
)

console.log(record.status, decision.action)

for await (const chunk of subscription.stream) {
console.log(chunk)

if (chunk.type === 'finish') break
}

On an idle thread, a high-priority notification delivers immediately and starts a run. On an active thread, Mastra summarizes it first, then delivers the full notification when the thread is idle. The subscription receives both the summary and the full notification as thread stream chunks.

Notification records
Direct link to Notification records

When a notification arrives, sendNotificationSignal() creates a record or merges it into a matching pending record. It then checks the delivery policy. If delivery is scheduled for later, the dispatcher reads the record when it's due and sends the notification or a summary.

The stored record includes routing, priority, status, delivery attempts, and the original payload. The model receives an XML element built from summary and attributes. It doesn't receive payload automatically.

A record's status can be pending, delivered, seen, dismissed, archived, discarded, or failed. A summarized record stays pending, because a summary tells the model that records exist without marking them delivered. The reference lists every record field and status.

Create and coalesce records
Direct link to Create and coalesce records

sendNotificationSignal() accepts one notification or an array. Each item is created independently, then storage decides whether it can merge into an existing pending record.

Use dedupeKey to identify one event that may arrive more than once. Use coalesceKey to group related events into one pending record, a process called coalescing. Both keys use the same matching rules.

Merging only considers records on the same thread, source, kind, agent, and resource that are still pending. The Notifications reference documents the fields that merge and their precedence.

Storage and dispatcher setup
Direct link to Storage and dispatcher setup

Notification records require a storage adapter with the notifications domain: libSQL, PostgreSQL, or MongoDB. Without it, sendNotificationSignal() throws instead of silently dropping the record.

Scheduled dispatch is enabled by default and runs through the workflow scheduler. Configure it on the Mastra instance:

src/mastra/index.ts
import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
agents: { ciAgent },
storage,
notifications: {
dispatch: {
enabled: true,
cron: '*/1 * * * *',
batchSize: 100,
},
},
})

The defaults are enabled: true, cron: '*/1 * * * *' (once per minute), and batchSize: 100. Only apps that defer or summarize a notification create the lazy dispatch schedule and activate the workflow scheduler with it. Set enabled: false to opt out.

Delivery policy
Direct link to Delivery policy

Every record gets a decision when received, and possibly again when dispatched. The built-in policy uses priority and thread state:

PriorityIdle threadActive thread
urgentDeliver immediatelyDeliver immediately
highDeliver immediatelySummarize now, then deliver when idle
mediumDeliver immediatelyBatch into a summary
lowBatch into a summaryBatch into a summary

Configure a policy on the agent when the defaults don't fit. Source rules run before priority rules, so a noisy source can be summarized while everything else delivers:

src/mastra/agents/ci-agent.ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'

// storage from the Quickstart
export const ciAgent = new Agent({
id: 'ci-agent',
name: 'CI Agent',
instructions: 'Triage CI failures and explain what broke.',
model: 'openai/gpt-6.1-sol',
memory: new Memory({ storage }),
notifications: {
deliveryPolicy: {
sources: {
github: 'summarize',
},
priorities: {
urgent: 'deliver',
low: 'discard',
},
},
},
})

Mastra evaluates the policy in this order:

  1. A custom decide() function.
  2. sources, matched against record.source.
  3. priorities, matched against record.priority.
  4. default.
  5. The built-in priority policy.

decide() runs at receipt and again at delivery for records whose first decision deferred them or scheduled a summary. The stored schedule fixes when and how they deliver. Delivery honors only streamOptions and hold from the new decision. Keep decide() side-effect free.

Dispatch
Direct link to Dispatch

The dispatcher reads due records, groups summaries by agentId, resourceId, and threadId, and emits signals through the thread runtime.

DecisionResult
deliverSend the full notification immediately
queueKeep the record pending for a later dispatch pass
summarizeSend a summary now or at summaryAt
deferKeep the record pending until deliverAt
persistStore the record without sending a signal
discardMark the record discarded

A decision with hold leaves the record for another process's dispatcher without counting a delivery attempt.

Summaries
Direct link to Summaries

Summaries keep a busy thread from receiving a burst of individual notifications. The model sees a compact digest:

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

A full notification delivers the record's summary as its contents:

<notification source="github" kind="ci-status" priority="high" status="delivered">CI failed on main: 3 tests failed in the API suite.</notification>

A summary clears summaryAt and sets summarySignalId. The records stay pending and readable. A full notification sets deliveredSignalId and marks the record delivered.

Summaries don't mark records delivered. When the agent needs the full content, it reads the records through the inbox; reading can inject the full signal and mark the record seen, preventing duplicate delivery later.

Retries and failures
Direct link to Retries and failures

A failed attempt increments deliveryAttempts and records lastDeliveryAttemptAt and lastDeliveryError. The record stays pending and is due again on the next dispatch pass.

After five failed attempts, the record moves to failed and stops being due. The cap is exported as MAX_NOTIFICATION_DELIVERY_ATTEMPTS. A hold leaves it pending without counting an attempt, so another dispatcher can pick it up.

If a custom policy throws when the notification first arrives, the record stays pending with an immediate deliverAt and a failed attempt, so the dispatcher can retry it.

Work with the inbox
Direct link to Work with the inbox

Give the agent the inbox tool so it can inspect records without exposing storage APIs:

src/mastra/agents/ci-agent.ts
import { Agent } from '@mastra/core/agent'
import { createNotificationInboxTool } from '@mastra/core/notifications'

const notificationsStorage = await storage.getStore('notifications')

export const ciAgent = new Agent({
// ...id, instructions, model, memory, and notifications from the policy example
tools: {
notificationInbox: createNotificationInboxTool({ storage: notificationsStorage }),
},
})

The tool supports list, read, search, markSeen, dismiss, and archive. Listing and searching mark returned unread records seen, including failed records so the agent learns delivery gave up. list defaults to pending, delivered, and failed records. search spans all statuses. read only acts on pending and delivered records, so it skips failed ones. A record read can't deliver, because neither the record nor the calling agent supplies a resourceId, stays pending.

read matters after a summary. It delivers the selected records' full contents as notification signals rather than ordinary tool output. The agent sees the context on the thread even though the call returned a short status message.

Agent and client surfaces
Direct link to Agent and client surfaces

agent.sendNotificationSignal() is the canonical way to create durable notification records. It returns the record and the policy decision, and includes accepted, signal, and persisted when a signal was emitted, matching the other signal methods described in Sending signals.

The JavaScript client SDK can send notification-shaped context through client.getAgent().sendSignal(). That path bypasses inbox storage, so use it only when the event needs no record, retries, or later reading. Durable records over HTTP go through the AgentController Session notification route, which forwards to sendNotificationSignal(). See the Session reference and Agent Controller.