Design & media

Join meeting

Try it

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

What it does

Joins a video meeting as an AI bot with voice and visual presence. Supports four modes: voice-only, audio from a local webpage, a webpage that doubles as the camera feed, and avatar plus screenshare for slides or dashboards. Real-time transcript events stream in as agent input and the bot auto-exits on silence or alone-timeout. Includes an email-code self-registration flow that mints a key with free trial credits, and runs on Python 3.10+ or Node.js 18+.

When to use it

  • Send an AI agent into a meeting to take notes or answer questions
  • Present slides or walk through a dashboard with avatar plus screenshare
  • Connect an existing voice-agent webpage into a meeting via webpage-audio
  • Run an audio-only assistant in calls where no video is needed

The skill document

join-meeting

IMPORTANT: Read this entire document before joining a meeting. This file contains the CALL_LOOP algorithm (mandatory), active participation rules, safety requirements (leave/cleanup), and mode-specific guidance. Skipping sections will result in broken meeting experiences — the user will be left talking to silence.

IMPORTANT: Read the whole document on every session, not just the parts you remember. This skill is updated frequently — new commands, new events, new recommended patterns (like the event-driven tail -f + Monitor flow in "How to read events") are added often. Do NOT rely on what you remember from previous sessions. Re-read this document each time you start a meeting so you pick up the latest guidance. If unsure whether you are on the latest version, run python scripts/python/check_update.py (see "Checking for Skill Updates").

Join a video meeting as an AI bot with voice and visual presence.

Prerequisites

  • Python 3.10+ (preferred) or Node.js 18+
  • Python dependencies: pip install aiohttp websockets
  • Node.js dependencies: cd scripts/node && npm install
  • For webpage modes: a local HTTP server running on the specified port

API Key Setup

Before joining a meeting, make sure an API key is configured:

  1. Check ~/.agentcall/config.json — if it exists and has api_key, you're ready.

  2. Check AGENTCALL_API_KEY env var — if set, you're ready.

  3. If neither exists, get a key one of two ways:

    Option A — Self-register via email (no dashboard, no human hand-off needed). AgentCall accounts are created with a 6-digit email code. New accounts include free trial credits (base plan, 360 minutes), so the first call works immediately.

    # 1. Request a code. Use the user's email, or — if you can read your own
    #    mailbox — an address you have access to:
    python scripts/python/register.py send --email you@example.com
    
    # 2. Get the 6-digit code from that inbox. If you can read the mailbox yourself,
    #    retrieve it directly; otherwise ask the user to paste it. Codes expire in
    #    10 minutes; a resend is allowed after 60 seconds.
    
    # 3. Verify the code. This mints an API key named "AgentCall Skill on "
    #    and saves it to ~/.agentcall/config.json:
    python scripts/python/register.py verify --email you@example.com --code 123456
    

    Node equivalent: node scripts/node/register.js send --email ... and node scripts/node/register.js verify --email ... --code .... Both scripts use only the language standard library, so they run before pip install / npm install.

    Option B — Use an existing key. Ask the user for their API key (create one at https://app.agentcall.dev/api-keys), then save it:

    mkdir -p ~/.agentcall
    cat > ~/.agentcall/config.json << 'EOF'
    {"api_key": "USER_KEY_HERE"}
    EOF
    

The scripts (bridge.py, join.py, agentcall.py, register.py) automatically read from ~/.agentcall/config.json if AGENTCALL_API_KEY env var is not set. Do NOT ask for the API key every session — check the config file first.

Meeting transcripts arrive as agent input — any participant in the call can therefore steer the agent. For high-trust workflows, configure your agent framework's permission system (e.g., Claude Code's allow allowlist, hooks, plan mode) to restrict what the agent can do during a call. The skill defers to the framework's enforcement. Recommended for use in trusted meetings or properly scoped projects.

User Preferences

First-call detection: if ~/.agentcall/config.json has no default_mode field saved, treat this as the user's first call.

First call (no default_mode in ~/.agentcall/config.json): new accounts include free trial credits. Offer the user a brief "experience call" with --mode webpage-av-screenshare --voice-strategy direct so they can see the full feature set — the pattern avatar (default), screenshare, interactive webpages, and voice with barge-in. If they prefer a simpler mode, honor that. After the call ends, ask which mode to save as the default going forward.

After the first call ends (in the agent conversation, not the meeting): Ask the user which mode to save as their default going forward. Present as a numbered list:

  1. webpage-av-screenshare — everything on tap (avatar + screenshare + webpage sharing)
  2. webpage-av — avatar only, no screenshare
  3. webpage-audio — audio from a webpage into the meeting
  4. audio — voice only, simplest

