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 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.
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:
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.
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.
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.”
All bot_detection fields live under automatic_leave.bot_detection on the create-bot payload.
automatic_leave.bot_detectionOther mechanisms from Recall.ai’s spec —
using_participant_events(no-speaker / no-screen-share heuristic) andsilence_detection(audio-energy fallback) — are not yet supported. Passing them returnsHTTP 400so we don’t silently ignore them.
automatic_leave.bot_detection.using_participant_namesmatches 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.bot_detection (or omit using_participant_names) to disable. The rest of automatic_leave continues to apply.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.Imagine a meeting that opens at t=0 and your bot is admitted at t=2s with activate_after=300, timeout=30:
What you see externally:
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.bot.kicked / bot.denied / bot.failed — this is a graceful, customer-configured exit, not a platform-side eject.audio.processed, transcription.processed, video.processed, and bot.done after the artifacts are ready.See Bot Lifecycle Webhook Events for the full webhook sequence.
matchesThe keywords below are a good starting baseline that catches most common notetakers in the wild. Substring matches, case-insensitive:
Tips:
"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."otter" matches "Otter.ai Notetaker", "OtterPilot", and "otter-bot" — you don’t need separate entries.lowercase(name).contains(keyword) per keyword. Special characters (., :, ;) are matched literally.activate_after and timeoutRule 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.
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.
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.
automatic_leave timeoutsbot_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:
Field-by-field reference below. All values are integer seconds.
waiting_room_timeoutUse 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_timeoutThis 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_timeoutIf 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_timeoutin_call_recording_timeoutTreat 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)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.
If multiple timeouts would fire at once, whichever expires first wins. In practice that means:
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.
"otter.ai", not "ai").activate_after to give real people more time to arrive before the check arms.timeout so a brief network blip on the human’s side doesn’t cause an early exit.Three things to check, in order:
GET /api/v1/bots/{bot_id} — the automatic_leave you see there is what the bot received."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.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.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.
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.
automatic_leave.bot_detection block.bot.leaving → bot.stopped sequence you’ll observe when this fires.bot_detection to scheduled bots via PATCH.