# Ephemeral messages
Ephemeral messages carry what the agent is doing right now, such as the tool the agent is calling, to every subscribed client on the channel. Ably does not store them.
In AI Transport, [ephemeral messages](https://ably.com/docs/pub-sub/advanced.md#ephemeral) carry [awareness](https://ably.com/docs/ai-transport/awareness.md) updates from the agent that matter only at the moment they are sent. Ephemeral messages go on the same [Ably channel](https://ably.com/docs/channels.md) as the [conversation](https://ably.com/docs/ai-transport/conversation.md), so each client receives them through its existing subscription.
## What an ephemeral message is
An ephemeral message is a message with `ephemeral: true` in its `extras`. Ably delivers it to every client [subscribed](https://ably.com/docs/pub-sub.md#subscribe) to the channel at the moment it is published. Ably leaves it out of:
- Persisted history
- Rewind
- Resume after a disconnection
- Integrations such as webhooks, queues, and Firehose
So a client that reloads does not receive an ephemeral message that was sent before the reload.
The agent's [realtime client](https://ably.com/docs/pub-sub/api/javascript/realtime/realtime-client.md) publishes an ephemeral message with a [transient publish](https://ably.com/docs/pub-sub/advanced.md#transient-publish), without attaching the channel, in the same way that the [adaptor](https://ably.com/docs/ai-transport/api/javascript/agent-adaptor.md) publishes the response. The agent's [HTTP client](https://ably.com/docs/pub-sub/api/javascript/rest/rest-client.md) can also publish an ephemeral message, with `ephemeral: true` in the publish params.
## Send from a Vercel AI SDK agent
In the AI SDK, `transient: true` on a data part keeps the part out of the merged message. The [Vercel adaptor](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md) publishes a `data-*` part with `transient: true` as an ephemeral message, and publishes a `data-*` part without `transient` as a stored message.
The agent can write the transient part inside the response's stream, with `createUIMessageStream`, `writer.write(...)`, and `writer.merge(...)`. The ephemeral message carries the headers that the agent passes to `pipe`, so the client can match it to a request.
This agent code sends a status before the model's response, in the same stream:
### Vercel AI SDK Javascript
```
import { convertToModelMessages, createUIMessageStream, streamText, toUIMessageStream } from 'ai';
const headers = { requestId: message.id };
// uiMessages is the conversation so far, ending with the prompt (message).
const result = streamText({ model, messages: await convertToModelMessages(uiMessages), abortSignal: req.signal });
const stream = createUIMessageStream({
execute: ({ writer }) => {
writer.write({ type: 'data-status', data: { text: 'Searching flights' }, transient: true });
writer.merge(toUIMessageStream({ stream: result.fullStream, originalMessages: uiMessages, generateMessageId: () => crypto.randomUUID() }));
},
});
await adaptor.pipe(stream, { signal: req.signal, headers });
```
The agent can also send the transient part outside the response's stream with `adaptor.send`, at any point while it works. The ephemeral message carries the headers that the agent passes to `send`.
This agent code sends a status with `adaptor.send`:
### Vercel AI SDK Javascript
```
await adaptor.send({ type: 'data-status', data: { text: 'Searching flights' }, transient: true }, { headers });
```
## Send from a custom codec
In a [custom codec](https://ably.com/docs/ai-transport/frameworks/custom-codec.md#write-the-codec) that you build with `defineCodec`, a row's `encode` can return `ephemeral: true`, and the adaptor then publishes that event as an ephemeral message. You can set the flag on the row for a short-lived event, such as a status, that the adaptor publishes as a new message.
The agent and the client should build their adaptor and [decoder](https://ably.com/docs/ai-transport/api/javascript/decoder.md) from the same custom codec. With this codec, the adaptor publishes the `status` event as an ephemeral message:
### Javascript
```
// Agent and client code.
import { defineCodec } from '@ably/ai-transport';
export const codec = defineCodec({
id: 'my-chat',
typeOf: (e) => e.type,
events: {
status: {
encode: (e) => ({ data: e, ephemeral: true }),
decode: ({ data, type }) => ({ ...data, type }),
},
// The rows for your other event types go here.
},
});
```
## Publish an ephemeral message
With any adaptor, the agent can also publish a message to the channel with `channel.publish` and `extras: { ephemeral: true }`.
The agent should give the message a name, such as `status`, so the client can pick it out from the codec's messages, which use the name `ai` by default. If the client needs to match the status to a response, the agent can put the request's headers in `extras.headers`.
In this example, the agent publishes to the channel that it passed to the adaptor, with the same realtime client. [`pipeEach`](https://ably.com/docs/ai-transport/api/javascript/agent-adaptor.md#pipe-each) reads the next event only when the loop body has finished with the previous one, so the agent should not await a slow `publish` call inside the loop.
This agent code publishes a status each time the model calls a tool:
### OpenAI Javascript
```
const headers = { requestId };
for await (const { event } of adaptor.pipeEach(stream, { signal: req.signal, headers })) {
if (event.type === 'response.output_item.added' && event.item.type === 'function_call') {
channel
.publish({ name: 'status', data: { text: `Calling ${event.item.name}` }, extras: { ephemeral: true, headers } })
.catch(console.error);
}
}
```
### Vercel AI SDK Javascript
```
const headers = { requestId: message.id };
for await (const { event } of adaptor.pipeEach(stream, { signal: req.signal, headers })) {
if (event.type === 'tool-input-available') {
channel
.publish({ name: 'status', data: { text: `Calling ${event.toolName}` }, extras: { ephemeral: true, headers } })
.catch(console.error);
}
}
```
## Subscribe on the client
A client's `onEvent` handler receives an event from a codec's ephemeral message in the same way as any other event. A Vercel transient part arrives as `{ type: 'data-status', data, transient: true }`.
A message that the agent publishes with [`channel.publish`](#publish) reaches `onEvent` as `{ event: undefined, message }`, because the codec does not hold a decode mapping for that message type. The client can read `message.name` and `message.data`. If the sender's token carries a `clientId`, `message.clientId` identifies the client that sent the message.
In this client code, `setStatus` is your code that updates the UI, and `merge` is your code that merges the response. The code passes each status to `setStatus` and every other event to `merge`, and clears the status when the response ends:
### Vercel AI SDK Javascript
```
decoder.onEvent(({ event, message }) => {
if (event === undefined) {
if (message.name === 'status') setStatus(message.data.text);
return;
}
if (event.type.startsWith('data-') && event.transient) {
setStatus(event.data.text);
return;
}
merge(event, message.extras?.headers?.requestId);
if (event.type === 'finish' || event.type === 'abort') setStatus(undefined);
});
```
### OpenAI Javascript
```
const endEvents = ['response.completed', 'response.incomplete', 'response.failed'];
decoder.onEvent(({ event, message }) => {
if (event === undefined) {
if (message.name === 'status') setStatus(message.data.text);
return;
}
merge(event, message.extras?.headers?.requestId);
if (endEvents.includes(event.type)) setStatus(undefined);
});
```
## When the agent stops
Ably does not store ephemeral messages, so the agent does not leave state on the channel to clean up.
The client keeps the last status it received until your client code clears it. The client should clear the status when the response ends. If the agent crashed partway through the response, the end event does not arrive, so the client can also clear the status after a timeout.
A client that reloads or rewinds does not receive a status until the agent publishes the next one. If the user must see the status after a reload, the agent can keep the status in the [agent's plan in LiveObjects](https://ably.com/docs/ai-transport/awareness/agent-progress.md#plan) or in [presence data](https://ably.com/docs/ai-transport/awareness/agent-progress.md#presence-status).
## Edge cases
- Rewind and history do not return ephemeral messages. A client that rewinds while a response is in flight receives the response's text, and does not receive a status until the agent publishes the next one.
- A client that resumes after a short disconnection does not receive the ephemeral messages that the agent sent while the client was away.
- An ephemeral message and the response's text can arrive out of order. A new message, such as an ephemeral message, can reach subscribers ahead of appends that the [rollup window](https://ably.com/docs/ai-transport/conversation/token-streaming.md#rollup) still holds.
- Ably rejects a `publish` call with an array of messages when one message sets `ephemeral: true` and another sets `ephemeral: false`. If one message in the array sets `ephemeral: true` and the others do not set the flag, Ably publishes every message in the array as ephemeral. The adaptor publishes one message per call, so both cases apply only to arrays that your code publishes.
- An ephemeral message cannot start work on your backend, because webhooks, queues, and Firehose do not receive it.
- Ably counts ephemeral messages against the agent connection's [inbound message rate](https://ably.com/docs/platform/pricing/limits.md#connection), along with the response's appends.
## FAQ
### Are ephemeral messages billed differently?
Ably [counts](https://ably.com/docs/platform/pricing/message-counting.md#pubsub-operations) an ephemeral message once when it is published and once for each client that receives it, like any other message. Ably does not store an ephemeral message, so it does not count for persistence storage.
### Why does my transient data part never show up in the merged message?
The AI SDK's `readUIMessageStream` drops transient parts. The client must read them in [`decoder.onEvent`](#client) before it merges the response.
## Related features
- [Agent progress](https://ably.com/docs/ai-transport/awareness/agent-progress.md): show the agent's plan and status on every device.
- [Presence](https://ably.com/docs/ai-transport/awareness/presence.md): show which users and agents are on the channel.
- [Vercel AI SDK](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md): use AI Transport with the AI SDK's streams and data parts.
- [Custom codec](https://ably.com/docs/ai-transport/frameworks/custom-codec.md): write a codec for your event types, including ephemeral ones.
- [Ephemeral messages in Pub/Sub](https://ably.com/docs/pub-sub/advanced.md#ephemeral): how to mark a message as ephemeral with any Ably client.
## Related Topics
- [Overview](https://ably.com/docs/ai-transport/awareness.md): Show users what an AI agent is doing right now, and let the agent check who is on the channel, with LiveObjects, ephemeral messages, presence, and occupancy on the channel the conversation already uses.
- [Agent progress](https://ably.com/docs/ai-transport/awareness/agent-progress.md): Show your users the AI agent's plan and its progress through each step while it runs, with LiveObjects, ephemeral messages, and presence on the conversation's Ably channel.
- [Presence](https://ably.com/docs/ai-transport/awareness/presence.md): Show whether the AI agent is thinking or streaming, show which users are on the channel and let the agent check whether the user is still there, with Ably presence and occupancy on the channel the conversation already uses.
- [Push notifications](https://ably.com/docs/ai-transport/awareness/push-notifications.md): Notify users when a long-running AI agent finishes or needs their input, even after they close the app, by publishing a message with a push payload to the channel when the user has left.
## Documentation Index
To discover additional Ably documentation:
1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages.
2. Identify relevant URLs from that index.
3. Fetch target pages as needed.
Avoid using assumed or outdated documentation paths.