Offer to explain any option if the user wants clarification. Mention they can see real-world examples at https://www.youtube.com/@pattern-ai-labs.

Save the choice to ~/.agentcall/config.json:

{
  "api_key": "ak_ac_xxxxx",
  "default_mode": "webpage-av-screenshare",
  "default_voice_strategy": "direct",
  "default_voice": "af_heart",
  "default_bot_name": "Juno"
}
  • Subsequent sessions: use saved defaults silently. No need to ask again.
  • Override anytime: if the user says "join with avatar this time" or "use audio mode", respect it for that call without updating the saved default. Only update the default if the user says "always use this" or "make this my default."
  • These are soft defaults, not rigid settings. The user's in-context request always takes priority over saved preferences.
  • All plan tiers (base, pro, enterprise) follow the same flow — everyone gets the first-call demo and the post-call prompt.

Usage

./scripts/run.sh  [options]

Options

OptionDefaultDescription
--modeaudioaudio (voice only, simplest), webpage-audio (audio from webpage), webpage-av (visual avatar), webpage-av-screenshare (avatar + screenshare). See Modes Explained below.
--voice-strategydirectcollaborative, direct
--bot-nameAgentDisplay name in the meeting participant list
--port3000Local port for webpage modes (your UI server)
--screenshare-port3001Local port for screenshare content
--templatepatternBuilt-in UI: pattern (default, radial sunburst with per-state colors and the work-in-progress task list), ring (neon ring), orb, avatar, dashboard, blank, voice-agent (no local server needed)
--transcriptiononReal-time transcript.final and transcript.partial events. Required for most workflows. Disable with --no-transcription to save STT billing if you only need lifecycle events.
--trigger-wordsComma-separated aliases for collaborative mode: june,juno,hey june
--contextInitial context for voice intelligence (max 4000 chars)
--webpage-urlPublic URL for webpage modes (no tunnel needed)
--screenshare-urlPublic URL for screenshare content (no tunnel needed)
--max-durationplan limitMax call duration in minutes. Cannot exceed your plan's limit. Check https://agentcall.dev for current limits.
--alone-timeout120Leave if alone for N seconds.
--silence-timeout300Leave if silent for N seconds.
--api-urlhttps://api.agentcall.devOverride API URL for development

Bot Naming

Choose STT-friendly names — short, distinctive, real-sounding words that speech-to-text can reliably capture. Avoid generic phrases like "AI Assistant" or "Hey Bot" — transcription often garbles these.

Good names: Juno, June, Nova, Sage, Atlas, Claude, Aria, Echo Avoid: AI Assistant, My Bot, Hey Agent, Assistant Bot

Always set trigger words in collaborative mode to cover STT mishearings:

--bot-name "Juno" --trigger-words "juno,june,you know,junior"
--bot-name "Claude" --trigger-words "claude,cloud,clod,clawed"
--bot-name "Nova" --trigger-words "nova,no va,over"

The display name in the participant list can be longer (e.g., "Juno - AI Assistant") but the trigger words should be the short phonetic variants that STT might produce.

Modes Explained

audio (default)

Voice only. Bot has no video. Best for: AI assistants, note-takers, voice agents. No local server needed. Simplest setup.

webpage-audio

Your local webpage provides audio. Bot's video is black. The webpage can play audio that meeting participants will hear. Best for: audio-only web apps. Requires: --port pointing to your local HTTP server.

If your webpage is publicly hosted, pass --webpage-url https://your-site.com/bot instead of --port. No tunnel or local server needed.

webpage-av

Your webpage IS the bot's video feed — what renders on the page is what meeting participants see as the bot's camera. Audio from the page is also captured into the meeting. The page is loaded once and runs continuously. All updates must come via WebSocket events from your agent — it does not auto-refresh.

Best for: animated avatars, branded visual presence, agent-controlled dynamic UIs.

The webpage can also be a standalone voice-to-voice agent: it receives the meeting's audio as microphone input, processes it with its own AI backend, and replies through the browser's speaker — which FirstCall (meeting infrastructure) captures into the meeting. This means any existing voice agent webpage can join meetings with zero modification.

Keep it simple. The agent controls the page via WebSocket. The page renders what the agent tells it to. Use --template orb or --template avatar for built-in options.

For slides or screen-sharing content, use webpage-av-screenshare instead.

webpage-av-screenshare

Same as webpage-av PLUS the ability to screenshare. Bot has two visual presences:

  • Camera feed — your avatar/brand page (always active, receives meeting audio via mic)
  • Screenshare — separate content page, inactive until you send screenshare.start

Screenshare starts inactive. The bot joins with only the avatar visible. Screenshare activates when the agent sends screenshare.start with a URL or port. If you don't need screenshare at all, use webpage-av mode instead.

