> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For full documentation content, see https://docs.meetstream.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# Guide to Configure Various Automatic Leave Triggers for the Bot

This guide explains how to make your MeetStream bot **automatically leave a meeting when only other vendor notetakers remain** — Otter, Fireflies, tl;dv, Read.ai, Fathom, Grain, Copilot, Notta, and so on.

It's an opt-in nested mechanism inside the existing `automatic_leave` payload field on `POST /api/v1/bots/create_bot` (and honored identically when you `PATCH` a scheduled bot).

> **Scope:** Works on all three supported platforms — Google Meet, Zoom, and Microsoft Teams. Same payload shape everywhere.

[Section 9](#9-the-other-automatic_leave-timeouts) also covers the rest of the `automatic_leave` knobs — `waiting_room_timeout`, `noone_joined_timeout`, `everyone_left_timeout`, `voice_inactivity_timeout`, `in_call_recording_timeout`, and `recording_permission_denied_timeout` — since `bot_detection` interacts with all of them.

---

## 1) Why this exists

`automatic_leave.everyone_left_timeout` only fires when the participant count drops to **zero**. In practice, a single lingering vendor notetaker (Otter, Fireflies, tl;dv, …) keeps the participant count at one and pins your bot to the meeting:

- The bot keeps recording empty audio.
- You keep accruing recording minutes.
- The transcript fills with crosstalk between notetakers, or with silence.
- A Fargate task stays warm until `in_call_recording_timeout` (default 4 hours) finally pulls the plug.

Turning on `bot_detection` lets the bot recognise that **every remaining participant looks like another notetaker**, wait a short countdown to be sure, and then leave cleanly.

---

## 2) How it works (one paragraph)

You give the bot a list of bot-name keywords (e.g. `["otter", "fireflies", "tl;dv"]`). After the bot has been admitted into the meeting for `activate_after` seconds, it begins checking the roster on every change. Whenever every remaining non-self participant's display name contains **at least one** of those keywords, the bot arms a `timeout`-second countdown. If a human reappears before the countdown fires, the timer is cancelled. If the condition still holds when the countdown fires, the bot leaves the meeting and you get the usual `bot.leaving` → `bot.stopped` webhook sequence.

The bot **never matches itself** against the keyword list — it identifies its own row in the roster via a stable platform identity (Google Meet `deviceId` / Zoom `user_id` / Teams `bot_id`), not its display name. Naming your bot `MeetStream Notetaker` will not cause a false positive even if `notetaker` is in `matches`.

---

## 3) Quick example

```bash
curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "bot_name": "MeetStream",
    "automatic_leave": {
      "everyone_left_timeout": 600,
      "bot_detection": {
        "using_participant_names": {
          "matches": [
            "notetaker", "recorder", "assistant", "copilot",
            "otter", "fireflies", "tl;dv", "read.ai",
            "fathom", "grain", "fellow", "notta", "krisp"
          ],
          "activate_after": 300,
          "timeout": 30
        }
      }
    }
  }'
```

What that payload says, in plain English:

> _"Five minutes after I'm admitted to the meeting, start watching the roster. The moment every other participant looks like a notetaker, give them 30 seconds to prove otherwise — and if they don't, leave."_

---

## 4) Field reference

All `bot_detection` fields live under `automatic_leave.bot_detection` on the create-bot payload.

### `automatic_leave.bot_detection`

| Field | Type | Required | Description |
|---|---|---|---|
| `using_participant_names` | `object` | Yes (when `bot_detection` is present) | Detect bots by matching their display name against a keyword list. See below. |

> Other mechanisms from Recall.ai's spec — `using_participant_events` (no-speaker / no-screen-share heuristic) and `silence_detection` (audio-energy fallback) — are **not yet supported**. Passing them returns `HTTP 400` so we don't silently ignore them.

### `automatic_leave.bot_detection.using_participant_names`

| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
| `matches` | `array of string` | **Yes** | — | non-empty list | Case-insensitive substrings. A participant is treated as a bot when their display name **contains** any of these substrings. Stored lowercased and deduped. |
| `activate_after` | `int` (seconds) | No | `300` | `60`–`1800` | Delay before the check becomes active, counted from when the bot is admitted to the meeting. Lets real participants trickle in before the check arms. |
| `timeout` | `int` (seconds) | No | `5` | `5`–`300` | Once every remaining participant matches, how long to wait before actually leaving. The timer resets if a human reappears. |

### Validation rules

- `matches` must be a non-empty array of non-empty strings. Whitespace is trimmed, casing is normalised to lowercase, duplicates are dropped — so `["Otter", "otter", " OTTER "]` becomes `["otter"]` server-side.
- `activate_after` and `timeout` are strict integers (no booleans, no numeric strings). Out-of-range values return `HTTP 400`.
- The block is **opt-in**: omit `bot_detection` (or omit `using_participant_names`) to disable. The rest of `automatic_leave` continues to apply.
- Validation runs in **two paths**: `POST /api/v1/bots/create_bot` and `PATCH /api/v1/calendar/scheduled_bots/{bot_id}`. They share the same schema, so anything that's valid at create time is valid at patch time.

---

## 5) Behaviour timeline

Imagine a meeting that opens at `t=0` and your bot is admitted at `t=2s` with `activate_after=300, timeout=30`:

| Time | Roster | What the bot does |
|---|---|---|
| `t=0–2s` | (joining) | No check yet. |
| `t=2s` | Bot admitted, `_in_meeting_since` clock starts. | Check is **dormant** until `t=302s`. |
| `t=10s` | Bot + 2 humans + 1 Otter notetaker. | Dormant. |
| `t=180s` | Both humans leave; only bot + Otter remain. | Still dormant — `activate_after` hasn't elapsed. |
| `t=302s` | Bot + Otter still in meeting. | Condition holds → arms a 30s timer. |
| `t=320s` | Late human joins. | Condition broken → **timer cancelled**. |
| `t=400s` | That human leaves again; only bot + Otter remain. | Condition holds again → re-arms a fresh 30s timer. |
| `t=430s` | Still bot + Otter. | Timer fires → bot leaves the meeting. |

What you see externally:

- A `bot.leaving` webhook event followed by `bot.stopped` (terminal). The `message` field on the underlying status update will mention `bot_detection.using_participant_names` so you can attribute the exit.
- No `bot.kicked` / `bot.denied` / `bot.failed` — this is a graceful, customer-configured exit, not a platform-side eject.
- Recording, transcription, and the rest of the post-call pipeline run as normal. You'll still receive `audio.processed`, `transcription.processed`, `video.processed`, and `bot.done` after the artifacts are ready.

> See [Bot Lifecycle Webhook Events](./MeetStream_Bot_Lifecycle_Webhook_Events_Guide.md) for the full webhook sequence.

---

## 6) Choosing `matches`

The keywords below are a good starting baseline that catches most common notetakers in the wild. Substring matches, case-insensitive:

```json
[
  "notetaker", "recorder", "assistant", "copilot",
  "otter", "fireflies", "tl;dv", "read.ai",
  "fathom", "grain", "fellow", "notta",
  "krisp", "meetgeek", "supernormal", "tactiq",
  "rewatch", "circleback"
]
```

Tips:

- **Match the visible display name, not a brand.** Most notetakers join with a name like `"Otter.ai Notetaker"`, `"tl;dv Bot"`, `"Fathom AI Notetaker"`, etc. — picking up either the brand fragment (`otter`) or the role fragment (`notetaker`) is usually enough.
- **Substring, not exact.** `"otter"` matches `"Otter.ai Notetaker"`, `"OtterPilot"`, and `"otter-bot"` — you don't need separate entries.
- **No regexes.** This is plain `lowercase(name).contains(keyword)` per keyword. Special characters (`.`, `:`, `;`) are matched literally.
- **Don't include your own bot name.** It's safe (the bot excludes itself by stable identity) but it's noise.

---

## 7) Tuning `activate_after` and `timeout`

| Profile | `activate_after` | `timeout` | When to use |
|---|---|---|---|
| **Aggressive** | `60`–`120` | `5`–`30` | Internal meetings where you know the call kicks off quickly. Faster reaction, slightly higher false-positive risk if a human joins late. |
| **Balanced** *(recommended)* | `300` | `30`–`60` | Default. Gives stragglers five minutes to arrive, then leaves within a minute of a roster going bot-only. |
| **Conservative** | `600`–`1200` | `60`–`300` | Customer-facing meetings or webinars where it's important to avoid leaving early. Strongly biases toward staying. |

Rule of thumb: **err on the side of larger values**. A false negative (bot stays when it should leave) is recoverable — the existing `everyone_left_timeout`, `in_call_recording_timeout`, and voice-inactivity fallbacks will eventually fire. A false positive (bot leaves while real people are still in the meeting) is not.

---

## 8) Self-identity: how the bot knows which row is itself

Each platform exposes a different stable identity for the bot's own row in the roster. You don't have to do anything — this is handled automatically — but it's useful to understand because it explains a subtle edge case in Google Meet.

| Platform | Self-identity used | Notes |
|---|---|---|
| **Google Meet** | `deviceId` (captured from the `(You)` marker in the Meet UI on join) | Captured non-blocking. If capture hasn't completed (rare), the check **fails closed** — it skips that tick and cancels any armed timer, then retries on the next tick. |
| **Microsoft Teams** | `bot_id` (stable from the bot's `TeamsBotConfig`) | Available as soon as the websocket is connected. |
| **Zoom** | `my_participant_id` returned by the Zoom SDK | Available as soon as the bot joins the participants list. |

Because of this, **naming your own bot to match a `matches` keyword is safe across all three platforms** — the self row is excluded by ID, never by name.

**Google Meet caveat:** in the rare case two of your own bots launch into the same meeting at almost the same instant *and* their self-identity capture races, the bot may briefly perceive its sibling as "another notetaker". The fail-closed safety means it will not leave on uncertain identity — but if you regularly send two bots to the same meeting, set `activate_after` ≥ 300s so the capture has fully settled by the time the check arms.

---

## 9) The other `automatic_leave` timeouts

`bot_detection` runs **alongside** the existing timeouts on the `automatic_leave` object — it doesn't replace any of them. They each cover a different failure mode and they can all be configured in the same payload:

```json
"automatic_leave": {
  "waiting_room_timeout": 600,
  "noone_joined_timeout": 600,
  "everyone_left_timeout": 300,
  "voice_inactivity_timeout": 100,
  "in_call_recording_timeout": 14400,
  "recording_permission_denied_timeout": 60,
  "bot_detection": { /* … */ }
}
```

Field-by-field reference below. All values are integer **seconds**.

### `waiting_room_timeout`

| | |
|---|---|
| **What it controls** | Max time the bot will sit in the waiting room / lobby before giving up and exiting. |
| **Range** | `60` – platform max (GMeet: `600`, Zoom: `1200`, Teams: `1800`). Out-of-range returns `HTTP 400`. |
| **Default** | `600` for GMeet/Zoom, `1200` for Teams. |
| **Triggers when** | The host has not admitted the bot before this timeout elapses. |
| **Terminal webhook** | `bot.notallowed` (the host never admitted the bot in time). |

Use a smaller value for ad-hoc bots so you don't keep a container alive waiting for an admit that's never coming. Use a larger value for scheduled bots where the host may take a few minutes to start the meeting.

### `noone_joined_timeout`

| | |
|---|---|
| **What it controls** | How long to stay in an empty meeting waiting for the first other participant to join. |
| **Range** | `60` – `1800`. Out-of-range returns `HTTP 400`. |
| **Default** | `600` for GMeet/Zoom, `1200` for Teams. |
| **Triggers when** | The bot has been admitted, but the participant count *besides the bot* has been `0` for this many seconds since join. |
| **Terminal webhook** | `bot.stopped` (graceful — nobody was coming). |

This handles the "host opened the meeting but the participants never showed up" case. It's distinct from `everyone_left_timeout`, which only kicks in after a participant joined and *then* left.

### `everyone_left_timeout`

| | |
|---|---|
| **What it controls** | How long to wait after the meeting goes empty (apart from the bot) before exiting. |
| **Range** | `60` – `1800`. Out-of-range returns `HTTP 400`. |
| **Default** | `600` for GMeet/Zoom, `1200` for Teams. |
| **Triggers when** | At least one other participant joined at some point and the roster has since dropped to bot-only. |
| **Terminal webhook** | `bot.stopped`. |

If you're using `bot_detection` (above), this is your **safety net** — it still fires if all the notetakers leave too, leaving the bot truly alone. The two cover different shapes of "the meeting is over": `bot_detection` for "humans gone, notetakers stayed", `everyone_left_timeout` for "everyone gone, including the notetakers".

### `voice_inactivity_timeout`

| | |
|---|---|
| **What it controls** | How long the bot tolerates total silence in a meeting where participants are present but nobody is speaking. |
| **Range** | Container-enforced. Sensible practical range is `60` – `1800`. |
| **Default** | `600` (10 minutes). |
| **Triggers when** | The bot is admitted, the roster is non-empty, but no audio activity has been observed for this many consecutive seconds. |
| **Terminal webhook** | `bot.stopped` with the message `Bot exited the call: Voice inactivity timeout`. |
| **Notes** | Useful for catching meetings that "ended" without anyone formally leaving — e.g. attendees walked away from their desks but kept their tab open. Independent of `bot_detection`: voice inactivity fires regardless of who is in the roster. |

### `in_call_recording_timeout`

| | |
|---|---|
| **What it controls** | Absolute hard cap on how long a single bot session can stay in the meeting once recording has started. |
| **Range** | `600` – `18000` (10 minutes to 5 hours). Out-of-range returns `HTTP 400`. |
| **Default** | `14400` (4 hours). |
| **Triggers when** | The cumulative time since the bot started recording exceeds this value. |
| **Terminal webhook** | `bot.stopped`. |

Treat this as a **failsafe**, not a knob you tune meeting-by-meeting. It exists so a stuck recording can't run for days against your credits. Leave the default unless you have a specific reason (e.g. multi-hour all-hands) to extend it.

### `recording_permission_denied_timeout` *(Zoom only)*

| | |
|---|---|
| **What it controls** | How long to wait for the host to act on Zoom's "allow recording" prompt before treating it as a denial and leaving. |
| **Range** | `60` – `300`. Out-of-range returns `HTTP 400`. |
| **Default** | `60`. |
| **Triggers when** | The bot has joined a Zoom meeting and asked for recording permission, but the host has neither granted nor denied within this window. |
| **Terminal webhook** | `bot.recording_permission_denied` → `bot.leaving` → `bot.stopped`. See the [Bot Lifecycle Webhook Events Guide](./MeetStream_Bot_Lifecycle_Webhook_Events_Guide.md#botrecording_permission_denied). |
| **Platforms** | Zoom only. The field is silently ignored on GMeet and Teams payloads. |

Most Zoom hosts respond to the recording prompt within a few seconds; the default of `60` is a good balance. Raise it only if your hosts routinely take a minute or more to acknowledge prompts.

---

### Putting them together

If multiple timeouts would fire at once, whichever expires first wins. In practice that means:

| Scenario | What ends the meeting |
|---|---|
| Host never admits the bot | `waiting_room_timeout` → `bot.notallowed` |
| Bot admitted, nobody else ever shows up | `noone_joined_timeout` → `bot.stopped` |
| Real meeting happens, everyone (including notetakers) eventually leaves | `everyone_left_timeout` → `bot.stopped` |
| Humans leave, vendor notetakers linger | `bot_detection` → `bot.stopped` (typically well before `everyone_left_timeout`) |
| Roster non-empty but room is silent | `voice_inactivity_timeout` → `bot.stopped` |
| Multi-hour meeting runs longer than expected | `in_call_recording_timeout` → `bot.stopped` (failsafe) |
| *(Zoom only)* Host doesn't respond to the recording prompt | `recording_permission_denied_timeout` → `bot.recording_permission_denied` → `bot.stopped` |

---

## 10) Common pitfalls

### "My bot is leaving meetings too early."

Most likely your `matches` list is too aggressive for your customer base — e.g. you have `"ai"` in there and a real participant named `"Ainsley Thompson"` is being treated as a bot.

- Substrings match anywhere in the name. Prefer specific brand fragments (`"otter.ai"`, not `"ai"`).
- Raise `activate_after` to give real people more time to arrive before the check arms.
- Raise `timeout` so a brief network blip on the human's side doesn't cause an early exit.

### "My bot isn't leaving even though only notetakers remain."

Three things to check, in order:

1. **Did you actually configure it?** The block is opt-in. Inspect the bot's stored config via `GET /api/v1/bots/{bot_id}` — the `automatic_leave` you see there is what the bot received.
2. **Are the notetakers' display names in the meeting actually matching?** Google Meet sometimes shows `"Unknown"` for the first ~10 seconds of a join before its name resolver populates a real name. While a participant is unnamed, they don't match — so the bot can't conclude "only bots remain". Wait for names to settle.
3. **Has `activate_after` elapsed?** The check is dormant during that window. If you set `activate_after: 1200` and the notetakers leave after only 600 seconds, the check never armed in the first place — `everyone_left_timeout` takes over instead.

### "How do I disable it on a scheduled bot?"

Send a PATCH to `/api/v1/calendar/scheduled_bots/{bot_id}` with the `bot_detection` key set to `{}`, or with `automatic_leave` set to an `automatic_leave` object that doesn't contain the `bot_detection` block. The next time the bot is launched from that schedule, the feature will be disabled.

---

## 11) FAQ

**Q: Is this on by default?**
A: No. It's strictly opt-in. Bots created without an `automatic_leave.bot_detection` block behave exactly as before.

**Q: Does it cost extra?**
A: No. There's no separate charge — you simply stop being billed for the meeting time you would otherwise spend waiting for `everyone_left_timeout`.

**Q: Can I change the keywords after the bot has joined?**
A: No. The config is locked in at bot creation (or at the most recent PATCH for a scheduled bot before the bot launches). There is no live-update path.

**Q: What happens if a human keeps their name as just an emoji or a single letter?**
A: Their name won't match any keyword (substring match requires at least one keyword to appear in the name). They will be treated as a human → the bot stays. False-positive-safe.

**Q: What if a bot in the meeting uses a non-Latin-script name?**
A: Substring matching is locale-agnostic — but the keywords you provide must literally appear in the participant's `displayName` as the platform reports it. If a notetaker joins with a name in Cyrillic or CJK, add those substrings to `matches` too.

**Q: Does it work for breakout rooms?**
A: The bot does not currently follow participants into breakout rooms. `bot_detection` operates on the main-room roster only.

**Q: Will the post-call transcript still be generated?**
A: Yes. A `bot_detection` exit is treated as a normal graceful leave — the post-call pipeline runs in full (audio upload, transcription, video processing, manifest).

**Q: Is the leave event distinguishable from a normal `bot.leaving`?**
A: The webhook event name is the same (`bot.leaving` → `bot.stopped`), but the underlying status update message includes the substring `bot_detection.using_participant_names`. If you need to attribute exits, parse the `message` field on the bot's status history.

---

## 12) Related docs

- [Create Bot Payload Reference](./MeetStream_Create_Bot_Payload_Reference.md) — full payload schema, including the `automatic_leave.bot_detection` block.
- [Bot Lifecycle Webhook Events](./MeetStream_Bot_Lifecycle_Webhook_Events_Guide.md) — the `bot.leaving` → `bot.stopped` sequence you'll observe when this fires.
- [Participant Events](./MeetStream_Participant_Events_Guide.md) — useful for auditing exactly which other participants were in the room at the moment the bot left.
- [Calendar Integration Guide](/guides/app-integrations/using-credentials-with-meetstream-api) — applying `bot_detection` to scheduled bots via PATCH.