"}'
```
Omit `repo` to swap back to the seed workspace. Check what's attached:
```bash theme={null}
curl -H "X-API-Key: $API_KEY" "$API_BASE/agent/workspace/attached"
# → { "active": {...}, "parked": [...] }
```
## Next
Put an agent to work on what's here — [chat with the workspace](/how-to/chat-workspace) or run an
[unattended routine](/how-to/daily-brief) over it.
# Vexa
Source: https://docs.vexa.ai/index
Sandboxed AI agents that grow corporate knowledge from your meetings and docs — open-source, self-hosted, air-gappable.
Sandboxed agents that grow corporate knowledge from your meetings and docs.
Real-time transcripts from Google Meet, Zoom, Teams, and Jitsi (Whisper included), fed to agents that build knowledge as code — fully self-hosted.
**These docs cover Vexa 0.12.** Hosted [vexa.ai](https://vexa.ai) runs the 0.12 meeting and transcription experience. Sandboxed knowledge agents are currently self-hosted only — [self-host Vexa](#try-it) to run the full meetings → agents stack.
Vexa tightens the loop every organization runs on: **sense → think → act**. Meetings and docs are
the data; agents turn data into signal, and signal into action that changes the world. The faster that
loop turns, the smarter the org gets.
## Why
Meetings and docs become Markdown in a versioned workspace.
The richest context in any org, captured before it's lost.
Claude Code's proven pattern, made safe and scalable.
## Two domains, together or apart
Meetings and agents are **separate domains**, each with its own API — run either on its own, or compose
them.
Plan, capture, and share meetings — calendar sync, auto-joining bots, real-time transcripts from
Google Meet, Zoom, Teams, and Jitsi. Usable standalone as a meeting API.
Sandboxed, scalable agents over your workspace. Put them to work on any knowledge — with or without
meetings.
## Explore
Self-host the full stack with one `make all`. Up in minutes on a single Linux host.
The primitives everything composes from — workspace, meeting, agent, container, identity, scheduler.
Dispatch agents, stream meetings, manage routines over HTTP.
How dispatch, execution, streaming, and trust actually work.
## What it does
* **Captures meetings natively** — real-time transcripts from [Google Meet, Zoom, Teams, and Jitsi](/core/meetings), no plugins or recorders. **Whisper included**.
* **Meets you at your calendar** — [connect a secret ICS address](/how-to/calendar-sync) and upcoming meetings import automatically; the bot [auto-joins](/core/meetings#auto-join--scheduled-means-the-bot-comes) each one at start. [Plan a meeting](/how-to/plan-a-meeting) ahead and share its prep workspace with the people you're meeting.
* **Turns knowledge into code** — meetings and emails become a living, Markdown [workspace](/concepts#workspace) that agents treat as their working directory.
* **Runs agents safely** — every [agent](/core/agents) executes in an isolated, ephemeral [container](/concepts#container): no egress except through brokered tools, thousands in parallel, no lateral movement.
* **Cloud-native deployment** — self-host the whole stack with [Docker Compose](/deployment) on a single Linux host.
* **Stays in your environment** — open-source, self-hostable, air-gappable, with [bring-your-own inference](/deployment#air-gapped). Recordings and agent state never leave infrastructure you control.
## Try it
```bash theme={null}
git clone https://github.com/Vexa-ai/vexa.git && cd vexa
make all # full stack via Docker Compose — seeds .env, brings everything up, prints your API key + URLs
make bot # build the meeting bot from source — needed before a bot can join a meeting
```
The API comes up at `http://localhost:18056` (gateway) and the terminal web
workbench at `http://localhost:13000`. Full options — air-gapped, bring-your-own
inference — in [Deployment](/deployment).
**Prefer to look before you build?** Hosted [vexa.ai](https://vexa.ai) runs Vexa 0.12 for meeting bots and transcription today — agents aren't part of the hosted service yet. These docs describe the full 0.12 stack, agents included, that you self-host above.
## Why Vexa
AI reshaped software development: a CLI agent on a Linux box is now enough to build
software. Vexa scales that same execution model to knowledge work — meetings and emails
as the source, agents as the workers, your infrastructure as the sandbox.
And the codebase itself is built for that world. Every service is a **modular monolith**:
a [module owns exactly one concern](/concepts#module) and is the single source of truth for
it; modules join only through [sealed contracts](/concepts#contract) that CI refuses to let
drift; every unit ships its code, its adversarial tests, and the goldens that validate it —
with fixtures collected from real meetings. That is what makes the system tractable for
human contributors **and** for AI agents working on it — debug any module in isolation, or
any chain of modules at the scale a bug lives at. The map: [Modules & Seams](/architecture/modules).
# Interactive bots
Source: https://docs.vexa.ai/interactive-bots
Speaking, chatting, screen-sharing bots — what is actually reachable in 0.12, what is proven at the bot layer but unwired at the API, and where the interactive shape is being decided.
**Honest status:** the interactive capability family this page once advertised is **partially available
in 0.12**. Chat *read* works. Speak, chat *write*, screen share and avatar are contract-sealed but not
reachable through the API today. The API shape for interactive capabilities is under active design —
it is **not** a straight restoration of the pre-0.12 endpoints.
## Per-surface status (0.12, verified against the tree)
| Surface | Contract (`api.v1`) | Reachable today | Notes |
| ----------------------- | ------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Chat **read** | sealed | ✅ `GET /bots/{platform}/{native_meeting_id}/chat` | mounted and served |
| **Speak** | sealed | ❌ 404 | proven live at the bot layer (acts.v1 `speak` → TTS → virtual mic), **not wired at the API layer** — tracked in [#514](https://github.com/Vexa-ai/vexa/issues/514) |
| Chat **write** | sealed | ❌ 404 | Teams chat write is prepared as [#500](https://github.com/Vexa-ai/vexa/issues/500) |
| Screen share | sealed | ❌ 404 | planned back in 0.12.x |
| Avatar / virtual camera | sealed | ❌ 404 | planned back in 0.12.x |
"Sealed" means the endpoint shape is frozen in the public API contract; it does not mean served. A call
to an unmounted surface returns `404` on every deployment.
## What you can do today
* **Read the meeting chat** while a bot is in the room: `GET /bots/{platform}/{native_meeting_id}/chat`
with a `bot`-scoped API key.
* **Capture and transcribe** as usual — the interactive gaps do not affect transcription, recordings,
or the transcript API.
## Where this is going
The pre-0.12 shape was REST-per-surface (`POST /bots/.../speak`, `/chat`, `/screen`). That design predates
the 0.12 agent layer, and the open question is **where interactive capabilities belong** — bot-level REST,
or the Agent API driving the bot's proven acts layer (cf. the external request to
[attach an external agent by URL, #333](https://github.com/Vexa-ai/vexa/issues/333)). That design decision
is tracked openly; demand for this family is measured and sustained, and comments on the design issue are
exactly the evidence the decision consumes.
# Model credentials & licensing
Source: https://docs.vexa.ai/model-credentials-licensing
Which Anthropic terms your deployment operates under — subscription OAuth vs API key — and the multi-tenant boundary you must not cross.
Vexa agent workers run the official `claude` CLI (interactive turns via the harness, one-shot
completions via `claude -p`). The credential that powers them is chosen per deployment and per
user (**Settings → Models**, resolved user > global > deployment env). Which credential you pick
determines **which Anthropic terms you operate under**.
## The primary sources
| Document | Link |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Claude Code — Legal and compliance (the authentication rules quoted below) | [code.claude.com/docs/en/legal-and-compliance](https://code.claude.com/docs/en/legal-and-compliance) |
| Consumer Terms of Service (Free / Pro / Max subscriptions) | [anthropic.com/legal/consumer-terms](https://www.anthropic.com/legal/consumer-terms) |
| Commercial Terms of Service (API / Team / Enterprise) | [anthropic.com/legal/commercial-terms](https://www.anthropic.com/legal/commercial-terms) |
| Anthropic Usage Policy | [anthropic.com/legal/aup](https://www.anthropic.com/legal/aup) |
| Use Claude Code with your Pro or Max plan | [Help Center article](https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-or-max-plan) |
| Use the Claude Agent SDK with your Claude plan (Agent SDK credit rules) | [Help Center article](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) |
The load-bearing sentences, from the Claude Code legal page (retrieved 2026-07-09):
> **OAuth authentication** is intended exclusively for purchasers of Claude Free, Pro, Max, Team,
> and Enterprise subscription plans and is designed to support ordinary use of Claude Code and
> other native Anthropic applications.
> **Developers** building products or services that interact with Claude's capabilities, including
> those using the Agent SDK, should use API key authentication through Claude Console or a
> supported cloud provider. **Anthropic does not permit third-party developers to offer Claude.ai
> login or to route requests through Free, Pro, or Max plan credentials on behalf of their users.**
> Advertised usage limits for Pro and Max plans assume ordinary, individual usage of Claude Code
> and the Agent SDK.
## How Vexa's credential modes map
**Subscription mode** ("Claude subscription (deployment credentials)"):
* Workers invoke the **official `claude` binary** — never the raw HTTP API with an OAuth token.
The credential file (`~/.claude/.credentials.json`) is bind-mounted read-only into worker
containers at the CLI's own standard path; only the official client consumes it. The token is
never extracted into headers, third-party clients, or proxies.
* `claude -p` completions are plain subprocess calls of the compiled CLI. Per the Agent SDK help
article, third-party apps authenticating with your subscription through the Agent SDK are a
**covered, credit-metered use**: programmatic usage draws a per-user monthly **Agent SDK
credit** (plan-dependent; overage at API rates when enabled). Nothing simulates a terminal to
disguise programmatic calls as interactive use.
**Custom mode** (API key / OpenAI-compatible gateway):
* Raw-API auth (`ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN`, or the OpenAI-compatible
`VEXA_LLM_*` set) is only ever brokered when the deployment or user supplied an **API key** —
Commercial Terms, no consumer authentication restrictions.
## The boundary: one human, one subscription, one beneficiary
A consumer subscription is a personal license. The moment a deployment serves **other users'
turns** on the operator's Pro/Max credential, it becomes "routing requests through plan
credentials on behalf of their users" — the explicitly disallowed case.
* **Self-host, single user** — your machine, your subscription, your turns: subscription mode as
the deployment default is ordinary individual use.
* **Any multi-user or hosted deployment** — the deployment default must be an **API key**
(Settings → Models → Custom endpoint, or `ANTHROPIC_API_KEY` in the deployment env), or each
user brings their own credential via user-level settings. Never default tenants onto anyone's
consumer OAuth.
## Operator checklist
1. Exactly **one human** benefits from a mounted subscription credential. If
`HOST_CLAUDE_CREDENTIALS` is set and more than one active user runs turns, switch the
deployment default to an API key.
2. Never move the OAuth token out of the credentials file — no proxies, no header injection, no
gateway re-serving. API-style env vars are for API keys only.
3. Expect `claude -p` usage to draw the Agent SDK credit separately from interactive limits —
exhaustion is a billing event, not a bug (it surfaces as a model-inference failure in the
worker).
4. When in doubt for a new topology: **use an API key** — the Commercial Terms path has no
authentication-shape restrictions.
Terms evolve (the Agent SDK credit rules changed during 2026 and the help article notes pending
updates). Re-verify the primary sources before relying on this page for a new deployment class.
This page is engineering compliance guidance, not legal advice.
# Automate meeting summaries with n8n
Source: https://docs.vexa.ai/n8n
Calendar → bot → transcript → summary → Slack, with no backend to write or host.
If you already run n8n, you can have meeting notes land in Slack without writing or hosting any
code. Vexa supplies two plain HTTP calls — send a bot, fetch the transcript — and n8n does the rest.
A community template already wires the whole path:
[**Automated meeting summaries: Google Meet to Slack with Vexa.ai and GPT-4o**](https://n8n.io/workflows/7072-automated-meeting-summaries-google-meet-to-slack-with-vexaai-and-gpt-4o/)
(workflow 7072). Import it, then read the warning below before your first run.
**The published template points at a base URL that no longer exists.** Its HTTP nodes are built
against `gateway.dev.vexa.ai`. The hosted API is **`https://api.cloud.vexa.ai`**. Until the
template is updated, swap the base URL in every Vexa HTTP node after importing — otherwise the
first node fails and the run stops there. This is the single most common reason this workflow
doesn't start.
The template is published on the n8n.io gallery by a community author, so we can't edit it
directly.
## What you need
* A running n8n (cloud or self-hosted).
* A Vexa API key and a base URL ([Authentication](/authentication)):
| Deployment | `API_BASE` |
| ---------------------------------- | -------------------------------------------------- |
| Hosted | `https://api.cloud.vexa.ai` |
| Self-hosted (`make all` / compose) | `http://localhost:18056` (`API_GATEWAY_HOST_PORT`) |
Self-hosting? n8n must be able to reach that host — see the [self-host note](#self-hosted-variant).
* A model credential for the summarization step (the template uses GPT-4o) and a Slack credential.
Every Vexa call authenticates with the `X-API-Key` header. Store the key as an n8n **credential**,
not inline in a node.
## The five steps
The template starts from a **Google Calendar** trigger and pulls the Meet link off the event.
Users have reported the Google Calendar trigger firing only on the oldest matching event, which
stalls the workflow when several meetings queue up. If you hit that, filter the trigger more
narrowly (single calendar, near-term window) or drive the workflow from a Schedule trigger that
queries the calendar itself.
You can also skip this step entirely — see [Let Vexa do the
calendar part](#let-vexa-do-the-calendar-part).
One `POST`. If you have the join URL, send it directly and let the API parse out the platform
and meeting id:
```http theme={null}
POST {{API_BASE}}/bots
X-API-Key: {{your key}}
Content-Type: application/json
{"meeting_url": "https://meet.google.com/abc-defg-hij", "bot_name": "Vexa"}
```
Or name the parts explicitly:
```json theme={null}
{"platform": "google_meet", "native_meeting_id": "abc-defg-hij", "bot_name": "Vexa"}
```
`native_meeting_id` is the id **inside** the join URL (`abc-defg-hij`), not the whole URL. A URL
that isn't a recognizable meeting link is refused with a `422`. Zoom, Teams, and Jitsi work the
same way with a different `platform` — see [Send a bot](/how-to/send-a-bot).
On Meet and Teams the bot may sit in the lobby until a host admits it.
Give the call time to happen. A **Wait** node set to the meeting's scheduled duration is the
simplest approach; polling the transcript on an interval and continuing once segments stop
arriving is the more responsive one.
```http theme={null}
GET {{API_BASE}}/transcripts/google_meet/abc-defg-hij
X-API-Key: {{your key}}
```
```json theme={null}
{
"segments": [
{ "speaker": "Jane Liu", "text": "Let's lock the renewal pricing by July 1.",
"start": 12.4, "end": 15.1, "language": "en", "completed": true, "confidence": 0.93 }
]
}
```
Segments are **speaker-attributed**, so your summary can name who committed to what. Segments
are also readable *while the meeting runs* if you'd rather stream than wait — see
[Stream a transcript](/how-to/stream-transcript).
Flatten `segments[]` into `speaker: text` lines, hand them to your model node, and post the
result with the Slack node. Nothing here is Vexa-specific — the transcript is ordinary JSON.
## Let Vexa do the calendar part
The calendar trigger is the most fragile node in the workflow, and you may not need it. Vexa can
import your calendar itself and **auto-join** meetings at their start time — no trigger, no
`POST /bots` on your side. The workflow then reduces to *wait → fetch transcript → summarize →
post*.
See [Sync your calendar](/how-to/calendar-sync) and [Plan a meeting](/how-to/plan-a-meeting). Sending
a bot to a link you already planned upgrades the same meeting record — it never creates a duplicate.
## Self-hosted variant
Everything above works unchanged against a self-hosted deployment; only `API_BASE` differs. The
constraint is reachability:
* **n8n and Vexa on the same host** — point n8n at `http://localhost:18056`.
* **n8n in Docker, Vexa on the host** — `http://host.docker.internal:18056`, or put both on one
Docker network and use the gateway's service name.
* **n8n cloud, Vexa self-hosted** — the gateway must be reachable from the internet over HTTPS.
Terminate TLS at a reverse proxy in front of it; don't expose the gateway port directly.
See [Deployment](/deployment) and [Configuration](/configuration).
## Troubleshooting
| Symptom | Cause |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| First HTTP node fails immediately | The template's stale `gateway.dev.vexa.ai` base URL. Swap it for `api.cloud.vexa.ai`. |
| `503` from `POST /bots` | No transcription backend configured. Default spawns require STT — see [Configuration](/configuration#transcription-stt). |
| `422` from `POST /bots` | `meeting_url` isn't a recognizable meeting link, or `platform` and `meeting_url` disagree. |
| Transcript empty | The bot may still be in the lobby, or the meeting hasn't produced audio yet. Check bot status before fetching. |
| Only the oldest calendar event ever fires | The Google Calendar trigger behaviour noted above. Narrow the trigger or use Vexa's calendar sync instead. |
More at [Troubleshooting](/troubleshooting).
# Quickstart
Source: https://docs.vexa.ai/quickstart
From zero to your first transcript and your first agent — in about five minutes.
This walkthrough takes you from a clean machine to **two first wins**: a bot capturing a live
meeting transcript, and an agent answering from your workspace. You'll self-host the whole stack
with one command.
## Prerequisites
* A Linux host (Ubuntu 24.04 recommended for production), `git`, **`make`**, and **Docker engine ≥ v26**
(`make all` checks and refuses older engines). A Mac with Docker Desktop works for a local
evaluation — everything runs in containers either way.
* `make all` **pulls the published, release-validated images** — no build, any modest machine works;
`make lite` pulls the single-container all-in-one image and is lighter still. Building from
source (`make dev`) is for contributors and wants **8 vCPUs / 16 GB RAM**.
* On Apple Silicon: arm64 images are published best-effort (not yet CI-validated) — see
[Deployment](/deployment#quick-start-docker-compose) for details.
* A **transcription (STT) token** — get one free at [`vexa.ai/account`](https://vexa.ai/account), or
[self-host transcription](/deployment#air-gapped) on a GPU. Default spawns **require** STT and
answer `503` when it is unset; capture-only is `{"transcribe_enabled": false}` on `POST /bots`
(or `TRANSCRIBE_ENABLED=false` for the whole deployment). See
[Configuration](/configuration#transcription-stt).
## 1. Install and start
```bash theme={null}
curl -fsSL https://get.docker.com | sh
git clone https://github.com/Vexa-ai/vexa.git && cd vexa
make all # full Docker Compose stack — pulls the published images (bot included), prints your API key
# contributors: `make dev` builds everything from this checkout instead (:dev tags)
```
`make all` brings up the whole stack and **prints an API key + your URLs** when it finishes:
```text theme={null}
Vexa is up.
Terminal UI : http://localhost:13000
API gateway : http://localhost:18056
API key : vxa_…
```
| Surface | URL |
| ------------------------------- | ------------------------ |
| **Terminal** (web workbench UI) | `http://localhost:13000` |
| **API gateway** | `http://localhost:18056` |
`make all` seeds `.env` from `.env.example` on first run and mints a self-host API key for you
(via `deploy/compose/bin/provision-token`). **The seeded `.env` ships with every credential empty**
— `make all` prints a warning box (and ⚠ lines in the banner) until you fill them: the STT pair for
win 1 (transcripts), one model credential for win 2 (agent chat) — either in `.env` or under
**Settings → Models** in the Terminal. To mint additional scoped keys yourself, see
[Authentication](/authentication).
## 2. First win — capture a meeting (UI)
Go to `http://localhost:13000`. You're already signed in to a self-host account.
Start a Google Meet, Zoom, or Teams call, copy the join URL, and add it from the UI. A bot joins
the call as a participant — no plugin, no host setup.
Speak. Speaker-attributed segments appear live, draft-then-confirmed, as the meeting runs.
## 2′. Same thing over the API
Use the key `make all` printed, then send a bot and read the transcript:
```bash theme={null}
# the key make all printed (or: make -s provision-token ADMIN_TOKEN=)
export API_KEY=vxa_...
export API_BASE=http://localhost:18056
# send a bot — native_meeting_id is the id from the join URL (e.g. abc-defg-hij)
curl -X POST "$API_BASE/bots" \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"platform":"google_meet","native_meeting_id":"abc-defg-hij","bot_name":"Vexa"}'
# read the transcript (poll while the meeting runs)
curl -H "X-API-Key: $API_KEY" \
"$API_BASE/transcripts/google_meet/abc-defg-hij"
```
Full surface — translation, WebSocket streaming, recordings, stopping the bot — in the
[Meetings API](/api/meetings).
## 3. Second win — ask your workspace a question
Every captured meeting compiles into your **workspace** — a Markdown knowledge base an agent reads
and writes like a developer in a repo. Chat with it:
```bash theme={null}
curl -N -X POST "$API_BASE/agent/chat" \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"What did we decide in my last meeting?"}'
```
The response streams back as Server-Sent Events — `message-delta` frames carry the reply, `commit`
frames mark anything the agent recorded. See the [Agent API](/api/agent).
## 4. Third win — connect your calendar
Stop pasting links: give Vexa your calendar's **secret ICS address** (Google Calendar → Settings →
your calendar → *Integrate calendar* → **Secret address in iCal format**, the `…/private-…/basic.ics`
one — *not* the public address above it) and every upcoming meeting with a Meet/Zoom/Teams link
imports on its own — and the bot **auto-joins at start time**. Connecting syncs immediately and
tells you what it imported; on a Google **Workspace** domain the secret-address field may need a
one-time admin unlock ([how](/how-to/calendar-sync#1-find-your-secret-ics-address)).
```bash theme={null}
curl -X PUT "$API_BASE/user/calendar" \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"ics_url":"https://calendar.google.com/calendar/ical/…/basic.ics"}'
```
Your meetings appear under **Upcoming** in the Terminal within a sync cycle. Full walkthrough
(Outlook too, what imports, per-meeting opt-out): [Calendar sync](/how-to/calendar-sync).
## Next steps
Prepare a shared knowledge space and invite the people you're meeting.
A routine that writes notes, decisions, and follow-ups when a call ends.
An unattended agent on a cron schedule.
Mint, scope, and rotate API keys for the full stack.
Docker Compose self-host, configuration, and air-gapped transcription.
# Approach
Source: https://docs.vexa.ai/roadmap/approach
How we build — principles first, execution core before features, honest re-baselining.
The roadmap is **derived from the [principles](/concepts)**, not bolted on. Three rules
govern the order of work.
## 1. The execution core comes before features
Every feature is a [dispatch](/architecture/dispatch). If the dispatch model is wrong, every feature built
on it is wrong. So the **isolated, runtime-spawned, generic-agent execution core** is built and proven
*before* piling on capabilities.
We learned this the hard way: early slices ran the agent **in the control plane** (no container) and
baked domain objects (a mailbox) into the backend. Both violated the model. They were reverted and the
core re-grounded — see [the model](/concepts) and the principle *isolation is the
enforcement*.
## 2. De-risk in dependency order
Build the thing everything else depends on, prove it, then build on it:
1. **Execution core** — [Runtime](/core/runtime) + generic [Agent](/concepts#agent) runner +
[Workspace](/concepts#workspace) mount + [Stream](/architecture/streaming). *Prove: one dispatch = one
short-lived, isolated container, resuming from a session file.*
2. **Trust** — [Identity](/architecture/identity-and-trust): SPIFFE workload identity + Keycloak token
exchange + the MCP Gateway. *Prove: every action is attributable + scoped.*
3. **Triggers** — the [Scheduler](/concepts#scheduler) (time + event) and
[Integrations](/how-to/email-triage). *Prove: a non-human launcher dispatches under a signed grant.*
4. **Capabilities** — chat, routines, integrations, [live meetings](/how-to/live-copilot), org
knowledge — each a thin composition.
5. **Enterprise** — SSO/SCIM, full air-gap, org-graph triage.
## 3. Every stage is proven, not declared
A stage is done only when it works **end-to-end** (backend + a real browser test against the live stack)
*and* its claim is proven by a test at the altitude of the claim. "Done" is never a status we assign
ourselves; it is a demo.
Current state, honestly — what's built, what's mid-rebuild, what's next.
# Stages
Source: https://docs.vexa.ai/roadmap/stages
The build, in dependency order. Each stage names what it delivers and the proof that closes it.
Derived from the [approach](/roadmap/approach): the execution core before features, dependency order, and
**every stage proven end-to-end, not declared**. This is the page we validate and refine together.
Format per stage — **Goal** · **Derives from** (a [principle](/concepts)) · **Delivers** ·
**Depends on** · **Done when** (the proof). A stage is closed only when its proof holds in a real browser
against the live stack *and* an eval asserts the claim.
***
## Stage 0 — Freeze & contracts
* **Goal.** Turn the [seven primitives](/concepts) into sealed, versioned schemas before any
code.
* **Derives from.** *Contracts are the seams.*
* **Delivers.**
* reshape `unit.v1` → the **dispatch** shape (`identity`, `runner`, `workspaces[+mode]`, `tools`,
`start: entrypoint|session`);
* **new** `agent-runner.v1` (image · argv template · stream format · session path);
* `workspace.v1` → **bucket + id + mode + encryption** (no content/layout schema);
* `tool.v1` — finish the **gate** (→ `proactive-card.v1`);
* keep `schedule.v1`, `runtime.v1`, `transcript.v1`, `identity.v1`; evolve `ws.v1` (per-dispatch topic);
* **retire** `event.v1` (an event is an Integration firing the Scheduler) and `task.v1` (tasks are files)
and the `workspace.v1` governance gate.
* **Depends on.** Nothing — this is the base.
* **Done when.** Contracts sealed; goldens validate; `schema` + `contract-version` gates green; the
[model](/concepts) and this site reference only these shapes.
***
## Stage 1 — Execution core
* **Goal.** One dispatch = one **isolated, short-lived container, spawned through the runtime**, running a
**generic agent**, resuming from a **session file**, streaming on its topic. **Delete** in-process
execution and any docker-from-agent-api.
* **Derives from.** *The agent is untrusted* · *isolation is the enforcement* · *one primitive*.
* **Delivers.** the agent worker entrypoint (generic over the [runner](/concepts#agent)); agent-api →
[Runtime](/core/runtime) `POST /workloads` for **every** dispatch; the
[workspace mount](/architecture/execution); the [Stream](/architecture/streaming) relay (chat over
`unit::out`).
* **Depends on.** Stage 0.
* **Done when.** Opening Chat **creates a container** — `docker ps` shows an ephemeral `vexa-agent`
workload that exits on idle; a second turn **resumes from the session file**; the reply streams over the
unit topic; agent-api touches no docker. Browser-tested + an eval that the in-process path is gone.
***
## Stage 2 — Trust
* **Goal.** Every dispatch is attributable + scoped; the proof survives to the tool.
* **Derives from.** *Identity is a chain of custody.*
* **Delivers.** [Identity & trust](/architecture/identity-and-trust): workload identity (token-bound
secret in dev, **SPIRE** in k8s), **Keycloak** + **RFC 8693** token exchange, the Envoy **MCP Gateway**,
signed dispatch tokens, launcher **delegation grants**.
* **Depends on.** Stage 1 (something to attest).
* **Done when.** A tool call routes through the gateway with an **exchanged, audience-scoped** token; an
**unauthorized launcher is rejected**; the audit log resolves every effect to `(subject · launcher ·
scope)`. Eval: a forged/expired token is refused at the boundary.
***
## Stage 3 — Triggers
* **Goal.** Non-human launchers dispatch agents under signed grants.
* **Derives from.** *One primitive* (a cron/email dispatch is the same unit).
* **Delivers.** the [Scheduler](/concepts#scheduler) (redis; schedule entry + event + now) and
[Integrations](/how-to/email-triage) (inbound watcher → fire; outbound MCP tool). Post-meeting rides
the same path (`transcript.completed`).
* **Depends on.** Stages 1–2.
* **Done when.** A **schedule entry fires unattended** and commits; a **Gmail event with a bound routine**
fires a [propose-only](/architecture/governance) triage that emits cards (never auto-sends). FakeClock +
fake-mailbox evals + browser test.
***
## Stage 4 — Capabilities
* **Goal.** The product surfaces — each a thin composition, none special.
* **Derives from.** *Everything is files* · *generic over tools*.
* **Delivers.** [Chat](/how-to/chat-workspace), [Routines](/how-to/daily-brief),
[Knowledge](/how-to/workspace-files) views, [Integrations](/how-to/email-triage) (email/calendar),
[Browser](/core/runtime), [Live meetings](/how-to/live-copilot),
[Org knowledge](/how-to/workspace-files).
* **Depends on.** Stages 1–3.
* **Done when.** Each capability has a browser + backend proof at the altitude of its claim (e.g. live
meeting: replay a golden transcript → cards + commit-on-approve).
***
## Stage 5 — Enterprise
* **Goal.** Self-host GA for the regulated verticals.
* **Derives from.** *Self-host & air-gap by default.*
* **Delivers.** SSO (Okta/Entra) + SCIM; **Keycloak + MCP Gateway in-cluster**; BYO inference; the
org-graph triage + review UI.
* **Depends on.** Stages 1–4.
* **Done when.** A **fully air-gapped** stack runs with **zero egress** and BYO inference; SCIM least
privilege; two conflicting person-folder updates produce a reviewable merge proposal (sign → org trunk
updated).
# Status
Source: https://docs.vexa.ai/roadmap/status
Where we are — honestly.
This page is the honest, current state — updated as we go, never aspirational.
## What 0.12 is
0.12 is a **complete refactoring of the 0.10 line into software of the same shape** — the same
services, the same wire surface (`api.v1` sealed hash-equal to 0.10.x's OpenAPI) — rebuilt so
that **every service is a modular monolith**.
The rule underneath everything: **a module owns exactly one concern, and is the single source
of truth for that concern.** Larger structures are built from modules — capture bricks compose
into the meeting bot, modules compose into services, services compose into deployments.
Modules join only through **contracts**: defined once, rarely changed, and **sealed** — a
sha256 per contract in `contracts.seal.json`, enforced in CI. If a seal breaks, we know;
a contract can never drift silently.
Why this shape:
* **Test in isolation.** Because a module's concern is exact and singular, it is tested fully
in isolation against fixtures — and debugged the same way, by manipulating the fixtures.
* **Test at any scale.** A chain of modules joined by sealed contracts tests in integration
with the same fixtures — so a bug is reproduced and debugged at exactly the scale it lives at.
* **AI-native by construction.** Every unit ships three things: the running code, the
adversarial code (tests that harness it), and the data (goldens that validate the module or
contract). All ports are deterministic for deterministic input, so the verify loop is simple
enough for an agent to own.
* **Fixtures come from reality.** We collect them from real meetings — real audio, speaker
activations, and environment metadata — and replay them through the system, so the whole
stack is harnessed against the real world largely autonomously.
The full map — module tree, the sealed contract registry (owner → consumers), and the four
eval levels — is [Modules & Seams](/architecture/modules).
**Proven in production (pre-existing Vexa):** the [Runtime](/core/runtime) (meeting bots are browser
workloads), transcription → `transcript.v1`, redis [streaming](/architecture/streaming).
**Built & proven live — the dispatch core (M1):** a [`unit.v1` dispatch](/concepts) runs in a
**runtime-spawned, isolated container** (the generic-agent worker), over a **bind-mounted
[workspace](/concepts#workspace)**, carrying a **per-dispatch signed [identity](/architecture/identity-and-trust)
token**, streaming UnitEvents on `unit::out` → SSE. The **in-process path is retired** (agents never
run in the control plane). **Chat memory is durable** — session + transcript are saved in the workspace,
so a fresh container resumes the conversation. Verified end-to-end on docker and through the terminal
chat. See [Execution](/architecture/execution).
**Designed & frozen (this site):** the [seven primitives](/concepts), the
[identity layer](/architecture/identity-and-trust) (kagenti-aligned), the
[governance model](/architecture/governance).
**Sound and kept:** the terminal workbench + surfaces, the generic event ingress, the generic tool
mechanism, the redis scheduler.
**Next:** finish M1 — the bucket-backed (minio) [workspace](/concepts#workspace) store + warm-reuse
(`touch` / idle-enforcer). Then M2 — the [live meeting](/how-to/live-copilot) dispatch.
## Delivery tracker
The live pickup queue is the [Vexa Roadmap board](https://github.com/orgs/Vexa-ai/projects/2)
(grouped by contributor lane) with milestone [`v0.12.x`](https://github.com/Vexa-ai/vexa/milestones)
as the version gate. How to claim and deliver: [the delivery constitution](/governance/delivery#the-roadmap).
The planned features, the [modules](/architecture/modules) each touches, and status. The
core features are **⬜ planned**; planned meetings, auto-join, and ICS calendar sync are **✅ done**
(OAuth calendars are **deferred**). See [Identity](/core/identity) for the auth and
encryption detail.
| Feature | Status | Module(s) it touches |
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------- |
| **Auth spine** — opaque token → `User` → scoped access end-to-end | ⬜ planned | core/identity (admin-api), core/gateway |
| **Owner-check adoption** — wire `OwnerOnlyPolicy` / `can_access` onto every meeting path (chat · stream · start · process) | ⬜ planned | core/agent, core/meetings, core/identity |
| **Real meetings list** — the live, persisted list of a user's meetings on the surface | ⬜ planned | core/meetings (meeting-api), core/gateway, clients/terminal |
| **Routines: scheduled meetings** — a `routine.v1` / `schedule.v1` job that joins on a cron | ⬜ planned | core/runtime (scheduler), core/agent, core/meetings |
| **Docs ↔ meetings binding** — bind a workspace doc to a meeting (capture → governed action) | ⬜ planned | core/agent (workspace.v1), core/meetings, clients/terminal |
| **WebSocket coverage** — the `/ws` multiplex fanning transcripts · bot status · chat | ⬜ planned | core/gateway (ws.v1), core/meetings, clients/terminal |
| **Bucket encryption** — per-workspace envelope encryption; keys brokered, decrypted only in-container | ⬜ planned | core/identity, core/agent (workspace.v1), core/runtime |
| **Transcript encryption** — encrypt transcripts at rest in the meetings database (the SSOT); protect the redis carrier (auth/TLS/ACLs) | ⬜ planned | core/meetings, core/identity |
| **User-token encryption** — store API tokens hashed/encrypted at rest, not cleartext | ⬜ planned | core/identity (admin-api) |
| **Planned meetings + auto-join** — plan ahead (`POST /meetings`), one record plan→transcript, the bot joins scheduled meetings at start | ✅ done | core/meetings, core/gateway, clients/terminal |
| **Calendar sync** — secret-ICS import → planned meetings, auto-join by default | ✅ done | core/meetings, core/identity, clients/terminal |
| **Calendar (OAuth)** — Google/Microsoft OAuth calendars (two-way, invite-aware) | 🟦 deferred | core/identity, core/meetings |
## Capability truth table
Where each partially-landed capability actually stands, layer by layer — verified against the tree
(route tables, module code, tests), not planning docs. "Module-tested" means unit/seam tests with fake
transports pass in CI; it is **not** an end-to-end claim.
| Capability | Bot / module | Service API | Terminal UI | Honest note |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Bot browser view** (watch the bot's browser live) | n/a | ❌ none | ❌ none | Not a product feature today. noVNC/x11vnc is baked into the bot image and the lite supervisor **for operator debugging only** (localhost-bound). |
| **Voice agent — TTS speak** | ✅ implemented + proven live (acts.v1 `speak` → TTS → virtual mic; June full-cycle matrix) | ❌ not wired — the gateway mounts `POST …/speak` per the sealed `api.v1` but the open-core meeting-api has no route behind it → `404`; nothing publishes the speak act. **Prepared: [#514](https://github.com/Vexa-ai/vexa/issues/514)** | ❌ none | Contract-sealed and module-tested (mock-bot speak-ack gate), **not user-reachable**. Also flagged in the [Meetings API](/api/meetings) and the [changelog](/changelog). |
| **Recording playback** | ✅ capture + chunking proven (acquire) | ✅ live + module-tested — `GET /recordings`, `/{id}`, `/{id}/master`, `…/raw` through the gateway | ❌ no player — retrieval is API/curl only ([how-to](/how-to/recordings)) | The full live loop (in-meeting capture → object storage → playback) has **not** been validated end-to-end. Retrieval durability is prepared as [#509](https://github.com/Vexa-ai/vexa/issues/509); the E2E playback proof is a declared roadmap-board direction. |
| **User webhooks** (your URL, signed events) | — | ✅ built + module-tested — self-serve config (URL/secret/events, secret-masked) and the `webhook.v1` HMAC delivery module with retry queue | ❌ no settings UI | **Never fired against a real external receiver** — module tests use an in-memory transport. Treat as untested until a live delivery is proven. Exactly-once emission and outage-durable retry are prepared as [#519](https://github.com/Vexa-ai/vexa/issues/519)/[#520](https://github.com/Vexa-ai/vexa/issues/520), whose acceptance tables carry the live-receiver leg. |
| **Service webhooks** (in-stack lifecycle callbacks) | — | ✅ wired (`/bots/internal/callback/lifecycle`, `/runtime/callback`) + seam-tested | n/a | Internal plumbing; exercised implicitly by live bot runs, no dedicated E2E harness. |
| **MCP server** | — | 🟡 ported 0.10.6 meeting-control server runs as its own compose service, module-tested, and is **fronted by the gateway at `/mcp`** — `POST` buffered, `GET` (the SSE stream) relayed unbuffered ([#795](https://github.com/Vexa-ai/vexa/issues/795)) | n/a | The forward is module-tested (streamed relay, verbatim status, typed 502/504) but the transport has **not** been proven against a real MCP client end-to-end. |
## 0.10.x features not (yet) in 0.12
The wire contract is sealed hash-equal to 0.10.x's OpenAPI 1.5.0, but a sealed endpoint is not
automatically a wired capability. What 0.10.x users had that 0.12 does not ship today — the
[changelog parity section](/changelog#parity-with-the-010x-line) is the source of truth, with
per-row verification notes:
| 0.10.x capability | 0.12 status |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mid-call bot config (`PUT …/config`) | contract-sealed, not wired — `404` · prepared: [#516](https://github.com/Vexa-ai/vexa/issues/516) |
| Voice agent / TTS speak (`POST …/speak`) | contract-sealed, not wired — `404` · prepared: [#514](https://github.com/Vexa-ai/vexa/issues/514) |
| Interactive bots beyond speak (in-meeting chat · screen share · avatar) | contract-sealed, not mounted — Teams chat read/write is ready as [#500](https://github.com/Vexa-ai/vexa/issues/500); screen share · avatar planned back in 0.12.x |
| Transcript public share links | contract-sealed, not mounted (was dashboard-coupled) — declared on the roadmap board; the sealed-vs-served reconcile is [#541](https://github.com/Vexa-ai/vexa/issues/541) |
| Meeting participants endpoint (`GET …/participants`) | post-seal — next `api.v1` revision · accepted: [#451](https://github.com/Vexa-ai/vexa/issues/451) |
| `discord` ingest platform | post-seal — next `api.v1` revision · community adapter accepted: [#463](https://github.com/Vexa-ai/vexa/issues/463) |
| Zoom authorized join (OBF/ZAK tokens, native SDK) | not carried — 0.12 joins Zoom via web client only; declared on the roadmap board (Zoom lane, with [#515](https://github.com/Vexa-ai/vexa/issues/515)/[#518](https://github.com/Vexa-ai/vexa/issues/518)/[#538](https://github.com/Vexa-ai/vexa/issues/538)/[#539](https://github.com/Vexa-ai/vexa/issues/539)) |
| Segment-latency env knobs | partial — programmatic config exists, no env wiring · prepared: [#535](https://github.com/Vexa-ai/vexa/issues/535) |
| Swagger auth-scheme fixes | not carried — and not cosmetic: all 61 secured operations reference a scheme that doesn't exist, so Swagger auth and generated clients fail · prepared: [#531](https://github.com/Vexa-ai/vexa/issues/531) |
# Swarm (idea)
Source: https://docs.vexa.ai/roadmap/swarm
Design proposal: agents spawning agents under human approval, coordinating over a shared workspace, with a heartbeat that makes silence the alarm.
**Status: idea.** Nothing on this page is implemented. It is a design proposal for how vexa's
existing primitives compose into a multi-agent swarm — recorded now so implementation PRs have an
invariant checklist to build against.
**The whole idea in a paragraph:** the agent workspace lives in object storage; redis only carries.
Agents write through per file, checkpoint at turn boundaries, and refresh by manifest etag;
conditional puts reject stale writes. A swarm is defined by the concierge but identified by its
bundle prefix: the shared ledger is membership truth — join by writing your entry, leave by removing
it; a child mounted on the bundle is a member. Fast messages ride existing unit streams; slow
coordination goes through the ledger. Agents request agents via the dispatcher: standing grants
auto-approve, everything else becomes a Telegram card, and approval mints the child's token scope —
silence means denied. Every unit carries its parent chain. The concierge is the single human channel
and swarm manager (map, reaper, finalizer) but never a gatekeeper: it can misreport, not grant. A
fixed-schedule heartbeat reports ground truth — runtime state joined with ledger checkpoints, wedged
agents shown stale. No beat means the system is dead; the runtime watches the concierge itself.
***
A swarm is many agent units working the same problem. Everything below composes primitives that
already exist — the [runtime kernel](/architecture/execution) (spawn + supervise), the redis
`unit::in` / `unit::out` streams ([streaming](/architecture/streaming)), the durable
object-storage workspace, and the [governance](/architecture/governance) rule that untrusted
proposals pass through a human gate. The swarm layer is **protocol, not new infrastructure**.
## Swarm = shared ledger
A swarm is **defined by the concierge** (the human-comms agent, below) — it decides that a set of
agents constitutes a swarm working one problem — but **identified by shared state**: the swarm id is
the workspace bundle's object-storage prefix, joining is writing your entry to the coordination
ledger (etag puts make concurrent joins safe), leaving is removing it, and a spawned child mounted
on the same bundle is in the swarm by construction. Definition lives in the manager, truth lives in
the workspace — the concierge can never claim a swarm shape the ledger contradicts. There is no
separate membership service or discovery protocol; the concierge owns the swarm map (live bundles,
members, heartbeat schedule per swarm) and the three lifecycle rules:
* **Reaper** — while assembling the heartbeat, the concierge joins ledger entries against runtime
state; an entry with no live unit and no recent checkpoint is moved to a tombstone section (kept,
not deleted, so a resuming agent finds its claim context).
* **Finalizer** — last member out: the concierge promotes durable decisions into the knowledge
base, archives the ledger, and stops that swarm's heartbeat.
* **Cross-swarm access** — touching *another* swarm's bundle is a grant-shaped event through the
dispatcher (scoped token for that prefix), not a casual read; otherwise the boundary is only a
convention.
## Shared workspace (durable plane)
The agent workspace lives in **object storage**, not redis — redis is the carrier, object storage
the store, same split as everywhere else in the [architecture](/architecture/architecture-as-code).
| Rule | What it means |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Write-through | every local file write is async-put to the bundle; the local FS is just the working copy |
| Checkpoint at turn boundaries | end of an agent turn = consistent snapshot; agents refresh (manifest etag → conditional GETs) at turn start |
| Conditional puts | writes carry the etag they read; stale overwrites are rejected — this is the concurrency safety between siblings |
| Coordination ledger | a HANDOVER-style file *inside* the bundle carries claims, holds, decisions, and open questions; agents read it before acting |
The ledger doubles as each agent's status line — the heartbeat report (below) is assembled from it,
so there is no separate status API.
## Swarm messaging (live plane)
Fast agent↔agent messages ride the **existing** unit streams — a message envelope on
`unit::in`/`:out`, no new broker or protocol. Slow coordination (claims, decisions) goes through
the workspace ledger and is seen at turn boundaries. Two planes, two latencies, both already deployed.
## Agents requesting agents
Spawning is the runtime's mechanism (P11); *who may spawn what* is policy, owned by a **dispatcher**:
1. **Request** — parent emits `spawn.request.v1` on its `:out` stream: child spec, task, workspace
scope, budget/TTL, parent id. Parents never call the runtime directly.
2. **Policy** — the dispatcher auto-approves inside a parent's standing grant (e.g. N read-only
children within budget) and escalates everything else to the human.
3. **Human gate** — an approval card in Telegram: task summary, requester chain, cost, what the
child can touch. Approve/deny via inline buttons carrying a signed request id.
4. **Grant** — on approve, the dispatcher spawns via the runtime and mints the child a
[dispatch token](/architecture/execution) whose scope is *the approved scope*. Approval is
**capability minting, not a yes/no** — the human decision is enforced by the same fail-closed
token machinery, not by the child's good behavior.
5. **Lineage** — every unit carries its parent chain: kill-tree revocation, budget inheritance,
and an audit trail ("this agent exists because X approved request Y at 14:32"), recorded in the
workspace ledger.
Timeouts **fail closed**: no answer in N minutes ⇒ denied, parent re-plans. Standing approvals are
**grant objects in the workspace** — visible, revocable, not dispatcher special cases.
## Concierge & heartbeat
One designated unit — the **concierge** — is the single human channel (the same Telegram bot that
carries approval cards) and the **swarm manager**: it defines swarms, holds the swarm map, and runs
the lifecycle rules above. Humans reply in-thread; the concierge routes `human.message.v1` onto the
target unit's `:in` stream and confirms delivery. It remains manager, not gatekeeper — spawn
approvals still flow through the dispatcher's token minting, so a compromised concierge can
misreport but cannot grant capabilities.
The heartbeat is a **dead-man's switch**: it fires on a fixed schedule, and the human contract is
simply *no message at the expected time ⇒ the system is down*. Silence is the alarm — a dead system
cannot send a failure notification, so none is designed.
* The report is assembled from **ground truth**, not self-reports: runtime supervision state
(alive/restarts) joined with each agent's last workspace-ledger checkpoint. An alive-but-wedged
agent shows as **stale** — the common failure, which liveness checks alone miss.
* Pending approvals re-surface in every beat until resolved.
* The **runtime owns the timer** and sends a bare "concierge down" if the concierge misses its
slot, so the concierge is not a single point of silence.
```
🫀 swarm · 14:00 · all nominal
Units: 4 alive, 0 stale, 1 finished since last beat
• indexer — 3,120/8,400 files (13:58)
• doc-writer — drafting architecture page (13:55)
Approvals pending: 1 (spawn: doc-reviewer, waiting 12m)
Budget: 340k/1M tokens today
```
## Message schemas
| Schema | Carrier | Purpose |
| ------------------ | --------------- | ------------------------------------------------------------- |
| `spawn.request.v1` | `unit::out` | parent asks for a child (spec, scope, budget, lineage) |
| `spawn.grant.v1` | `unit::in` | dispatcher's answer: granted (token scope) / denied / timeout |
| `human.message.v1` | `unit::in` | human → agent, routed by the concierge, logged to the ledger |
Unknown message types are ignored, same forward-compatibility rule as `acts.v1`.
# SDKs & clients
Source: https://docs.vexa.ai/sdks
The canonical interface is the REST API; published libraries wrap it.
Vexa's contract is its **HTTP API** — the [Meetings API](/api/meetings) and [Agent API](/api/agent) are
the same surface whether you self-host or use the hosted service. Any HTTP client works.
## What ships today
The open-core ships three ways to drive Vexa:
* **The HTTP API** — the canonical interface. Every capability is reachable over plain REST + WebSocket;
see the [API reference](/api/agent).
* **`vexa-slim`** — a minimal **Python** client ([`clients/slim`](https://github.com/Vexa-ai/vexa/tree/main/clients/slim)).
Gateway-only (it speaks `api.v1` and holds no domain internals), with two peer sub-clients
(`slim.agent.*`, `slim.meetings.*`) and a high-level cookbook (`listen_to_meeting`, `agent_on_meeting`,
`harvest`). Doubles as the living proof that the `meetings ⊥ agent` boundary holds.
```python theme={null}
from vexa_slim import Slim, listen_to_meeting
from vexa_slim.config import api_key, gateway_url, load_env
load_env()
slim = Slim(gateway_url(), api_key())
harvest, doc = await listen_to_meeting(slim, "ety-jhht-nek", seconds=20)
print(harvest.counts()) # e.g. {"transcript": 80, "note": 99, "card": 33}
```
* **The terminal** — the bundled browser-based [workbench](/architecture/modules) (Next.js), a thin
client over the same API. It comes up with `make all` at `http://localhost:13000`.
Other typed clients / CLI wrappers are **on the roadmap** — until then, use `vexa-slim`, call the API
directly, or generate a client from the OpenAPI surface.
## Prefer raw HTTP?
Every guide in these docs uses plain `curl`, so you can port any example to your language directly. The
only invariant is the auth header:
```bash theme={null}
-H "X-API-Key: $API_KEY"
```
Identity is **server-derived** from the key — you never send a user id or subject. See
[Authentication](/authentication).
## Building your own client
The API is stable and self-describing:
* **Meetings** — `POST /bots`, `GET /transcripts/{platform}/{id}` (poll or WebSocket), `GET /recordings`.
* **Agent** — `POST /agent/chat` (SSE), `POST /agent/routines`, `GET /agent/sessions`, `GET /agent/models`,
and the `/agent/workspace/*` reads. (`/api/*` is a deprecated alias for the same routes.)
* **Errors** — conventional status codes with a JSON `detail`; see the [error reference](/api/errors).
Streamed endpoints emit typed frames (SSE / WebSocket) documented per endpoint in the
[Agent API](/api/agent).
# Security & compliance
Source: https://docs.vexa.ai/security-compliance
The one-page answer for a security or procurement review — deployment posture, trust model, isolation, and the audit artifacts the repo ships.
Vexa is built for the buyer whose question is not *"which cloud?"* but *"how do we run this
**without** a cloud?"* — banks, healthcare, government, and anyone whose meeting content cannot
transit a vendor's infrastructure. This page collects what a security review asks for.
## Deployment posture
* **Self-hosted, single perimeter.** Every service — capture bots, transcription, storage,
agents, the web workbench — runs on infrastructure you control ([Deployment](/deployment),
[Kubernetes](/deployment-kubernetes)). Services bind to loopback; the gateway is the one
front door, with an optional pre-auth edge layer — per-IP rate throttle, auto-ban of repeat
offenders, and static IP allow/deny — behind a `GUARD_ENABLED` kill switch. Self-hosted default
OFF; hosted/cloud runs it on. See [Configuration → Gateway edge protection](/configuration#gateway-edge-protection).
* **Air-gapped / zero egress.** Nothing phones home. Pair the stack with the
[self-hosted GPU transcription unit](/deployment#transcription-the-separate-gpu-unit) and
your own LLM endpoint and no request leaves your network.
* **Bring your own models.** LLM and speech-to-text endpoints are configuration
([Configuration](/configuration)); credential terms are documented in
[Model credentials & licensing](/model-credentials-licensing).
* **Own, don't rent.** Apache-2.0. Transcripts and derived knowledge are Markdown in a git
repo you hold — leaving the product does not orphan the data.
## Trust model
* **Agents are untrusted by design.** They are prompt-injectable, so they enforce nothing
themselves. Every agent runs in an **isolated, ephemeral container** with no egress except
brokered tools, scoped to only the workspaces it was granted — agents never run in the
control plane. See [Identity & trust](/architecture/identity-and-trust).
* **Trusted vs untrusted input.** You, in chat, write to the workspace directly (git is the
undo). Untrusted input — an email, a web page — runs **propose-only**: the agent suggests,
a human approves, trusted code applies. Irreversible effects (send, order) are always gated.
See [Governance](/architecture/governance).
* **Identity at the edge.** The gateway derives identity from your API key server-side;
internal services trust only the gateway's asserted identity, not client-supplied fields.
## Audit artifacts in the repo
| Artifact | What it gives a reviewer |
| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`architecture.calm.json`](https://github.com/Vexa-ai/vexa/blob/main/architecture.calm.json) | Machine-readable architecture (FINOS **CALM**): every service, data carrier, and flow — drift-gated in CI. See [Architecture as Code](/architecture/architecture-as-code). |
| [`SECURITY.md`](https://github.com/Vexa-ai/vexa/blob/main/SECURITY.md) | Vulnerability reporting policy and contact. |
| [`security-insights.yml`](https://github.com/Vexa-ai/vexa/blob/main/security-insights.yml) | OpenSSF Security Insights manifest. |
| [`license-exceptions.json`](https://github.com/Vexa-ai/vexa/blob/main/license-exceptions.json) | FINOS license gating: deps are Category-A permissive; every exception is explicit and reasoned. |
| [`LICENSE`](https://github.com/Vexa-ai/vexa/blob/main/LICENSE) | Apache-2.0. |
| CI gates | Isolation, contract seals, config contract, license audit, and a full-stack readiness proof run on every push (`scripts/gates.mjs`). |
## Known gaps (honest)
At-rest encryption for workspaces, transcripts, and tokens is **planned, not shipped** — today
you bring your own disk/volume encryption. Mid-call bot control endpoints are not wired in the
open-core stack. Track both on the [status page](/roadmap/status).
## Reporting a vulnerability
Follow [`SECURITY.md`](https://github.com/Vexa-ai/vexa/blob/main/SECURITY.md) — report
privately to the maintainer contact listed there; please do not open public issues for
security reports.
# Troubleshooting
Source: https://docs.vexa.ai/troubleshooting
The common failures, what causes them, and how to fix them.
## Bot won't join the meeting
A `POST /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:
```bash theme={null}
curl -H "X-API-Key: $API_KEY" "$API_BASE/bots/status"
```
* **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 at `requested`; if you're on the
stale published `vexaai/vexa-bot:dev` (the old 0.10 line) the bot reaches `joining` then fails the
`lifecycle.v1` handshake. Fix: `make bot`, and point `BROWSER_IMAGE` at the built tag.
* **Bad meeting id** — `native_meeting_id` is the id *inside* the join URL (e.g. `abc-defg-hij`), not the
whole URL. Wrong platform value also fails: use `google_meet`, `zoom`, or `teams`.
* **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.
* **Concurrency cap** — a user can run at most `max_concurrent_bots` bots 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 with `BOT_AUTHENTICATED=true` (see
[Authenticated bots](/authenticated-bots)).
* **`session-restore` failure at spawn** — the bot could not download the stored session (wrong
`BOT_S3_ENDPOINT`, bad scoped credentials, or the `aws` CLI 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_AUTHENTICATED` is set but `BOT_USERDATA_S3_PATH` /
`BOT_S3_ENDPOINT` / `BOT_S3_BUCKET` are 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](/authenticated-bots#3-session-lifetime--the-session-stays-alive-because-it-is-used)
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](/configuration#auto-join--calendar-sync)). If the bot never appeared, check the
meeting's row in the Terminal — auto-join failures are stamped **onto the meeting** as a visible
`auto_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 (prep view), or for imported meetings the global
switch in the calendar popover.
* **Concurrency cap** — you were already running `max_concurrent_bots` bots 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](#bot-joins-but-theres-no-transcript).
## Calendar isn't syncing
**Start with the panel's status line** — the calendar panel shows the last sync's outcome and
every failure names itself (`⚠ Last sync failed: …`). **Sync now** re-runs it on demand, and the
API mirrors both: `GET /user/calendar/sync` (last status), `POST /user/calendar/sync` (run now).
The full message-by-message reference is in
[Calendar sync → Reading the sync status](/how-to/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 answers `HTTP 401`/`404`; a calendar *page* URL (embed/share) is
rejected at save. Reads via `GET /user/calendar` show the 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](/how-to/calendar-sync#1-find-your-secret-ics-address).
* **The event has no meeting link** — only events with a recognizable Meet/Zoom/Teams link import;
everything else is skipped by design. A healthy feed with none of those reads
`✓ Synced — no meetings with joinable links found`.
* **Self-host wiring** — confirm `ADMIN_API_URL` and `INTERNAL_API_SECRET` are set for
meeting-api; without them the sync loop is disabled (and `POST /user/calendar/sync` answers
`503`).
* **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 resolve `transcribe_enabled=true`. When no STT backend is configured, meeting-api
refuses **before** writing a meeting row:
```text theme={null}
no transcription backend configured — set it in Settings or environment variables
TRANSCRIPTION_SERVICE_URL + TRANSCRIPTION_SERVICE_TOKEN
```
That is intentional (refuse loud). Fixes:
* Configure `TRANSCRIPTION_SERVICE_URL` + `TRANSCRIPTION_SERVICE_TOKEN` (see
[Configuration](/configuration#transcription-stt)), or
* Spawn capture-only with `{"transcribe_enabled": false}`, or set `TRANSCRIBE_ENABLED=false` for
the deployment.
If STT *is* set but the live probe says the backend is wrong, the 503 detail names the probe
reason instead — fix the URL/token and re-probe (`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` (or `GET /meetings/{id}`)
reports the resolved flag: `data.transcribe_enabled: false` means the bot was never asked to
transcribe, so no STT request was ever made and no error will appear anywhere. If it is `false`
and you did not intend that, an explicit `{"transcribe_enabled": true}` on `POST /bots` overrides
the deployment default and confirms the diagnosis in one call.
* Confirm `TRANSCRIBE_ENABLED` is not set to a false value in your `.env`. An **empty**
`TRANSCRIBE_ENABLED=` means `true` (the default) — but a trailing comment on that line does not:
`docker run --env-file` keeps the comment as part of the value. Keep comments on their own line.
* Confirm `TRANSCRIPTION_SERVICE_URL` and `TRANSCRIPTION_SERVICE_TOKEN` are set (see
[Configuration](/configuration#transcription-stt)). 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"}` on
`GET /api/meeting/relay-health` rather 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/calendar → 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.
| Signal | Where to read it | Meaning |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Pod exit code `3` | `kubectl describe pod ` → `Last State: Terminated, Exit Code: 3` | control plane unreachable at boot — the bot never navigated to the meeting |
| `infra_fault: control_plane_unreachable`, `failure_stage: requested` | meeting API / dashboard (if any callback channel recovered) or the pod logs | distinguishes a **broken node** from a **broken join** (a real join failure is exit `1`, `failure_stage: joining`/`awaiting_admission`/`active`) |
**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](/deployment-kubernetes).
## Authentication failures
| Symptom | Cause | Fix |
| --------------------------------------------------- | ------------------------------ | ----------------------------------------------------- |
| `401 Missing API key` | no `X-API-Key` header | add the header |
| `401 Invalid API key` | unknown or revoked key | mint a fresh one ([Authentication](/authentication)) |
| `403 Token scope not authorized` | key lacks the route's scope | mint with the right `scopes=` |
| `403 Invalid or missing admin token.` on `/admin/*` | missing or wrong `ADMIN_TOKEN` | use the `X-Admin-API-Key` matching your `ADMIN_TOKEN` |
## 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_KEY` or `HOST_CLAUDE_CREDENTIALS` in `deploy/compose/.env`,
then `make all` (the frame names exactly the keys it found unset).
* **Per-user** — add a custom endpoint under **Settings → Models** in the Terminal (a `custom`
config needs its `base_url`; the API key is optional for keyless local gateways).
A turn that *starts* and then fails with **"Model credentials are missing or expired for this
deployment"** (a `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](/configuration#claude-subscription-credentials-host_claude_credentials));
otherwise refresh/rotate the key. The raw provider text rides in the frame's `detail` field.
## Containers don't spawn (self-host)
The [runtime](/core/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_GID` to 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 published `vexaai/vexa-bot:dev` is the old 0.10 line and is incompatible. `AGENT_IMAGE` is built by
`make all` — confirm it resolves and `IMAGE_TAG` matches 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](/core/runtime)). If a meeting ends and you still see stray `chrome`/`node`
processes, check inside the container:
```bash theme={null}
ps -eo pid,ppid,etime,comm # PPID 1 = orphaned (reparented to init)
```
As of this release the backend reaps each workload's whole process group when it exits or is
stopped, so no strays should remain — a descendant that detached into its own process group
(the debug-view x11vnc/websockify) is the one declared exception. On older Lite images the
workaround is a container restart.
## Agent write was rejected
A streamed turn can end with a `rejected` frame carrying `violations`. This is governance, not a bug:
**untrusted input** (email, web) runs [propose-only](/architecture/governance) 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 all` brings the compose stack up health-gated; tail the logs with `make logs` or
`docker compose -p vexa-v012 logs -f` to see which service is wedged.
* Port already in use → override the host port (e.g. `API_GATEWAY_HOST_PORT`) in `deploy/compose/.env`.
* Wipe and restart from clean: `make down` (or `docker compose -p vexa-v012 down -v` to drop the
postgres + minio volumes too), then `make all`.
## Scaling meeting-api to more than one replica
Running `meetingApi.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?
Open an issue or ask on the [GitHub repo](https://github.com/Vexa-ai/vexa). Include the failing request,
the response (status + `detail`), and the relevant `docker logs`.
# MCP server
Source: https://docs.vexa.ai/vexa-mcp
Vexa's meeting capabilities as MCP tools — working on self-hosted compose, not yet on hosted or Kubernetes.
**Read the boundary before you wire anything.** The MCP server is real, runs as its own service,
and is fronted by the gateway at `/mcp` on **self-hosted Docker Compose from 0.12.18 onward**. It is
**not available on hosted [vexa.ai](https://vexa.ai), and not available on Kubernetes** — the Helm
chart does not deploy the service at all, so there is no `/mcp` route there to point a client at.
The gateway forward is module-tested (streamed relay, verbatim status, typed 502/504) but the
transport has **not been proven against a real MCP client end-to-end**
([#888](https://github.com/Vexa-ai/vexa/issues/888)). Treat a working session as something you
should verify yourself, not something we have witnessed for you.
## What it is
An MCP client — Claude Desktop, an IDE, any MCP-compatible agent — gets Vexa's meeting capabilities
as standard **MCP tools and prompts**, with no bespoke API integration.
The service is a stateless FastAPI app whose routes *are* the tools. It wraps the **public API
only**: every tool call forwards the caller's credential to the gateway as `X-API-Key`, and the
gateway resolves the key and enforces scopes. It holds no credentials, touches no database, and
never reaches into other services directly.
## Tools
Nine tools, plus four prompts (`vexa.meeting_prep` · `vexa.during_meeting` · `vexa.post_meeting` ·
`vexa.teams_link_help`).
| Tool | Wraps |
| ------------------------ | ----------------------------------------------------------------------- |
| `parse_meeting_link` | pure — URL to platform / `native_meeting_id` / passcode, no gateway hop |
| `request_meeting_bot` | `POST /bots` (accepts `meeting_url` **or** `native_meeting_id`) |
| `get_bot_status` | `GET /bots/status` |
| `update_bot_config` | `PUT /bots/{platform}/{native}/config` |
| `stop_bot` | `DELETE /bots/{platform}/{native}` |
| `list_meetings` | `GET /meetings` |
| `get_meeting_transcript` | `GET /transcripts/{platform}/{native}` |
| `list_recordings` | `GET /recordings` |
| `get_recording` | `GET /recordings/{recording_id}` |
## Connect a client (self-hosted compose)
Requires a compose deployment on **0.12.18 or later** — that is the release in which the gateway
began fronting `/mcp`.
```json theme={null}
{
"mcpServers": {
"Vexa": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:18056/mcp",
"--header", "Authorization: Bearer ${VEXA_API_KEY}"]
}
}
}
```
`18056` is the compose `API_GATEWAY_HOST_PORT`. Point at the gateway, not at the MCP service's own
port — the gateway is the authenticated front door.
The key may arrive as `X-API-Key` or as the MCP transport's `Authorization: Bearer `; both are
forwarded on. Auth at the edge is fail-closed and identical to every other route
([Authentication](/authentication)).
The MCP service also publishes a direct host port (`127.0.0.1:18010` by default) for local
debugging. **It bypasses the gateway, and therefore bypasses authentication.** Do not expose it.
### How the transport is forwarded
The two legs of streamable HTTP are forwarded differently, and the difference is the point
([#795](https://github.com/Vexa-ai/vexa/issues/795)):
| Leg | What it is | How the gateway forwards it |
| -------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `POST /mcp` (and `PUT`/`PATCH`/`DELETE`/`OPTIONS`) | a message — short request/response JSON | buffered forward, status and body verbatim |
| `GET /mcp` | the server-to-client SSE stream: headers, then silence until the server pushes | **relayed unbuffered**, on a dedicated streaming client |
Buffering the `GET` leg is what produced the original failure: the proxy waits on the next body read
of a healthy-but-silent stream, hits its read timeout, and manufactures a `503` the MCP service
never sees.
## What is not available
* **Hosted.** No MCP endpoint on [vexa.ai](https://vexa.ai).
* **Kubernetes / Helm.** The chart deploys no MCP service and defines no route — see
[#1035](https://github.com/Vexa-ai/vexa/issues/1035).
* **End-to-end proof.** No real MCP client session has been witnessed
([#888](https://github.com/Vexa-ai/vexa/issues/888)).
* **Tools blocked on API parity** — these wrap REST routes the public API does not expose yet, and
will be ported when the routes land: `delete_recording` · `get_recording_media_download` ·
`get_recording_config` / `update_recording_config` · `create_transcript_share_link` ·
`update_meeting_data` / `delete_meeting` · `get_meeting_bundle`.
The live status row for this feature is maintained on [Roadmap → Status](/roadmap/status).