# Concurrent runs
Your application runs several turns on one conversation at the same time. Each run carries its own id, so each has its own stream, its own cancel signal, and its own lifecycle.
Concurrent runs let several turns stream at the same time on one conversation. Each has its own stream, its own cancel signal, and its own lifecycle. You can build interruption, several people in one conversation, and multi-agent setups on top of them.

## How it works
Runs are multiplexed on one channel by run id. Every message a [run](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md) publishes, whether text deltas, tool calls, or lifecycle events, carries the run's id in its transport headers, and the decoder surfaces it as `meta.runId`. That header is the whole separation mechanism: there is no second channel and no per-run subscription.
On the client, publish twice and key what you render on the run id each message carries:
### Javascript
```
// Client-side.
const summary = await transport.publishInput({ kind: 'message', payload: summaryPrompt });
const risks = await transport.publishInput({ kind: 'message', payload: risksPrompt });
await Promise.all([wakeAgent(summary.eventId), wakeAgent(risks.eventId)]);
const panelFor = new Map([
[await summary.runId, 'summary'],
[await risks.runId, 'risks'],
]);
transport.subscribe((event) => {
if (event.kind === 'message') renderTo(panelFor.get(event.meta.runId), event.outputs);
if (event.kind === 'run-lifecycle' && event.event.type === 'end') {
markDone(panelFor.get(event.event.runId), event.event.reason);
}
});
```
On the agent, each run is an independent handle with its own abort signal and lifecycle. Two invocations of your endpoint open two runs on the same channel, and nothing coordinates them:
### Javascript
```
// Agent-side. One invocation, one run.
const trigger = await transport.locateInput(eventId);
const run = transport.openRun({ inputCodecMessageId: trigger?.meta.codecMessageId });
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
messages: conversation,
abortSignal: run.abortSignal,
});
const { reason } = await run.pipe(result.toUIMessageStream());
await run.end({ reason });
```
One transport can hold several open runs at once. A cancel routes to the run whose id it carries, so `abortSignal` fires on that handle alone and the others keep streaming.
## Track live runs
The transport does not keep track of live runs for you, so tracking them is your own bookkeeping over the lifecycle stream:
### Javascript
```
// Client-side.
const live = new Map(); // runId -> clientId
transport.subscribe((event) => {
if (event.kind !== 'run-lifecycle') return;
const { type, runId, clientId } = event.event;
if (type === 'start') live.set(runId, clientId);
if (type === 'end') live.delete(runId);
});
```
The map holds what the client has observed since it subscribed, which is not the same as everything alive on the conversation. A device that joined mid-answer never saw that run start, so its map is legitimately shorter than another's. Page [`history`](https://ably.com/docs/ai-transport/streaming/history.md) on attach to recover the starts you missed.
A suspended run stays in the map above, because nothing ended it. That is right for cancel-before-send and wrong for a Stop button, so track the suspend and resume events too if your interface has to separate the two.
## Cancel one run without touching the others
A cancel carries one run id:
### Javascript
```
// Client-side.
await transport.cancel(await summary.runId);
// The risks run carries on streaming.
```
Cancelling only the runs this client started is a filter over the run ids this client's own `publishInput` calls resolved:
### Javascript
```
// Client-side.
const mine = [...live].filter(([, clientId]) => clientId === myClientId).map(([runId]) => runId);
await Promise.all(mine.map((runId) => transport.cancel(runId)));
```
[Cancellation](https://ably.com/docs/ai-transport/streaming/cancellation.md) covers the rest, including the agent-side authorisation hook.
## Wait for a run to finish
`publishInput` returns a `runId` promise that resolves when the agent's run start for your input arrives. The end is a lifecycle event, so waiting for it is a subscription:
### Javascript
```
// Client-side.
function whenRunEnds(transport, runId) {
return new Promise((resolve) => {
const off = transport.subscribe((event) => {
if (event.kind === 'run-lifecycle' && event.event.type === 'end' && event.event.runId === runId) {
off();
resolve(event.event.reason);
}
});
});
}
const reason = await whenRunEnds(transport, await summary.runId);
```
`subscribe` returns its own unsubscribe function, so a waiter cleans up after itself rather than accumulating handlers per run.
## With a durable session
At this level two concurrent runs are two independent streams, and your UI keeps them apart by keying on `meta.runId`. A durable session goes further and interleaves them into one conversation: each run becomes a node in the [conversation tree](https://ably.com/docs/ai-transport/durable-sessions/conversation-tree.md) under the input that triggered it, so two answers to two prompts are siblings rather than two lists you lay out yourself.
A client can then select a branch. With parallel runs on a tree, a view picks one path through the tree's branches, and switching path is a selection rather than a re-render of state you assembled.
What each client enumerates is scoped to its own view. `runs()` is filtered by that view's pagination window, branch selection, and regenerate substitution, so a client that hydrated partial history or joined mid-conversation lists fewer runs than one holding the whole conversation. It answers what this view can see, so a session-wide count needs a source that is not scoped to one branch.
## Use cases
### Interruption
Cancel what is live, then publish the replacement:
#### Javascript
```
// Client-side.
await Promise.all([...live.keys()].map((runId) => transport.cancel(runId)));
const replacement = await transport.publishInput({
kind: 'message',
payload: { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Actually, focus on the budget instead' }] },
});
await wakeAgent(replacement.eventId);
```
[Interruption and steering](https://ably.com/docs/ai-transport/streaming/interruption-and-steering.md) covers the alternatives, including steering the run rather than replacing it.
### Several people in one conversation
Two users prompting the same conversation from their own devices produce two runs. Neither client coordinates with the other, and both see both:
#### Javascript
```
// Client-side, on each device. Same channel, different clientId.
const sent = await transport.publishInput({ kind: 'message', payload: prompt });
await wakeAgent(sent.eventId);
```
Each run's start event carries the agent's own `clientId`, and that is an empty string when the agent supplied none. To label whose question is being answered, your UI can read the asking device's id from the triggering input's `meta.clientId`; the run's output events carry an `input-codec-message-id` that points back at that input.
### Multi-agent
An orchestrator dispatches work to sub-agents, each streaming onto the same channel. Every sub-agent opens its own run, so their output multiplexes by run id with no coordination between them:
#### Javascript
```
// Agent-side, in a sub-agent's endpoint.
const transport = createAgentTransport({ channel, codec, clientId: 'research-agent' });
await transport.connect();
// No inputCodecMessageId: this run answers the orchestrator rather than a
// client input, so no client is waiting on its run id.
const run = transport.openRun();
await run.pipe(researchStream());
await run.end({ reason: 'complete' });
transport.close();
```
Let each sub-agent generate its own run id, which is what `openRun` does when you pass none. Sharing one run id across sub-agents collides their output on a single run.
## Edge cases and unhappy paths
- Concurrent runs share the channel's message rate. A burst of parallel streams approaches the per-connection rate limit faster than a single stream, so tune the [append rollup window](https://ably.com/docs/ai-transport/streaming/token-streaming.md#rollup) to bring the publish rate down.
- `transport.cancel(runId)` against a run that has already ended is a no-op. A cancel for a run no agent recognises is dropped rather than raised.
- A multi-agent setup must let each sub-agent's `openRun` generate its own run id. Passing a shared id across sub-agents collides their output on one run, and `openRun` already generates a fresh one when you pass none.
- Two clients publishing at the same time produce two runs whose output interleaves on the channel. Group what you render by `meta.runId`, or the two answers arrive as one.
## FAQ
### How many turns run concurrently?
There is no hard limit on the channel side. Practical limits come from your application's concurrency (server compute, model rate limits) and the channel's message rate. Plan for the publish rate rather than the turn count.
### Does the client need to track run ids?
Yes, at this level. The transport holds no registry, so keeping the ids you care about is your bookkeeping over the lifecycle stream. Keep only what your interface acts on: a run you offer to cancel, or one you are waiting on. A durable session tracks them for you.
### How do I tell which run a message belongs to?
Read `meta.runId` off the `message` event. The decoder lifts it from the wire headers, so you never parse `extras` yourself. A run-less input, such as a user prompt before any agent has answered it, carries no run id.
### Can one user have two runs open?
Yes. `clientId` does not limit how many runs a client has in flight, and a cancel carries one run id, so targeting a single one is straightforward.
### Why run ids rather than message ids?
A run is one unit of agent work that produces several messages. The run id groups all of them, so cancel and wait operate on the unit a user recognises as one answer.
## Related features
- [Cancellation](https://ably.com/docs/ai-transport/streaming/cancellation.md): scoped cancel signals and server-side abort handling.
- [Interruption and steering](https://ably.com/docs/ai-transport/streaming/interruption-and-steering.md): steer the active run, cancel and re-prompt, or send alongside.
- [Multi-device and fan-out](https://ably.com/docs/ai-transport/streaming/multi-device.md): the same runs reaching every attached device.
- [Runs and steps](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md): the run ids that keep parallel runs apart.
## Related Topics
- [Overview](https://ably.com/docs/ai-transport/streaming.md): Stream an agent's output to every connected client over one Ably channel, with run and step lifecycle, cancellation and steering on top, while the conversation stays in your own database.
- [Runs and steps](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md): Understand runs in AI Transport: the unit of agent work for one prompt-response cycle, with explicit identity, lifecycle, and an end reason, triggered by an invocation and published as steps.
- [Token streaming](https://ably.com/docs/ai-transport/streaming/token-streaming.md): Stream AI-generated tokens to clients in realtime using AI Transport. Tokens are appended to a single durable message, and the full response is served to clients that join later.
- [Cancellation](https://ably.com/docs/ai-transport/streaming/cancellation.md): Cancel AI responses mid-stream with Ably AI Transport. A cancel is a signal on the channel, scoped to one run, authorised on the agent, and idempotent.
- [Interruption and steering](https://ably.com/docs/ai-transport/streaming/interruption-and-steering.md): Let users change direction mid-response in Ably AI Transport. Steer the active run with a follow-up prompt, cancel and re-prompt, send alongside as a concurrent run, or queue the follow-up.
- [Reconnection and recovery](https://ably.com/docs/ai-transport/streaming/reconnection-and-recovery.md): AI Transport streams survive connection drops automatically. Clients reconnect and resume from where they left off with the whole response intact.
- [Multi-device and fan-out](https://ably.com/docs/ai-transport/streaming/multi-device.md): One agent run reaches every client attached to the conversation with Ably AI Transport. Fan-out comes from the channel, so a user can start on a laptop and carry on from a phone.
- [History and replay](https://ably.com/docs/ai-transport/streaming/history.md): Page conversation history backwards from the Ably channel with AI Transport. Chronological batches, a resumable cursor, and joining the channel to your own store.
## 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.