$npx -y skills add butterbase-ai/butterbase-skills --skill meetingsUse when building features that join, record, or transcribe Zoom/Meet/Teams/Webex calls — meeting bots, call notetakers, sales-call summarizers, interview transcribers. Covers ctx.ai.meetings.start/get/stop/list, webhook setup, and credit metering.
| 1 | # Meeting Bots on Butterbase |
| 2 | |
| 3 | Guide for building features that spawn meeting bots via `ctx.ai.meetings`. Covers the four SDK methods, webhook handling, idempotency, and cost-aware design. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## 1. When to Use |
| 8 | |
| 9 | Reach for this skill when your feature involves any of: |
| 10 | |
| 11 | - **Meeting notetaker** — bot joins a recurring standup or sales call and stores a transcript. |
| 12 | - **Sales call enricher** — join a customer call, wait for `bot.done`, then run an LLM over the transcript to populate a CRM record. |
| 13 | - **Interview pipeline** — record and transcribe candidate interviews, then score against a rubric. |
| 14 | - **Voice-of-customer transcripts** — capture support or discovery calls; feed chunks into a RAG collection for future search. |
| 15 | |
| 16 | If you need only audio/video file upload without a live meeting join, use `manage_storage` instead. |
| 17 | |
| 18 | --- |
| 19 | |
| 20 | ## 2. The Four SDK Methods |
| 21 | |
| 22 | | Method | What it does | One-line example | |
| 23 | |--------|-------------|-----------------| |
| 24 | | `bb.ai.meetings.start(opts)` | Spawns a bot and returns immediately with `status: "joining"` | `const { data: bot } = await bb.ai.meetings.start({ meetingUrl, transcript: true })` | |
| 25 | | `bb.ai.meetings.get(id)` | Fetches current status + artifact URLs | `const { data } = await bb.ai.meetings.get(bot.id)` | |
| 26 | | `bb.ai.meetings.stop(id)` | Removes the bot from the call early | `await bb.ai.meetings.stop(bot.id)` | |
| 27 | | `bb.ai.meetings.list(opts)` | Lists bots with optional `status`, `limit`, `cursor` filters | `await bb.ai.meetings.list({ status: 'done', limit: 50 })` | |
| 28 | |
| 29 | All methods return `{ data, error }` — always check `error` before using `data`. |
| 30 | |
| 31 | --- |
| 32 | |
| 33 | ## 3. Typical Flow |
| 34 | |
| 35 | 1. **Configure webhook** — call `manage_ai` with `action: "configure_meetings_webhook"`, passing `forward_url` pointing at a deployed Butterbase function or your own endpoint. |
| 36 | 2. **Start the bot** — app calls `bb.ai.meetings.start(...)`. Status begins as `joining`. |
| 37 | 3. **Bot joins** — status progresses `joining → waiting_room → in_call → recording`. The `bot.in_call_recording` event fires. |
| 38 | 4. **Call ends** — bot status moves to `ended`, then `done` once artifacts are processed. |
| 39 | 5. **Webhook fires `bot.done`** — your handler receives the event. The bot's `recordingUrl` and `transcriptUrl` are now populated. |
| 40 | 6. **Fetch artifacts** — download the recording or transcript using the URLs from `bb.ai.meetings.get(id)`. |
| 41 | 7. **Write to substrate / database** — store the transcript text, link to the relevant entity (deal, candidate, customer), or insert into a RAG collection. |
| 42 | 8. **Trigger downstream extract** — run an LLM summarization, action-item extraction, or scoring job. |
| 43 | |
| 44 | --- |
| 45 | |
| 46 | ## 4. Webhook Handler Skeleton |
| 47 | |
| 48 | Deploy this as a Butterbase function with `trigger: { type: "http", config: { method: "POST", auth: "none" } }`. |
| 49 | |
| 50 | ```typescript |
| 51 | export async function handler(req: Request, ctx: any): Promise<Response> { |
| 52 | const event = req.headers.get('x-bb-event'); |
| 53 | const body = await req.text(); |
| 54 | const payload = JSON.parse(body); |
| 55 | |
| 56 | const botId: string = payload.data?.bot_id ?? payload.data?.id; |
| 57 | |
| 58 | // Idempotency: skip if we already processed this bot + event combo. |
| 59 | const claimed = await ctx.idempotency.claim(`meeting:${botId}:${event}`); |
| 60 | if (!claimed) { |
| 61 | return new Response('duplicate', { status: 200 }); |
| 62 | } |
| 63 | |
| 64 | switch (event) { |
| 65 | case 'bot.done': { |
| 66 | // Artifacts are ready — store a reference or kick off extraction. |
| 67 | await ctx.db.query( |
| 68 | `UPDATE meeting_jobs SET status = 'done', bot_id = $1 WHERE external_ref = $2`, |
| 69 | [botId, payload.data?.metadata?.jobRef ?? botId] |
| 70 | ); |
| 71 | break; |
| 72 | } |
| 73 | case 'transcript.done': { |
| 74 | // Transcript artifact URL is now available. Kick off downstream processing. |
| 75 | await ctx.db.query( |
| 76 | `INSERT INTO transcript_queue (bot_id, created_at) VALUES ($1, now())`, |
| 77 | [botId] |
| 78 | ); |
| 79 | break; |
| 80 | } |
| 81 | case 'bot.fatal': { |
| 82 | await ctx.db.query( |
| 83 | `UPDATE meeting_jobs SET status = 'fatal' WHERE bot_id = $1`, |
| 84 | [botId] |
| 85 | ); |
| 86 | break; |
| 87 | } |
| 88 | default: |
| 89 | // Other events (bot.in_call_recording, recording.done, transcript.failed) — handle as needed. |
| 90 | break; |
| 91 | } |
| 92 | |
| 93 | return new Response('ok', { status: 200 }); |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | Key points: |
| 98 | - Use `ctx.idempotency.claim(key)` — the meetings service may retry for up to 24 hours. |
| 99 | - Verify `x-bb-key-id` matches the first 16 characters of your current webhook secret to detect stale post-rotation events. |
| 100 | - Return `200` even for events you don't handle — a non-2xx response triggers a retry. |
| 101 | |
| 102 | --- |
| 103 | |
| 104 | ## 5. Cost-Aware Design |
| 105 | |
| 106 | Before dispatching a bot in a user-pays context, call `estimateCost` and surface the projected charge: |
| 107 | |
| 108 | ```typescript |
| 109 | const { data: estimate } = await bb.ai.meetings.estimateCost({ |
| 110 | duration |