Meeting recording
env.MEETING sends a bot into a video call (Zoom, Google Meet, Microsoft Teams). It joins as a participant, records the meeting, and produces a transcript. Recording is asynchronous — the bot stays for the whole call — so you start a recording, then fetch the result once it's done. Turn on indexing and every transcript becomes searchable by meaning through env.SEARCH.
env.MEETING is a paid-plan feature. Reference it in any api/*.js file and it's wired up on deploy — no keys to manage.Start a recording
record takes the meeting join link and returns immediately with a id and a status. The bot records for the entire call, so you don't wait here — you fetch the transcript later with the id.
const { id } = await env.MEETING.record(meetingUrl, {
name: "Acme notetaker", // the bot's display name IN the call (see note below)
index: true, // index the transcript into env.SEARCH when done (default true)
video: false, // capture video too (default false = audio only, cheaper)
screenshots: false, // also keep periodic screenshots of the call (default off)
metadata: { projectId }, // free-form JSON you can filter on in list()
});
// { id: "…", status: "recording" }name is what the other participants see when the bot joins the call — it's the bot's visible display name in Zoom/Meet/Teams, and it's also the label shown in list(). Omitted, the bot joins as "Notetaker".
The metadata you attach is stored on the meeting and can be used to filter list() later — e.g. group every recording by project or customer.
Schedule a recording for later
Pass startAt to send the bot in at a future time instead of right now — ideal when you already know when a meeting begins. No cron job or polling needed: you register it once and Cloudrizz dispatches the bot at that time (within about a minute).
// A Date, a millisecond timestamp, or an ISO string all work.
const { id, status } = await env.MEETING.record(meetingUrl, {
startAt: meeting.startsAt, // e.g. new Date("2026-09-01T14:00:00Z")
name: "Weekly sync",
metadata: { meetingId: meeting.id },
});
// { id: "…", status: "scheduled" }
// Changed your mind before it starts? Cancel it:
await env.MEETING.stop(id);A startAt in the past (or within the next minute) just records immediately. Set it a little before the real start if you want the bot in the room as the meeting opens. Until it fires, the meeting's status is "scheduled" (with scheduledAt set); it flips to "recording" when the bot joins.
Get the transcript & recording
Fetch a meeting by the id you got from record. The result is status-aware — it never blocks. While the call is live the status is recording; afterwards it becomes processing, then done (or failed). The transcript and media links are filled in once it's done.
const m = await env.MEETING.get(id);
// {
// id, status: "scheduled" | "recording" | "processing" | "done" | "failed",
// name, meetingUrl, durationSec, scheduledAt, startedAt, endedAt, metadata,
// transcript: { text, segments: [{ start, end, speaker, text }] } | null,
// audioUrl: "https://…" | null, // short-lived signed link
// videoUrl: "https://…" | null,
// screenshotUrls: ["https://…"], // only if you set screenshots: true
// }audioUrl / videoUrl are short-lived signed links to the recording — fetch a fresh one from get() when you need to play or download the media.
List past meetings
list returns your recordings newest-first. Filter by the metadata you set, or by a single meeting link to see its whole recording history.
// Every recording for one project
const { meetings } = await env.MEETING.list({ where: { projectId: "p_42" }, limit: 20 });
// Every recording of one recurring meeting link
await env.MEETING.list({ meetingUrl });React the moment a meeting finishes
Instead of polling get(), you can have Cloudrizz call your code the instant a recording is ready. Add an api/meeting.js that exports onMeetingComplete — it receives the finished meeting (the same shape get() returns).
// api/meeting.js
export async function onMeetingComplete(meeting, env) {
// meeting.transcript, meeting.audioUrl, meeting.metadata, …
await env.DB.prepare("UPDATE calls SET transcript = ? WHERE id = ?")
.bind(meeting.transcript.text, meeting.metadata.callId)
.run();
}api/meeting.js simply poll get() instead — both work.Search across your meetings
With index: true (the default), each transcript is indexed into env.SEARCH when the recording finishes. You can then ask questions across every call you've recorded — the entries are tagged so you can search only meetings.
const { matches } = await env.SEARCH.query("what did we decide about pricing?", {
where: { _cr_kind: "meeting" },
});API
record(url, { name?, index = true, video = false, screenshots = false, metadata?, startAt? })— send a bot into a meeting (now, or atstartAtfor a scheduled recording). Returns{ id, status }.get(id)— one meeting, status-aware. Returns the meeting object (transcript + signedaudioUrl/videoUrloncedone).list({ where?, meetingUrl?, limit = 20 })— recordings newest-first →{ meetings: [...] }.stop(id)— make the bot leave the call early →{ ok }.refresh(id)— re-sync a meeting from the recorder if its transcript never arrived (self-heal). Returns the refreshed meeting, same shape asget(). Safe to call repeatedly.
Notes
- Recording is asynchronous.
record()returns while the bot is still in the call; the transcript and media appear once the meeting ends and processing finishes. - Supported meeting platforms: Zoom, Google Meet, and Microsoft Teams.
- Recorded minutes count against your plan's monthly allowance — see Limits & quotas.
- Recordings are stored in your app's own file storage, so they're yours to keep, move, or delete.
- Audio-only recording is cheaper and enough for transcripts; pass
video: trueonly when you actually need the video.