Bot has two visual presences when screenshare is active:

  • Camera feed — your avatar/brand page (receives meeting audio via mic)
  • Screenshare — separate content page (slides, charts, docs, demos)

Meeting audio is routed ONLY to the avatar page (not screenshare). Audio from both pages is captured into the meeting.

Agent controls screenshare dynamically during the call:

  • screenshare.start with url — share a public URL: {"command": "screenshare.start", "url": "https://slides.google.com/..."}
  • screenshare.start with port — share a local server via tunnel: {"command": "screenshare.start", "port": 3001}
  • screenshare.stop — stop sharing: {"command": "screenshare.stop"}
  • screenshare.swap — atomically swap to a different page: {"command": "screenshare.swap", "port": 3002} or {"command": "screenshare.swap", "url": "https://..."}. Use this instead of stop+start when changing what's shared during a call — it serializes the stop and waits for FirstCall to confirm before starting the new share, which avoids races and the "old content keeps showing" bug.

Requires: --port AND --screenshare-port (local), or --webpage-url AND --screenshare-url (public, no tunnel).

IMPORTANT — screenshare is a live, agent-controlled canvas: Once loaded, the screenshare page cannot be clicked, scrolled, or typed into by anyone — it runs in a headless browser. The agent controls what's on screen by updating files or API responses on its local server — the page polls for changes via HTTP (every 2 seconds) through the tunnel and re-renders automatically.

Design for 1280x720 viewport. FirstCall's headless browser renders at this resolution. Use large fonts (40px+ for headings, 24px+ for body text) so content is readable in the meeting participant's screenshare view.

Live screenshare pattern — for slides, dashboards, or any dynamic content:

  1. Create an HTML page with a polling loop that fetches /state.json every 2s
  2. Create a state.json file that holds the current state (e.g., {"slide": 0})
  3. Serve both from a local HTTP server via python -m http.server
  4. Start screenshare with port — tunnel proxies HTTP to your localhost
  5. To update: write new state to state.json — the page picks it up within 2s
Agent: "Let me show you the Q3 numbers."
  → agent creates /tmp/screenshare/index.html + state.json
  → agent starts: python -m http.server 3001 --directory /tmp/screenshare/
  → agent sends: {"command": "screenshare.start", "port": 3001}

Agent: "Moving to the next slide."
  → agent writes: echo '{"slide": 1}' > /tmp/screenshare/state.json
  → page polls, detects change, renders slide 2

Agent: "Here's the revenue chart."
  → agent writes: echo '{"slide": 2}' > /tmp/screenshare/state.json
  → page renders the chart slide

This makes the screenshare a real-time visual companion to the agent's voice, fully synchronized — the agent narrates while updating files that control what everyone sees. No WebSocket needed — all updates flow via HTTP through the tunnel. See Webpage AV Screenshare Guide for full HTML snippet and examples.

Bonus feature — share an interactive webpage with participants. This mode also supports webpage.open, which exposes a page from your localhost via a shareable URL. Participants open it in their own browser (fully interactive — clickable, scrollable, can type and submit forms). This is NOT a screenshare (headless, in-meeting only) — it is a shareable link the agent builds and sends to participants. Ideal for agent-generated dashboards, reports, forms, interactive code diffs, and any content you want participants to actually click. The tunnel closes automatically when the call ends. See "Sharing a live webpage" under Pattern 5 for commands, events, and the full workflow.

Which mode should I use?

NeedModeWhy
Voice only, no videoaudioSimplest. No webpage, no tunnel.
Audio from a webpagewebpage-audioWebpage plays audio into meeting.
Visual avatar/brandwebpage-avYour page = bot's camera feed.
Avatar + might screensharewebpage-av-screenshareAvatar always on. Screenshare on demand.

Rule of thumb: For first-time users (no default_mode in ~/.agentcall/config.json), always use webpage-av-screenshare to showcase the full experience — see User Preferences section for the first-call demo flow. For returning users, use their saved default_mode. In general, start with audio if no preference is known. Add webpage-av if you need visual presence. Add webpage-av-screenshare only if the agent will share content (slides, charts, demos) during the call. Screenshare is always dynamic — activated via screenshare.start command, not at call creation.

Need participants to interact with something (not just see it)? Use webpage-av-screenshare mode and the webpage.open command. The agent serves a page from its localhost; participants open the shareable URL in their own browser — clickable, scrollable, fillable. Different from screenshare (which is a headless view only). Examples: a form to collect meeting feedback, a dashboard participants can drill into, a code diff viewer. See "Sharing a live webpage" in Pattern 5 for commands and workflow.

How the Tunnel Works (Webpage Modes)

