Build an agent
Last updated Oct 26, 2026 An agent is a meeting participant that your backend controls over a WebSocket. To add one, configure a preset, add the agent as a participant, and connect to the Websocket endpoint with the agent's participant token.
My Worker
My Durable Object
Agent Participant
RealtimeKit Meeting
Add participant with the agent preset
start(token)
Upgrade with token + subscriptions
Join as visible or hidden participant
session.started
Transcripts, joins, broadcasts
Subscribed events as JSON
Choose an agent pattern
Decide whether people in the meeting should see the agent. Both patterns use the same steps on this page and receive the same events. Only the preset changes.
| Pattern | Preset | Use it for |
|---|---|---|
| Hidden agent | hidden_participant: true |
Compliance monitoring, live analytics |
| Visible agent | hidden_participant: false |
A note-taker people can see in the meeting |
Hidden agent
Use a hidden agent when people should not see it, for example to monitor for compliance, review call quality, or collect live meeting analytics.
PeopleBrowser or app with the RealtimeKit SDKs
Audio, video, chat
RealtimeKit meeting
What people see
Alice
Bob
Hidden agentMonitorFiltered out of the list and grid
Transcripts, joins, broadcasts
My codeDurable ObjectWebSocket ↔ JSON
PeopleAudio, video, chat ↔RealtimeKit meeting
MonitorHidden from peopleRealtimeKit meeting
MonitorTranscripts, joins, broadcasts →Durable Object
- Not shown to people. The RealtimeKit SDKs filter the agent out of the participant list and the video grid.
- Join and leave events still go out. Every client connected to the meeting receives them, and the SDKs choose not to show them. Do not rely on
hidden_participantto keep the agent secret from someone inspecting their own client. - Same events and billing. A hidden agent receives every event it subscribes to, is billed as a participant, and keeps the session active while connected.
- No one removes it for you. People cannot see the agent, so your backend must end it.
- Disclosure is up to you. Hiding the agent does not hide that the meeting is transcribed. Tell participants if your policy requires it.
Visible agent
Use a visible agent when people should know it is there, for example a note-taker named "Notes bot" that starts taking notes as soon as it joins.
PeopleBrowser or app with the RealtimeKit SDKs
Audio, video, chat
RealtimeKit meeting
What people see
Alice
Bob
Notes bot
Visible agentNotes botShows as a tile, with no audio or video
Transcripts, joins, broadcasts
My codeDurable ObjectWebSocket ↔ JSON
PeopleAudio, video, chat ↔RealtimeKit meeting
Notes botTile in the gridRealtimeKit meeting
Notes botTranscripts, joins, broadcasts →Durable Object
- Shown by name. The
nameyou pass when you add the agent as a participant is what people see. - Takes a tile in the grid. The agent appears in the video grid even though it sends no audio or video. In a custom Core SDK grid, filter it out by
participant.presetName. - Starts when it joins. Anything said before the agent joins is not captured. Add the agent before the meeting starts to capture the whole session.
Requirements
- A RealtimeKit app and meeting. Refer to the quickstart.
- A Cloudflare API token with RealtimeKit access.
- To receive live transcripts, real-time transcription turned on in the presets of the participants you want transcribed. Without it, the agent receives no
transcript.receivedevents.
1. Configure an agent preset
The agent joins the meeting with a preset, like any other participant. You can use an existing preset, but a dedicated agent preset keeps agent settings separate from human participants. Create one in the RealtimeKit dashboard ↗︎ or with the Create a preset API.
To hide the agent from other participants, set hidden_participant with the Update a preset API:
<details>
<summary>
Required API token permissions
</summary>
At least one of the following <a href="https://developers.cloudflare.com/fundamentals/api/reference/permissions/">token permissions</a> is required:
- <code>Realtime Admin</code>
- <code>Realtime</code>
</details>
Update a presetbash
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/presets/$PRESET_ID" \
--request PATCH \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"permissions": {
"hidden_participant": true
}
}'
hidden_participant changes only what other participants see. The agent receives the same events either way. For what a hidden agent looks like to others, refer to Hidden agent.
2. Create a meeting and a participant token for your agent
If you do not have a meeting yet, create one with the Create a meeting API:
<details>
<summary>
Required API token permissions
</summary>
At least one of the following <a href="https://developers.cloudflare.com/fundamentals/api/reference/permissions/">token permissions</a> is required:
- <code>Realtime Admin</code>
- <code>Realtime</code>
</details>
Create a meetingbash
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/meetings" \
--request POST \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"title": "Weekly product review"
}'
The response data.id is the meeting ID. Use it as $MEETING_ID in the next request.
Then add a participant to the meeting with the agent preset:
<details>
<summary>
Required API token permissions
</summary>
At least one of the following <a href="https://developers.cloudflare.com/fundamentals/api/reference/permissions/">token permissions</a> is required:
- <code>Realtime Admin</code>
- <code>Realtime</code>
</details>
Add a participantbash
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/realtime/kit/$APP_ID/meetings/$MEETING_ID/participants" \
--request POST \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"name": "Notes bot",
"preset_name": "agent",
"custom_participant_id": "notes-bot"
}'
The response data.token is the agent's participant token. Reuse the same participant for every session of the meeting. A participant token is valid for 100 days by default.
3. Connect to the Agent's WebSocket URL
Open a WebSocket to the endpoint with the participant token and the realtimekit.v1 subprotocol:
GET wss://api.realtime.cloudflare.com/agent-ws/v1?subscriptions=transcript,participant,broadcast
Authorization: Bearer <PARTICIPANT_TOKEN>
Sec-WebSocket-Protocol: realtimekit.v1
The token selects the meeting. HTTP 101 means the connection is ready. The agent then joins the meeting and sends session.started.
4. Set connection parameters
| Parameter | Value |
|---|---|
subscriptions |
Optional. Any of transcript, participant, broadcast, comma-separated. |
connection_id |
Optional. Resumes an earlier connection. Refer to Reconnect. |
subscriptions decides which events the agent receives. Session events are always sent.
| Category | Events | Needs |
|---|---|---|
transcript |
transcript.received |
Real-time transcription on the speakers' presets |
participant |
participant.join, participant.leave |
Nothing |
broadcast |
broadcast.received |
Your app sends broadcasts |
Note
The transcript subscription only delivers events if real-time transcription is on. Set permissions.transcription_enabled: true in the presets of the participants whose speech you want transcribed. Refer to Turn on real-time transcription.
Subscribe only to what you use. Fewer events means less queueing during reconnects. For payloads and delivery rules, refer to Events.
5. Run the agent in a Durable Object
This example puts steps 1 to 4 together. A Worker adds the agent to a meeting, and a Durable Object holds the agent's WebSocket and logs final transcripts and participant joins.
A Durable Object fits the agent: one instance per meeting, a long-lived outbound WebSocket, and storage for the resume connection_id.
- Configure the Worker. Bind the Durable Object and set the app details:
{
"name": "meeting-agent",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-10-27",
"vars": {
"ACCOUNT_ID": "<ACCOUNT_ID>",
"APP_ID": "<APP_ID>",
"AGENT_PRESET": "agent",
},
"durable_objects": {
"bindings": [{ "name": "MEETING_AGENT", "class_name": "MeetingAgent" }],
},
"exports": {
"MeetingAgent": {
"type": "durable-object",
"storage": "sqlite",
},
},
}
```
```toml
name = "meeting-agent"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-10-27"
[vars]
ACCOUNT_ID = "<ACCOUNT_ID>"
APP_ID = "<APP_ID>"
AGENT_PRESET = "agent"
[[durable_objects.bindings]]
name = "MEETING_AGENT"
class_name = "MeetingAgent"
[exports.MeetingAgent]
type = "durable-object"
storage = "sqlite"
```
Store the API token as a secret:npmyarnpnpm
npx wrangler secret put CLOUDFLARE_API_TOKEN ```
yarn wrangler secret put CLOUDFLARE_API_TOKEN
```
pnpm wrangler secret put CLOUDFLARE_API_TOKEN ```
- Add and remove the agent.
POSTadds a participant with the agent preset and starts the meeting's Durable Object.DELETEremoves the agent from the meeting:
src/index.jsjs
```js export { MeetingAgent } from "./agent";
export default { async fetch(request, env) { const url = new URL(request.url); const match = url.pathname.match(/^\/meetings\/([^/]+)\/agent$/); if (!match) return new Response("Not found", { status: 404 }); const meetingId = match[1]; const agent = env.MEETING_AGENT.getByName(meetingId);
// Authenticate the caller before adding or removing an agent.
if (request.method === "DELETE") { await agent.stop(); return new Response("Agent stopped", { status: 200 }); } if (request.method !== "POST") { return new Response("Method not allowed", { status: 405 }); }
const res = await fetch(
https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/realtime/kit/${env.APP_ID}/meetings/${meetingId}/participants,
{
method: "POST",
headers: {
Authorization: Bearer ${env.CLOUDFLARE_API_TOKEN},
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Notes bot",
preset_name: env.AGENT_PRESET,
custom_participant_id: agent-${meetingId},
}),
},
);
if (!res.ok) return new Response("Add participant failed", { status: 502 });
const { data } = await res.json();
await agent.start(data.token); return new Response("Agent started", { status: 202 }); }, }; ```
src/index.tsts
```ts export { MeetingAgent } from "./agent";
export default {
async fetch(request, env): Promise
// Authenticate the caller before adding or removing an agent.
if (request.method === "DELETE") { await agent.stop(); return new Response("Agent stopped", { status: 200 }); } if (request.method !== "POST") { return new Response("Method not allowed", { status: 405 }); }
const res = await fetch(
https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/realtime/kit/${env.APP_ID}/meetings/${meetingId}/participants,
{
method: "POST",
headers: {
Authorization: Bearer ${env.CLOUDFLARE_API_TOKEN},
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Notes bot",
preset_name: env.AGENT_PRESET,
custom_participant_id: agent-${meetingId},
}),
},
);
if (!res.ok) return new Response("Add participant failed", { status: 502 });
const { data } = await res.json<{ data: { token: string } }>();
await agent.start(data.token);
return new Response("Agent started", { status: 202 });
},
} satisfies ExportedHandler
- Establish the WebSocket in a Durable Object. The Durable Object connects, stores
connection_id, handles events, reconnects, and leaves the meeting onstop():
src/agent.jsjs
```js import { DurableObject } from "cloudflare:workers";
const AGENT_WS = "https://api.realtime.cloudflare.com/agent-ws/v1"; const SUBSCRIPTIONS = "transcript,participant";
export class MeetingAgent extends DurableObject { ws;
async start(token) { await this.ctx.storage.put("token", token); await this.ctx.storage.delete("connectionId"); await this.connect(); }
async stop() { // Closing with 1000 makes the agent leave the meeting immediately. await this.ctx.storage.deleteAll(); await this.ctx.storage.deleteAlarm(); this.ws?.close(1000, "agent_stopped"); this.ws = undefined; }
