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: "Weekly sync", // optional label
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" }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.