---
name: join-meeting
description: >
  AgentCall (agentcall.dev) — Join a video meeting (Google Meet, Teams, Zoom)
  as an AI bot with voice and visual presence. Supports audio-only mode with
  voice intelligence (barge-in, interruptions), text-to-speech mode, and
  webpage modes for custom UI. Use when asked to join a call, attend a meeting,
  or participate in a video conference.
argument-hint: <meet-url> [--mode audio|webpage-av] [--voice-strategy collaborative|direct] [--port PORT]
license: MIT
---

# 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, check if the API key is already 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**, ask the user for their API key:
   - Register at https://app.agentcall.dev/login (Google OAuth)
   - Get API key at https://app.agentcall.dev/api-keys
   - Add credits at https://app.agentcall.dev/add-credits (base plan includes 360 minutes)
4. **Save the key** so it persists across sessions:
```bash
mkdir -p ~/.agentcall
cat > ~/.agentcall/config.json << 'EOF'
{"api_key": "USER_KEY_HERE"}
EOF
```

The scripts (bridge.py, join.py, agentcall.py) automatically read from
`~/.agentcall/config.json` if `AGENTCALL_API_KEY` env var is not set.
**Do NOT ask the user 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`:

```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

```bash
./scripts/run.sh <meet-url> [options]
```

## Options

| Option | Default | Description |
|--------|---------|-------------|
| `--mode` | `audio` | `audio` (voice only, simplest), `webpage-audio` (audio from webpage), `webpage-av` (visual avatar), `webpage-av-screenshare` (avatar + screenshare). See Modes Explained below. |
| `--voice-strategy` | `direct` | `collaborative`, `direct` |
| `--bot-name` | `Agent` | Display name in the meeting participant list |
| `--port` | `3000` | Local port for webpage modes (your UI server) |
| `--screenshare-port` | `3001` | Local port for screenshare content |
| `--template` | `pattern` | Built-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) |
| `--transcription` | on | Real-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-words` | | Comma-separated aliases for collaborative mode: `june,juno,hey june` |
| `--context` | | Initial context for voice intelligence (max 4000 chars) |
| `--webpage-url` | | Public URL for webpage modes (no tunnel needed) |
| `--screenshare-url` | | Public URL for screenshare content (no tunnel needed) |
| `--max-duration` | plan limit | Max call duration in minutes. Cannot exceed your plan's limit. Check https://agentcall.dev for current limits. |
| `--alone-timeout` | 120 | Leave if alone for N seconds. |
| `--silence-timeout` | 300 | Leave if silent for N seconds. |
| `--api-url` | `https://api.agentcall.dev` | Override 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](references/guides/webpage-av-screenshare.md) 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?

| Need | Mode | Why |
|------|------|-----|
| Voice only, no video | `audio` | Simplest. No webpage, no tunnel. |
| Audio from a webpage | `webpage-audio` | Webpage plays audio into meeting. |
| Visual avatar/brand | `webpage-av` | Your page = bot's camera feed. |
| Avatar + might screenshare | `webpage-av-screenshare` | Avatar 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.

```javascript
// 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):**
| Param | Default | Description |
|-------|---------|-------------|
| `trigger_words` | `[]` | Alternate names (handles STT mishearing): `["june", "juno"]` |
| `barge_in_prevention` | `true` | Wait for silence before speaking |
| `interruption_use_full_text` | `true` | Use full text during interruptions for smarter responses |
| `context` | `""` | Initial knowledge scratchpad (4000 chars max) |
| `voice` | `voice.heart` | TTS 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](references/guides/collaborative-mode.md) 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 adv