For webpage modes, AgentCall creates a secure tunnel from the cloud to your localhost:

  1. You run a local HTTP server (or use --template which starts one automatically).
  2. The bridge script connects a tunnel client to AgentCall's tunnel server via WebSocket.
  3. The bot's browser (running in the cloud) loads your page via the tunnel URL.
  4. HTTP requests to the tunnel URL are proxied through the tunnel to your localhost.

You do NOT need to expose your machine to the internet. The tunnel handles it. When using --template, the bridge starts a local server and tunnel automatically — no manual setup. When using --webpage-url (public URL), no tunnel is needed — FirstCall loads it directly.

Port conflicts: Before starting a local server on a specific port, verify it's available: lsof -i :PORT. If another process (e.g., Node.js on port 3000) is already bound, the tunnel will proxy to the wrong server, causing unexpected 404 errors. Use a different port or use --template which auto-selects a free port.

Tunnel Authentication

When creating a call with ui_port, the API response includes:

  • tunnel_id — unique identifier for this tunnel
  • tunnel_access_key — per-call credential for tunnel authentication
  • tunnel_url — the public URL where FirstCall loads your page

The tunnel client registers with the server using tunnel_id + tunnel_access_key.

IMPORTANT: The tunnel_access_key is NOT your API key (ak_ac_...). It is a separate, per-call credential generated specifically for tunnel authentication. Using your API key will fail with an error message explaining the correct credential to use. If using bridge-visual.py, this is handled automatically.

Mic Permissions (Webpage Modes)

In all webpage modes, FirstCall (meeting infrastructure) automatically grants microphone permission to your page. Your webpage receives the meeting's audio as browser microphone input.

Important: Your page MUST start mic recording automatically on load — no button clicks, no user interaction. FirstCall (meeting infrastructure) loads your page in the rendering environment and cannot interact with UI elements. Use navigator.mediaDevices.getUserMedia({ audio: true }) on page load or in a script that runs immediately.

// Auto-start mic on page load (required for voice agent webpages)
window.addEventListener('load', async () => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  // stream now has meeting audio — process it with your AI
});

Meeting audio is routed to the main avatar page only. In screenshare mode, the screenshare page does NOT receive mic input.

Voice Strategies Explained

collaborative (group meetings)

Uses GetSun as a speech intelligence layer — it handles real-time voice timing (trigger words, barge-in, interruptions) so the agent doesn't need sub-second response times. But the agent is still the brain: it provides context, injects data, triggers responses, and does all the thinking. GetSun is the mouth, not the mind. The bot:

  • Listens for its name (or trigger words) before responding
  • Waits for silence before speaking (barge-in prevention)
  • Stops immediately if interrupted (uses full text for smart interruption handling)
  • Handles follow-up questions for 20 seconds after responding
  • Has 4 built-in voices: voice.heart (F), voice.bella (F), voice.echo (M), voice.eric (M)

Configuration (collaborative field in call creation):

ParamDefaultDescription
trigger_words[]Alternate names (handles STT mishearing): ["june", "juno"]
barge_in_preventiontrueWait for silence before speaking
interruption_use_full_texttrueUse full text during interruptions for smarter responses
context""Initial knowledge scratchpad (4000 chars max)
voicevoice.heartTTS voice: voice.heart, voice.bella, voice.echo, voice.eric

Your agent receives transcript.final events only (not partials — GetSun (collaborative voice intelligence) handles those). Use the context-first pattern:

  1. Receive transcript.final
  2. Fetch/compute data
  3. Send voice.context_update with the data
  4. Send trigger.speak with the original question
  5. GetSun (collaborative voice intelligence) answers naturally from context

Announcing background task results:

  1. Complete the task (analysis, lookup, etc.)
  2. Send voice.context_update with the full results data
  3. Send inject.natural with a SHORT announcement: "I've got the results" (1 sentence only)
  4. GetSun speaks the announcement at the next natural pause
  5. User asks follow-up → GetSun answers from context instantly

See Collaborative Mode Guide for detailed examples, context patterns, and full working code.

Collaborative Mode: Working with GetSun

GetSun responds in <1 second — the agent will NEVER beat it to a response. This means the agent's job is to prepare GetSun's context in advance so GetSun says the right thing when a participant speaks.

1. Pre-load capabilities on join:

Immediately after call.bot_ready, send a voice.context_update that tells GetSun what the agent can do. This is the most important step — without it, GetSun will refuse requests the agent could handle.

voice.context_update: "You are [bot_name], an AI assistant in this meeting.
You have a backend agent that can: search code, edit files, run commands,
check deployments, browse documentation, analyze data, and more.
When asked to do something:
- If you know the answer from context, respond directly.
- If you need to check or look something up, say 'Let me check on that'
  or 'Sure, give me a moment.' The agent will handle the work and provide
  results for you to speak.
