# Signals

> **Experimental:** 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 signals. Mastra either wakes the agent when the thread is idle or drops the signal into the running agent loop.

Signals are a context engineering tool for guiding the agent in real time as the agent loop progresses. Use them to add system-generated content from external event sources, such as incoming email notifications, GitHub pull request comments, background task notifications, and similar events.

## Quickstart

Subscribe to the thread before sending signals. The subscription receives the active stream when the signal wakes the agent or enters a running loop.

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

agent.sendSignal(
  {
    type: 'user-message',
    contents: '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, the signal becomes new input inside that agent loop. When the thread is idle, Mastra starts a stream with the signal as the first input.

## Control signal behavior

By default, Mastra delivers signals to active runs and wakes idle threads. Use `ifActive.behavior` and `ifIdle.behavior` to change that behavior.

```typescript
const result = agent.sendSignal(
  {
    type: 'user-message',
    contents: 'Store this for later, but do not wake the agent.',
  },
  {
    resourceId: 'user_123',
    threadId: 'thread_456',
    ifIdle: {
      behavior: 'persist',
    },
  },
)

await result.persisted
```

The behavior options are:

- `ifActive.behavior: 'deliver'`: Add the signal to the running agent loop. This is the default.
- `ifActive.behavior: 'persist'`: Save the signal to memory without adding it to the running loop.
- `ifActive.behavior: 'discard'`: Ignore the signal while the thread is active.
- `ifIdle.behavior: 'wake'`: Start a stream with the signal as the first input. This is the default.
- `ifIdle.behavior: 'persist'`: Save the signal to memory without starting a stream.
- `ifIdle.behavior: 'discard'`: Ignore the signal 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.

```typescript
agent.sendSignal(
  {
    type: 'user-message',
    contents: 'Continue with the next step.',
  },
  {
    resourceId: 'user_123',
    threadId: 'thread_456',
    ifIdle: {
      behavior: 'wake',
      streamOptions: {
        maxSteps: 3,
      },
    },
  },
)
```

## Identify users with attributes

Use `attributes` to tag each signal with user identity. The signal type and attributes are rendered as XML so the model can distinguish who said what in a multi-user thread:

```typescript
agent.sendSignal(
  {
    type: 'user',
    contents: 'Can we simplify the API surface?',
    attributes: { name: 'Devin', from: 'slack' },
  },
  {
    resourceId: 'user_123',
    threadId: 'thread_456',
  },
)
```

The model receives:

```xml
<user name="Devin" from="slack">Can we simplify the API surface?</user>
```

The UI sees just the message contents but can also read `attributes` and `metadata` off the signal message for custom rendering (e.g. showing user names, avatars, or platform badges).

## Send external event context

Use custom signal types for system-generated context. Non-user signal types are rendered as XML-style user-role context so they can appear inside conversation history without looking like assistant output.

```typescript
agent.sendSignal(
  {
    type: 'system-reminder',
    contents: 'User X has left a new PR comment asking for a smaller API surface.',
    attributes: {
      source: 'github',
      pr: '123',
    },
  },
  {
    resourceId: 'user_123',
    threadId: 'thread_456',
  },
)
```

The model receives the custom signal as context like this:

```xml
<system-reminder source="github" pr="123">User X has left a new PR comment asking for a smaller API surface.</system-reminder>
```

Use XML-safe signal type names and attribute names. Signal type names and attribute names can contain letters, numbers, underscores, periods, and hyphens. They must start with a letter or underscore.

## Delivery attributes

Use `ifActive.attributes` and `ifIdle.attributes` to tag a signal with context that depends on whether the agent is active or idle at delivery time. Mastra resolves the correct branch when the signal is accepted.

```typescript
agent.sendSignal(
  {
    type: 'user-message',
    contents: 'Also cover the edge cases.',
    attributes: { source: 'chat' },
  },
  {
    resourceId: 'user_123',
    threadId: 'thread_456',
    ifActive: { attributes: { delivery: 'while-active' } },
    ifIdle: { attributes: { delivery: 'message' } },
  },
)
```

When the agent is working, the model sees:

```xml
<user-message source="chat" delivery="while-active">Also cover the edge cases.</user-message>
```

When the agent is idle:

```xml
<user-message source="chat" delivery="message">Also cover the edge cases.</user-message>
```

Top-level `attributes` always apply. The selected branch's `attributes` are merged into them at delivery time. The `ifActive.attributes` branch merges when the signal is delivered to a running agent loop. The `ifIdle.attributes` branch merges when the signal wakes an idle thread. If the selected branch or its `attributes` field is `undefined`, no extra attributes are added.

Delivery attributes work with any signal type and can carry any key-value pairs. The `delivery` name shown above is not a special Mastra API field. It is a custom attribute name used for this example; you can use any attribute names that fit your application.

> **Note:** Visit [Agent.sendSignal() reference](https://mastra.ai/reference/agents/agent) for the full signal input and options types.

## Use the client SDK

The JavaScript client exposes the same thread signal APIs. Use `subscribeToThread()` before `sendSignal()` so the client can render the stream that wakes from, or receives, the signal.

```typescript
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 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:

```typescript
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.

## Related

- [`Agent.sendSignal()`](https://mastra.ai/reference/agents/agent)
- [`Agent.subscribeToThread()`](https://mastra.ai/reference/agents/agent)
- [`client.getAgent().sendSignal()`](https://mastra.ai/reference/client-js/agents)
- [`client.getAgent().subscribeToThread()`](https://mastra.ai/reference/client-js/agents)