A small, opinionated framework for bootstrapping a fleet of Vapi voice assistants from a single source of truth in code. It is the same setup that powers the multilingual voice demos on the Vapi homepage, extracted so you can read it, run it, and adapt it to your own agents.
The framework builds assistants along two axes:
- Scenarios — what the agent is for (
appointment-scheduling,qualification-screening,customer-support). - Languages —
en,es,it,fr, and amultivariant that handles 60+ languages in one call.
Three scenarios across five languages gives you 15 assistants, all defined in code and pushed to Vapi with one idempotent command. Add a scenario or a language and the matrix grows automatically.
Building one assistant in the dashboard is easy. Keeping a dozen of them consistent — same guardrails, same analysis plans, the same prompt tweak applied everywhere — is the hard part. This repo keeps every prompt, voice, transcriber, and analysis plan in version control, so a single edit re-applies across the whole fleet on the next bootstrap.
You'll need Bun and a Vapi account.
# 1. Install dependencies
bun install
# 2. Add your Vapi private key
cp .env.example .env.local
# then edit .env.local and paste in VAPI_PRIVATE_KEY
# 3. Create the fleet (first run creates all 15 assistants)
bun run bootstrapThe first run prints the new assistant ids:
✓ Created appointment-scheduling/en → 1a2b3c...
...
Add these to .env.local:
VAPI_ASSISTANT_APPOINTMENT_SCHEDULING_EN_ID=1a2b3c...
...
Paste those into .env.local, then run bun run bootstrap again. The second run finds the ids and updates each assistant in place instead of creating duplicates. From then on, editing a prompt and re-running is all it takes to ship a change.
src/
types.ts # Scenario + language ids, env-var naming, labels
languages.ts # Voice + transcriber stack, keyed by language
loadPrompt.ts # Composes a system prompt from markdown fragments
buildAssistant.ts # Stitches everything into one Vapi assistant body
bootstrap.ts # Idempotent create/update loop (the entry point)
scenarios/
index.ts # Scenario registry
appointment-scheduling.ts # One scenario = plain data + tools + analysis
qualification-screening.ts
customer-support.ts
prompts/
shared/
language-en.md # `## Language` section for EN
language-multi.md # `## Language` section for Multi
preambles/{es,fr,it}.md # Per-language preamble prepended to ES/FR/IT
appointment-scheduling/
body.md # Shared body for all 5 language variants
off-topic-{es,fr,it}.md # Localized off-topic redirect lines
qualification-screening/
...
customer-support/
body.md
idle-hook.md # Prompt for the silence-timeout hook
off-topic-{es,fr,it}.md
assistants/ # Generated raw config snapshot (see below)
appointment-scheduling/{en,es,it,fr,multi}.json
qualification-screening/{en,es,it,fr,multi}.json
customer-support/{en,es,it,fr,multi}.json
Each scenario has a single body.md. It contains exactly one {{LANGUAGE_SECTION}} placeholder, and loadPrompt.ts resolves it per language:
- EN → placeholder replaced with
shared/language-en.md. - Multi → placeholder replaced with
shared/language-multi.md. - ES / FR / IT → placeholder stripped, and
shared/preambles/<lang>.mdis prepended (with that scenario's localized off-topic lines spliced in).
So a wording change shared by every variant of a scenario is one edit to body.md, applied to all five assistants on the next bootstrap.
languages.ts holds two records keyed by language: one for the voice, one for the transcriber. English uses a Deepgram + ElevenLabs eleven_turbo_v2 stack; every non-English variant (including multi) runs Soniox with a Deepgram fallback and an ElevenLabs multilingual voice. Adding a sixth language is one new entry in each record.
Per-scenario voice tweaks (a specific voice id, speed, stability) live on the scenario as voiceOverrides and are spread on top of the language defaults in buildAssistant.ts.
- Change what an agent says → edit the relevant
prompts/<scenario>/body.md(or ashared/fragment), then re-runbun run bootstrap. - Add a scenario → add
prompts/<scenario>/body.md, ascenarios/<scenario>.tsfile, then register it inscenarios/index.tsandtypes.ts. - Add a language → add the id to
types.ts, a voice + transcriber entry inlanguages.ts, and a## Languagesection or preamble underprompts/shared/. - Customer-support knowledge base →
prompts/customer-support/body.mdships with two placeholder knowledge items under "Knowledge base — REFERENCE ONLY." Replace them with your own product facts so the agent has something real to answer from.
The assistants/ folder holds a JSON snapshot of every assistant — one file per (scenario × language) tuple. Each file is the exact body the bootstrap script sends to assistants.create / assistants.update, so it's a readable record of the fully composed prompt, voice, transcriber, tools, and analysis plan that goes live on Vapi.
Regenerate the snapshot any time the prompts or config change:
bun run dumpThese files are generated artifacts — src/ stays the single source of truth. They're committed so you can diff config changes in review and import a config into another tool without running anything.
- The bootstrap is the single source of truth. One-off edits in the Vapi dashboard work, but the next bootstrap run overwrites them — keep anything you want to persist in code.
- The capture tools (
capture_lead,capture_appointment,capture_intent) are client-side (async: true, no server URL). They fire structured data to the browser mid-call without a backend round-trip. If you drive calls server-side instead, point them at aserver.urland handle the result there.