Bot won’t join the meeting
APOST /bots returns success once the bot is dispatched, not once it’s in the call — a join can
still fail. Check the bot’s live state:
- Bot image not built (self-host) — the meeting bot is built from source (
make bot), not pulled. If it’s missing, the bot can’t spawn and the meeting sticks atrequested; if you’re on the stale publishedvexaai/vexa-bot:dev(the old 0.10 line) the bot reachesjoiningthen fails thelifecycle.v1handshake. Fix:make bot, and pointBROWSER_IMAGEat the built tag. - Bad meeting id —
native_meeting_idis the id inside the join URL (e.g.abc-defg-hij), not the whole URL. Wrong platform value also fails: usegoogle_meet,zoom, orteams. - Waiting room / admission — on Meet and Teams the bot may be parked in a lobby until a host admits
it. Admit it like any guest. A run that ended in the lobby says so:
awaiting_admission_timeoutorawaiting_admission_rejected, andautomatic_leave.max_wait_for_admissionsets how long it waits. - Concurrency cap — a user can run at most
max_concurrent_botsbots at once (set when the user was created). Stop an existing bot or raise the cap.
Authenticated bot fails or joins signed-out
Applies only to deployments withBOT_AUTHENTICATED=true (see
Authenticated bots).
session-restorefailure at spawn — the bot could not download the stored session (wrongBOT_S3_ENDPOINT, bad scoped credentials, or theawsCLI missing from a custom image). The bot log names the step and endpoint; the meeting fails rather than joining signed-out. Fix the config and respawn.- 503 on
POST /bots—BOT_AUTHENTICATEDis set butBOT_USERDATA_S3_PATH/BOT_S3_ENDPOINT/BOT_S3_BUCKETare incomplete. The error names what is missing. - 409 “authenticated session in use” — one stored session runs one bot at a time; the message names the meeting holding it. Wait for it to finish, stop it, or provision another identity.
- The session decayed (signed-out on join) — re-provision with
make login; the next spawn picks the fresh session up automatically. If this fires soon after provisioning rather than after weeks of use, check write-back and the session-lifetime levers instead of re-provisioning in a loop.
A scheduled meeting didn’t auto-join
Auto-join sends the bot ~60 s before the scheduled time (see Configuration). If the bot never appeared, check the meeting’s row in the Terminal — auto-join failures are stamped onto the meeting as a visibleauto_join_error, never swallowed. The usual causes:
- No meeting link — a plan without a Meet/Zoom/Teams link has nothing to join. Attach the link in the meeting’s prep view.
- Auto-join is off — the per-meeting toggle, or the source calendar’s Auto-join switch
(dashboard Calendar page on hosted;
PATCH /user/calendars/{calendar_id}anywhere). - Concurrency cap — you were already running
max_concurrent_botsbots at start time. The row shows the cap error and retries after a backoff (default 5 min). - The start time is long past — a meeting more than the grace window (default 10 min) past its start is skipped deliberately; the bot never joins hours late.
- STT not configured — like a manual spawn, an auto-spawn refuses to launch a bot that could never transcribe; the row shows the unset keys. See Bot joins but there’s no transcript.
Calendar isn’t syncing
Start with the connection’s status line — on hosted Vexa that is the Calendar page of the dashboard at dashboard.vexa.ai, which shows each feed’s last sync outcome, and every failure names itself (⚠ Last sync failed: …). Sync now re-runs that
connection. Self-hosted deployments do not ship that dashboard, and the API carries the same two
facts either way: GET /user/calendars/{calendar_id}/sync (last status),
POST /user/calendars/{calendar_id}/sync (run now).
The full message-by-message reference is in
Calendar sync → Reading the sync status. The
usual causes:
- Wrong URL kind — you need the secret ICS address (Google: Secret address in iCal
format,
…/private-…/basic.ics; Outlook: the published ICS link). The public iCal address of a private calendar answersHTTP 401/404; a calendar page URL (embed/share) is rejected at save. Reads viaGET /user/calendarsshow every connected feed masked. - The secret-address field is missing (Google Workspace) — a restrictive Workspace sharing policy hides it. An admin unlocks it: admin.google.com → Apps → Google Workspace → Calendar → Sharing settings → External sharing options for primary calendars → “Share all information…”. Details in Calendar sync.
- The event has no meeting link — it still imports as an unarmed planned meeting with
platform: "unknown"; Vexa cannot auto-join until the feed contains a recognizable Meet/Zoom/Teams link. - Self-host wiring — confirm
ADMIN_API_URLandINTERNAL_API_SECRETare set for meeting-api; without them the sync loop is disabled (andPOST /user/calendar/syncanswers503). - Feed too large / unreachable / redirecting — feeds over 2 MB, behind redirects, or resolving to blocked/internal addresses are refused; the status line records which.
POST /bots returns 503 (transcription required)
Default spawns resolvetranscribe_enabled=true. When no STT backend is configured, meeting-api
refuses before writing a meeting row:
- Configure
TRANSCRIPTION_SERVICE_URL+TRANSCRIPTION_SERVICE_TOKEN(see Configuration), or - Spawn capture-only with
{"transcribe_enabled": false}, or setTRANSCRIBE_ENABLED=falsefor the deployment.
GET /health?force=1).
Bot joins but there’s no transcript
The bot is capturing audio, but transcription isn’t configured — or the bot was spawned capture-only, which looks identical from the outside.- First, check whether the bot was spawned capture-only.
GET /bots(orGET /meetings/{id}) reports the resolved flag:data.transcribe_enabled: falsemeans the bot was never asked to transcribe, so no STT request was ever made and no error will appear anywhere. If it isfalseand you did not intend that, an explicit{"transcribe_enabled": true}onPOST /botsoverrides the deployment default and confirms the diagnosis in one call. - Confirm
TRANSCRIBE_ENABLEDis not set to a false value in your.env. An emptyTRANSCRIBE_ENABLED=meanstrue(the default) — but a trailing comment on that line does not:docker run --env-filekeeps the comment as part of the value. Keep comments on their own line. - Confirm
TRANSCRIPTION_SERVICE_URLandTRANSCRIPTION_SERVICE_TOKENare set (see Configuration). Unset → audio is recorded, no text is produced. - Segments arrive draft-first (
completed: false) then confirmed (completed: true) — a short delay is normal, not a failure. - A stale bot key shows up as
native_resolve:{ok:false,kind:"unauthorized"}onGET /api/meeting/relay-healthrather than as silent dead air.
What terminal errors look like — and where the technical details live
Terminal error surfaces speak user vocabulary (“Couldn’t reach the Vexa server — check that the stack is running.”, “Your API key was rejected — sign in again.”, or the backend’s own reason when it sent one). The full technical string — proxied URL, HTTP status, backend exception detail, e.g./api/vexa/user/calendars → 502: upstream unreachable: ConnectError — is never lost: it is written
verbatim to the browser console (api failure … / meeting action failed … warnings) on every
presented error. Support and self-hosters debug from the console line; the on-screen headline tells
the user which layer to suspect.
A bot on a fresh k8s node fails instantly with exit code 3 (control_plane_unreachable)
On Kubernetes each bot is a bare Pod, and a freshly autoscaled node’s network can take up to a few
minutes to converge after the node reports Ready. A bot scheduled into that window may find its
control plane (the meeting-api callback and redis) unreachable. Rather than crashloop opaquely or
report a misleading join_failure, the bot now fails fast: it refuses to join and exits with a
dedicated code.
What to check on the node: is the CNI fully programmed? can a canary pod on that node reach the
meeting-api Service and redis? Deleting the pod to force a reschedule (onto a warmed node, or the same
node after its window passes) is the manual remedy. The bot’s exit is fast (< a few seconds) and the
retry budget on the first emit (≤ ~15s) already rides out brief transient programming lag. See
Kubernetes deployment → autoscaling and bot bursts.
Authentication failures
Don’t have a key yet? Hosted: sign in at vexa.ai/signin with a Google
account and copy your key from your account page — free credit, no card
required. Self-hosted:
make all prints a key when the stack comes up.
Agent chat says no model credentials are configured
POST /agent/chat streams a single error frame — “No model credentials are configured, so the
agent cannot run…” — and no turn runs. This is the credential preflight: the deployment has no
model credential and your user has no custom endpoint, so a worker could only fail. Fix either side:
- Deployment-wide — set ONE of
CLAUDE_CODE_OAUTH_TOKEN,ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,VEXA_LLM_API_KEYorHOST_CLAUDE_CREDENTIALSindeploy/compose/.env, thenmake all(the frame names exactly the keys it found unset). - Per-user — add a custom endpoint under Settings → Models in the Terminal (a
customconfig needs itsbase_url; the API key is optional for keyless local gateways).
done frame, ok:false) is the mid-flight variant: a credential that existed at
dispatch went stale — on macOS typically the Keychain-exported HOST_CLAUDE_CREDENTIALS file
(install the sync daemon);
otherwise refresh/rotate the key. The raw provider text rides in the frame’s detail field.
Containers don’t spawn (self-host)
The runtime spawns bot and agent containers via the Docker socket.- The runtime needs access to the host Docker socket and the right group — set
DOCKER_GIDto the host’s docker group id. - The meeting bot (
BROWSER_IMAGE) is built from source (make bot) and the runtime spawns it without pulling — it must exist locally first. Confirm it’s there (docker image inspect "$BROWSER_IMAGE"); the publishedvexaai/vexa-bot:devis the old 0.10 line and is incompatible.AGENT_IMAGEis built bymake all— confirm it resolves andIMAGE_TAGmatches what you built.
Leftover chrome/node processes after a bot ends (Lite)
On Lite every bot and agent runs as a child process inside one shared container (the process backend). If a meeting ends and you still see straychrome/node
processes, check inside the container:
Agent write was rejected
A streamed turn can end with arejected frame carrying violations. This is governance, not a bug:
untrusted input (email, web) runs propose-only and cannot write directly —
it emits proposal cards a human approves. Trusted input (your chat) may write. If a legitimate trusted
write is rejected, check that the dispatch’s trigger is message/scheduled (which mount the workspace
rw), not an untrusted event.
The stack won’t come up
make allbrings the compose stack up health-gated; tail the logs withmake logsordocker compose -p vexa-v012 logs -fto see which service is wedged.- Port already in use → override the host port (e.g.
API_GATEWAY_HOST_PORT) indeploy/compose/.env. - Wipe and restart from clean:
make down(ordocker compose -p vexa-v012 down -vto drop the postgres + minio volumes too), thenmake all.
Scaling meeting-api to more than one replica
RunningmeetingApi.replicaCount > 1 (the Helm chart default is 2) is safe. Every replica
starts the same background sweeps, but each sweep’s real work is single-flighted by a Postgres
session-level advisory lock (one per loop): the replica that acquires the lock runs the tick, the
others skip it that interval. So a sweep’s real work — notably the calendar-sync external ICS
fetch — runs once per interval across the cluster, not once per replica. Sweep intervals
(CALENDAR_SYNC_INTERVAL_S, etc.) are therefore per-cluster, not per-replica. A replica that dies
mid-tick drops its lock on disconnect, so the next interval is picked up by another replica — no
leader-election setup required. On a single replica the lock is always free, so behaviour is
unchanged. Note that scheduler-tick is not a production loop and is no longer started.
Still stuck?
If nothing above matches, that is itself worth telling us — it means the failure has no published cause yet. Support is where to send it, and says what to include: the meeting id and platform (the join key between your account of the failure and our record of the same meeting), your deployment shape and version, the failing request, the response (status +detail), and the relevant
docker logs.