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();
}
The handler activates on your app's next deploy after you add the file. Apps that don't ship 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 at startAt for a scheduled recording). Returns { id, status }.
  • get(id) — one meeting, status-aware. Returns the meeting object (transcript + signed audioUrl/videoUrl once done).
  • 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 as get(). 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: true only when you actually need the video.