- NEVER say 'I can't do that' or 'I don't have access to that.'
  The agent behind you has the tools — always defer to it."

Customize the capabilities list based on what your agent framework can actually do (code search, web browsing, API calls, file editing, etc.).

2. GetSun will always speak first:

When a participant asks something:

User: "Juno, what's the deployment status?"
GetSun (instant, from context): "Sure, let me check on that for you."
Agent (sees transcript.final): runs health check → gets result
Agent: sends voice.context_update with full deployment data
Agent: sends inject.natural "I've got the deployment status." (short announcement)
GetSun: speaks the announcement at next pause
User: "What's the status?" → GetSun answers from context instantly

The pattern is always: GetSun buys time → Agent does work → Agent updates context + announces.

3. Handle capability mismatches:

If GetSun says "I can't do that" (because the context didn't cover this capability):

  • Agent sees voice.text with GetSun's refusal
  • Agent sends voice.context_update adding the missing capability
  • Agent sends inject.natural with a correction: "Actually, I can help with that. Let me take a look."
  • This teaches GetSun for the rest of the session — it won't refuse the same thing again

4. Keep context fresh:

After completing any task, update GetSun's context with the results:

Agent completes deployment check →
voice.context_update: "Latest deployment status: all services healthy,
last deploy 2 hours ago, 3 pods running, 0 errors in last 30 min."

Now if a participant asks a follow-up ("Any errors?"), GetSun answers instantly from context without the agent needing to run another check.

5. Ongoing conversation awareness:

The agent should monitor voice.text events (what GetSun said) AND transcript.final events (what participants said). This gives the agent full awareness of the conversation — both sides. Use this to:

  • Detect when GetSun deferred ("let me check") → agent must act
  • Detect when GetSun answered from context → no action needed
  • Detect when participants discuss topics the agent has context on → proactively update context

6. Predictive context updates:

The agent can preemptively update GetSun's context with data relevant to the current discussion — BEFORE anyone asks. This makes GetSun answer instantly (<1s) instead of deferring ("let me check") and waiting for the agent.

Participants discussing deployment...
Agent: (I have deployment data) → context_update with deployment status
User: "Juno, is the deployment healthy?"
GetSun: answers instantly from context — no delay, no "let me check"

When to preload context:

  • The conversation shifts to a topic the agent has data on
  • The agent just completed a task — results may be relevant to ongoing discussion
  • The agent recognizes a pattern (e.g., participants keep asking about metrics)
  • The agent knows the meeting agenda and can preload relevant data

context_update is silent — GetSun absorbs it without speaking. If nobody asks about the preloaded topic, no harm done. If someone does ask, GetSun answers instantly. The 4000-char limit means the agent must prioritize — preload data most relevant to the current discussion, not everything it knows.

direct (full control)

No voice intelligence. Your agent controls everything:

  • transcript.final — completed utterances
  • transcript.partial — in-progress transcription (someone is still talking)
  • active_speaker — who's talking

Your agent decides when to speak using:

  • tts.speak — AgentCall TTS (54 voices, 9 languages, <1s latency)
  • audio.inject — raw PCM 16kHz 16-bit mono (your own audio pipeline)

You are responsible for turn-taking and timing.

Available TTS voices (direct mode):

Voice IDNameLanguageGender
af_heartHearten-usFemale
af_bellaBellaen-usFemale
af_sarahSarahen-usFemale
af_nicoleNicoleen-usFemale
am_adamAdamen-usMale
am_michaelMichaelen-usMale
bf_emmaEmmaen-gbFemale
bf_isabellaIsabellaen-gbFemale
bm_georgeGeorgeen-gbMale
bm_lewisLewisen-gbMale

Voice ID convention: {language}{gender}_{name}af_ = American Female, am_ = American Male, bf_ = British Female, bm_ = British Male. For the full list of all available voices, query GET /v1/tts/voices.

Collaborative mode voices (GetSun, different naming from direct): voice.heart (F), voice.bella (F), voice.echo (M), voice.eric (M). Direct mode voices (Kokoro TTS) use af_heart, am_adam etc. — these are different systems, names are NOT interchangeable.

Barge-in prevention: in direct mode, the bridge automatically holds tts.speak until the human finishes speaking — you don't need to gate it yourself. If the human keeps speaking for more than 10 seconds while the bot has queued speech, the bridge politely raises the bot's hand (and in webpage modes, flips the avatar to "waiting_to_speak") so participants see the bot has something to say. In collaborative mode, GetSun handles all of this.

For 1:1 conversations (customer support, interviews, tutoring): Use direct mode and respond to every transcript.final. The bridge handles barge-in prevention automatically — just send tts.speak and the bridge will wait for silence before delivering the audio. Tips:

  • First audio reaches the meeting in <1s automatically — send your response in one tts.speak
  • Transcripts are ALWAYS from human participants — never the bot itself
  • transcript.final is NOT dropped during bot speech — if a user speaks while the bot is talking, you will receive their message

Interruption handling (webpage modes 2-4, direct mode): In webpage modes, interruption is automatic but debounced — a single transcript.partial doesn't cut the bot off (too easily triggered by mic noise, brief fillers, or acknowledgments like "mhm"). The webpage pauses the bot's audio on the first partial, flips the avatar to "interrupted" (red) for immediate visual feedback, and waits up to 2 seconds for sustained speech (about 2 spoken words). If sustained, the audio is cleared and tts.interrupted is delivered to the agent with played and not_played sentence lists:

{
  "event": "tts.interrupted",
  "reason": "user_speaking",
  "played":     ["Hello there.", "How are you?"],
  "not_played": ["I was about to ask...", "...something important."]
}
  • played — sentences the participant heard in full
  • not_played — sentences cut mid-way OR queued but never started

If the wait window expires without sustained speech, the bot resumes playing from where it paused and the avatar flips back to "speaking" — false alarm, no tts.interrupted fires.

On confirmed interruption the avatar stays "interrupted" until the next state-changing event takes over (typically auto-thinking when user.message arrives, or the bot's next tts.speak).

The agent decides what to do based on the lists:

  • Skip already-played material; rephrase or continue with the not_played content
  • Generate a new response incorporating what the user said
  • Acknowledge the interruption: "Sorry, go ahead"

(Collaborative mode interruption — GetSun-driven — is unchanged: tts.audio_clear from the backend bypasses the debounce.)

Interruption in audio mode (mode 1): No automatic interruption — the bot's audio is injected directly into FirstCall. The agent still receives transcript.final during bot speech and can send audio.clear to stop playback. See Interruption Handling Guide.

Events (stdout)

Each line is a JSON object.

Event key convention: Lifecycle events use the "event" field. Transcription, meeting, and media events use the "type" field. Always check both: event.get("event") or event.get("type") (Python) or event.event || event.type (JS). Tip: bridge.py normalizes all events to use the "event" field — if using join.py directly, always check both fields.

Startup time: After creating a call, the bot takes 30-90 seconds to join the meeting (varies by platform — Google Meet is fastest, Teams/Zoom can take longer). During this time you'll see lifecycle events: call.createdcall.bot_joiningcall.bot_joining_meetingcall.bot_ready. The agent MUST wait patiently and NOT timeout or assume failure during this window. If using bridge.py, it handles this automatically — the agent simply waits for the first user.message or greeting.prompt event.

Lifecycle

{"event": "call.created", "call_id": "call-xxx", "ws_url": "wss://...", "status": "bot_joining"}
{"event": "call.tunnel_ready", "call_id": "call-xxx"}
{"event": "call.bot_joining", "call_id": "call-xxx"}
{"event": "call.bot_joining_meeting", "call_id": "call-xxx", "detail": "starting"}
{"event": "call.bot_joining_meeting", "call_id": "call-xxx", "detail": "joining"}
{"event": "call.bot_joining_meeting", "call_id": "call-xxx", "detail": "initializing"}
{"event": "call.bot_waiting_room", "call_id": "call-xxx"}
{"event": "call.bot_ready", "call_id": "call-xxx"}
{"event": "call.ended", "call_id": "call-xxx", "reason": "meeting_ended", "duration_minutes": 45}
{"event": "call.state", "call_id": "call-xxx", "status": "bot_ready", "mode": "audio", "voice_strategy": "collaborative", "created_at": "..."}
{"event": "call.transcript_ready", "call_id": "call-xxx", "transcript_url": "...", "expires_at": "..."}
{"event": "call.max_duration_warning", "call_id": "call-xxx", "minutes_remaining": 5}
{"event": "call.credits_low", "call_id": "call-xxx", "balance_microcents": 500000, "estimated_minutes_remaining": 12}
{"event": "call.degraded", "reason": "voice_disconnected"}
{"event": "call.recovered", "reason": "voice_reconnected"}

call.bot_joining means AgentCall has requested the bot — the meeting infrastructure is spinning up. call.bot_joining_meeting means the bot is actively entering the meeting (launching browser, navigating to URL, initializing audio). The detail field shows the sub-step: startingjoininginitializing. These fire in rapid succession — no action needed, just progress indicators.

call.max_duration_warning fires 5 minutes before the call reaches its plan's max duration limit. Default action: create a new call for the same meeting URL. The new bot joins while the old one is still active. When the old bot leaves (max_duration), the new bot continues seamlessly. Your session context is preserved — no information is lost. The concurrent limit allows one extra call during this 5-minute grace window so the rejoin works even on the base plan. If the user is on the base plan (1hr max), mention that Pro offers 4hr max duration — upgrade at https://app.agentcall.dev/add-credits ($29/mo subscription or recharge $50+ for 30 days of Pro).

call.credits_low fires at call start if the user's credit balance is below $1. Includes balance_microcents (raw balance) and estimated_minutes_remaining (calculated from current call's mode and add-on rates). The agent should inform the user and suggest recharging at https://app.agentcall.dev/add-credits. Credits low does NOT terminate the call — the call continues and credits can go negative. This is a courtesy warning, not a cutoff.

call.degraded means a backend service disconnected (e.g., voice intelligence). The bot is still in the meeting and transcripts still flow. In collaborative mode, voice commands (inject.natural, trigger.speak) may not work until call.recovered. In direct mode, tts.speak is unaffected. No action needed — the system auto-recovers.

call.state is sent on every WS connect/reconnect — use it to restore agent state after crash recovery. call.bot_waiting_room means the meeting has a lobby — the bot is waiting to be admitted by the host. Do NOT send any commands (tts.speak, send_chat, etc.) during this state — no one will hear or see them and they are not queued.

IMPORTANT: Even after call.bot_ready, wait for at least one participant.joined event before sending any commands. If the bot is admitted but no participants have joined yet, the bot is alone in the meeting — no one will hear what it says. Always wait for a participant before speaking or sending data.

Transcription

{"type": "transcript.final", "text": "What do you think about Q3?", "speaker": {"id": "p-1", "name": "Alice"}, "timestamp": "2026-03-25T10:05:23.456Z"}
{"type": "transcript.partial", "text": "What do you thi", "speaker": {"id": "p-1", "name": "Alice"}, "timestamp": "2026-03-25T10:05:22.100Z"}

Note: transcript.partial in direct mode only. Includes speaker.id, speaker.name, and timestamp.

Meeting Awareness

{"type": "participant.joined", "participant": {"id": "p-1", "name": "Alice"}, "participants": [{"id": "p-1", "name": "Alice"}]}
{"type": "participant.left", "participant": {"id": "p-2", "name": "Bob"}, "participants": [{"id": "p-1", "name": "Alice"}]}
{"type": "active_speaker", "speaker": {"id": "p-1", "name": "Alice"}}
{"type": "chat.message", "sender": "Alice", "message": "Can everyone hear me?", "message_id": "msg-123"}

Voice State (collaborative only)

{"event": "voice.state", "state": "listening"}
{"event": "voice.text", "text": "The revenue was 2.4 million dollars."}

7 states (collaborative mode only — GetSun (collaborative voice intelligence)):

StateMeaning
listeningDefault — hearing the conversation, not engaged
actively_listeningTrigger word detected, capturing the full question
thinkingProcessing a response
waiting_to_speakResponse ready, waiting for silence (barge-in prevention)
speakingSpeaking via TTS
interruptedSomeone talked over the bot, stopped speaking
contextually_awareJust responded — actively monitoring conversation for follow-up questions or related discussion. Lasts ~20 seconds after speaking.

voice.text shows each sentence the bot is speaking (for agent awareness).

TTS Events

{"event": "tts.started", "destination": "meeting"}
{"event": "tts.done", "destination": "meeting"}
{"event": "tts.audio", "data": "base64-pcm-24khz...", "chunk_index": 0, "is_last": false, "duration_ms": 2500}
{"event": "tts.webpage_audio", "data": "base64-pcm-24khz..."}
{"event": "tts.error", "reason": "tts_unavailable"}
{"event": "tts.interrupted", "reason": "user_speaking", "played": ["..."], "not_played": ["..."]}
  • tts.started/done — bracket TTS generation with destination info.
  • tts.interrupted — bot audio was stopped because a human started sustained speech (webpage modes, direct only). played lists sentences the participant heard fully; not_played lists sentences cut mid-way or never started. See "Interruption handling" above for the full debounce mechanic.
  • tts.audio — raw 24kHz PCM chunks returned to agent (when destination: "agent").
  • tts.webpage_audio — audio sent to webpage via tunnel (when destination: "webpage").

Media

{"type": "audio.chunk", "data": "base64-pcm-16khz...", "timestamp": "..."}
{"type": "screenshot.result", "data": "base64-jpeg...", "width": 1920, "height": 1080, "request_id": "req-1"}
{"type": "capture.started", "interval_ms": 1000}
{"type": "capture.frame", "data": "base64-jpeg...", "frame_number": 5}
{"type": "capture.stopped", "total_frames": 30}
{"type": "screenshare.started", "url": "https://..."}
{"type": "screenshare.stopped"}
{"type": "screenshare.error", "message": "Failed to load URL"}

audio.chunk requires audio_streaming: true in the call creation request (REST API only — not available as a CLI flag). This streams raw 16kHz PCM meeting audio to the agent. Most workflows don't need this — use transcript.final instead. See the Multilingual Note-Taker example for a use case.

System

{"type": "command.ack", "command": "meeting.send_chat", "request_id": "req-1"}
{"type": "command.error", "message": "Bot container not connected", "command": "meeting.send_chat"}

Commands (stdin)

Send one JSON object per line.

Voice Intelligence (collaborative only)

{"type": "inject.natural", "text": "Q3 revenue was $2.4M, up 15%", "priority": "normal"}
{"type": "inject.verbatim", "text": "The meeting will end in 5 minutes.", "priority": "high"}
{"type": "trigger.speak", "text": "Tell me about the financial results", "speaker": "Alice"}
{"type": "voice.contribute"}
{"type": "voice.context_update", "text": "Q3 Revenue: $2.4M, up 15% YoY. Enterprise: $1.6M..."}

trigger.speak — Conversational. Forces GetSun to respond to the text as if asked. If interrupted, content is LOST — GetSun moves on (like a person being cut off). Use for: answering direct questions, conversational replies. inject.natural — High reliability. GetSun rephrases your text and speaks it at the next natural pause. If interrupted, GetSun remembers and retries until fully spoken. Keep inject text SHORT (1 sentence). Long inject text that gets interrupted causes a retry loop — GetSun keeps coming back to finish, which feels robotic. Use for: short announcements only ("I've got the results", "Task complete", "I found the issue"). Do NOT dump data into inject — put data in context_update, announce with inject. inject.verbatim — Same as inject.natural but speaks exact text without rephrasing. Same retry behavior — keep it short. voice.contribute — GetSun reads the conversation and contributes something relevant from its context at the next natural pause, without being addressed by name. Use when the conversation topic matches data in the bot's context. voice.context_update — Replaces GetSun's context scratchpad (4000 chars max). This is where ALL data goes. Context is queryable — participants can ask follow-up questions and GetSun answers from context instantly. Context is NOT conversation memory. GetSun remembers the conversation separately.

Correct pattern for delivering results:

1. context_update → full data (deployment status, revenue numbers, etc.)
2. inject.natural → short announcement: "I've got the deployment status ready."
3. User asks follow-up → GetSun answers from context instantly

Anti-pattern (DO NOT do this):

inject.natural "The deployment is healthy. All 3 services running. Last deploy 2 hours
ago. No errors in 30 minutes. CPU 45%. Memory 62%..."
→ Long inject gets interrupted → GetSun retries → interrupted again → poo

Questions people ask

How does the bot leave a meeting?
It auto-exits after the configured alone-timeout (default 120s) or silence-timeout (default 300s), and is capped by the plan's max-duration limit.
Do I need to host my own UI?
Only for the webpage modes. Built-in templates (pattern, ring, orb, avatar, dashboard, blank, voice-agent) cover common cases; otherwise point --port or --webpage-url at your own server.
How is the API key obtained?
Self-register with an email plus 6-digit code to mint a key with trial credits, or paste an existing key into ~/.agentcall/config.json.

Related skills

Manage Google Meet spaces, records, participants, recordings, and transcripts with managed OAuth.

by byungkyu

Google Meet (workspace.google.com). Use this skill for ANY Google Meet request — reading, creating, and updating data. Whenever a task involves Google Meet, use this skill instead of calling the API directly.

Give AI agents Every meeting becomes searchable. using BlueColumn persistent memory. Use when an agent takes meeting notes and needs recall; when the user wants to store, recall, or search meeting memory context. Requires a BlueColumn API key (bc_live_*).

13 installs

AI会议助手(Meeting Join)免费版,让AI机器人以语音方式加入Google Meet、Microsoft Teams、Zoom等视频会议平台,支持实时语音转写、智能问答和会议纪要自动生成。机器人加入会议后可全程聆听并转写内容,会议结束后提取关键决策、待办事项与责任人,输出Markdown格式纪要便于归档与分享。适合需要自动记录和整理meeting内容的个人用户。

1 installs

Manage scheduled Zoom meetings, cloud recordings, and account users from agent workflows via Server-to-Server OAuth.

21 installs1 stars

Debrief after an AI agent phone call — compare outcome to goal, capture commitments, flag escalations, and produce follow-up tasks and CRM-ready notes.

2 installs