Vapi GitOps¶
Config-as-code management for the v3.0 voice agent squad
Last Updated: 2026-03-09 (v4.3.0 — audited against resolver.ts, state files, and README)
Overview¶
VitaraVox uses Vapi GitOps to manage the v3.0 squad configuration as code. All assistant prompts, tool definitions, and squad routing are defined in YAML/Markdown files and pushed to the Vapi API.
Directory: /home/ubuntu/vitara-platform/vapi-gitops/
Directory Structure¶
vapi-gitops/
├── .env.dev # Dev Vapi token (VAPI_TOKEN, not VAPI_API_KEY)
├── .env.prod # Prod Vapi token (separate account)
├── .vapi-state.dev.json # Dev slug → UUID mappings (auto-generated)
├── .vapi-state.prod.json # Prod slug → UUID mappings (auto-generated)
├── package.json # 15 npm scripts
├── src/
│ ├── pull.ts # Pull platform state
│ ├── push.ts # Push local state to platform
│ ├── apply.ts # Orchestrator: pull -> merge -> push
│ ├── call.ts # WebSocket call script
│ ├── cleanup.ts # Cleanup orphaned resources
│ ├── types.ts # TypeScript interfaces
│ ├── config.ts # Environment & configuration
│ ├── api.ts # Vapi HTTP client
│ ├── state.ts # State file management
│ ├── resources.ts # Resource loading (YAML, MD, TS)
│ ├── resolver.ts # Reference resolution
│ └── delete.ts # Deletion & orphan checks
├── resources/
│ ├── assistants/
│ │ ├── router-v3.md # Router prompt
│ │ ├── patient-id-en.md # Patient-ID English prompt
│ │ ├── patient-id-zh.md # Patient-ID Mandarin prompt
│ │ ├── booking-en.md # Booking English prompt
│ │ ├── booking-zh.md # Booking Mandarin prompt
│ │ ├── modification-en.md # Modification English prompt
│ │ ├── modification-zh.md # Modification Mandarin prompt
│ │ ├── registration-en.md # Registration English prompt
│ │ └── registration-zh.md # Registration Mandarin prompt
│ ├── tools/ # 14 tool YAML definitions
│ │ ├── search-patient-by-phone-8474536c.yml
│ │ ├── search-patient-4889f4e5.yml
│ │ ├── get-patient-d86dee47.yml
│ │ ├── get-clinic-info-aaec50cf.yml
│ │ ├── get-providers-1ffa2c33.yml
│ │ ├── find-earliest-appointment-7fc7534d.yml
│ │ ├── check-appointments-74246333.yml
│ │ ├── create-appointment-65213356.yml
│ │ ├── update-appointment-635f59ef.yml
│ │ ├── cancel-appointment-f6cef2e7.yml
│ │ ├── register-new-patient-9a888e09.yml
│ │ ├── add-to-waitlist-0153bac0.yml
│ │ ├── transfer-call-d95ed81e.yml
│ │ └── log-call-metadata-4619b3cb.yml
│ └── squads/
│ └── vitaravox-v3.yml # Squad routing configuration
└── simulations/ # Vapi simulation test resources
├── personalities/ # Caller personality definitions (.yml)
├── scenarios/ # Test scenario definitions (.yml)
├── tests/ # Individual simulation files (.yml)
└── suites/ # Simulation suite files (.yml)
Configuration¶
Environment¶
The .env.dev file contains the Vapi API token:
Variable Name
The environment variable is VAPI_TOKEN, not VAPI_API_KEY. Using the wrong name will cause silent authentication failures.
| Variable | Required | Description |
|---|---|---|
VAPI_TOKEN |
YES | API authentication token |
VAPI_BASE_URL |
no | API base URL (defaults to https://api.vapi.ai) |
State File¶
.vapi-state.dev.json maps local resource slugs to Vapi UUIDs. This file is auto-generated on first push and should not be manually edited.
Format:
{
"assistants": {
"router-v3": "4f70e214-6111-4f53-86c9-48f8f7c265e1",
"patient-id-en": "7d054785-9074-4856-81db-9fe44da47bc5"
},
"tools": {
"search-patient-by-phone-8474536c": "8474536c-663f-4a94-91ae-19e6221f9af9"
},
"squads": {
"vitaravox-v3": "13fdfd19-a2cd-4ca4-8e14-ad2275095e32"
},
"structuredOutputs": {},
"personalities": {},
"scenarios": {},
"simulations": {},
"simulationSuites": {}
}
Commands¶
All commands are defined in package.json and use tsx to run TypeScript directly.
| Command | Description |
|---|---|
npm run build |
Type-check the codebase (tsc --noEmit) |
npm run push:dev |
Push local files to Vapi (dev) |
npm run push:prod |
Push local files to Vapi (prod) |
npm run push:dev:force |
Push with --force flag (dev) |
npm run push:prod:force |
Push with --force flag (prod) |
npm run pull:dev |
Pull platform state, preserve local changes (dev) |
npm run pull:dev:force |
Pull platform state, overwrite everything (dev) |
npm run pull:prod |
Pull from prod, preserve local changes |
npm run pull:prod:force |
Pull from prod, overwrite everything |
npm run apply:dev |
Pull -> Merge -> Push in one shot (dev) |
npm run apply:prod |
Pull -> Merge -> Push in one shot (prod) |
npm run call:dev |
Start a WebSocket call to an assistant/squad (dev) |
npm run call:prod |
Start a WebSocket call to an assistant/squad (prod) |
npm run cleanup:dev |
Clean up orphaned resources (dev) |
npm run cleanup:prod |
Clean up orphaned resources (prod) |
Selective Push¶
Push specific resource types or files:
# By resource type
npm run push:dev assistants
npm run push:dev tools
npm run push:dev squads
# By specific file
npm run push:dev resources/assistants/router-v3.md
npm run push:dev resources/tools/transfer-call-d95ed81e.yml
# Multiple files
npm run push:dev resources/assistants/booking-en.md resources/tools/create-appointment-65213356.yml
Note
Partial pushes skip deletion checks. Run full npm run push:dev to sync deletions.
Common Workflow¶
Push prompt changes¶
cd /home/ubuntu/vitara-platform/vapi-gitops
# Edit prompt markdown files in resources/assistants/
# Edit tool YAML files in resources/tools/ if needed
# Push all changes to Vapi dev environment
npm run push:dev
# Verify in Vapi dashboard or test with a phone call to +1 236-305-7446
Pull remote changes¶
# Preserve local changes, pull new platform resources
npm run pull:dev
# Force overwrite (platform becomes source of truth)
npm run pull:dev:force
# Review what changed
git diff
First-time setup¶
cd /home/ubuntu/vitara-platform/vapi-gitops
npm install
# Create .env.dev with your Vapi token
echo "VAPI_TOKEN=<your-token>" > .env.dev
# Pull all existing resources from Vapi
npm run pull:dev:force
# Commit initial state
git add . && git commit -m "initial pull"
Squad Architecture (v3.0)¶
Squad ID: 13fdfd19-a2cd-4ca4-8e14-ad2275095e32
Phone: +1 236-305-7446
| Agent | Slug | UUID (short) | STT | TTS |
|---|---|---|---|---|
| Router | router-v3 | 4f70e214 |
AssemblyAI Universal | ElevenLabs |
| Patient-ID EN | patient-id-en | 7d054785 |
Deepgram nova-2 en |
ElevenLabs |
| Patient-ID ZH | patient-id-zh | 7585c092 |
Deepgram nova-2 zh |
Azure Xiaoxiao |
| Booking EN | booking-en | ac25775b |
Deepgram nova-2 en |
ElevenLabs |
| Booking ZH | booking-zh | 6ef04a40 |
Deepgram nova-2 zh |
Azure Xiaoxiao |
| Modification EN | modification-en | 9cd8381d |
Deepgram nova-2 en |
ElevenLabs |
| Modification ZH | modification-zh | e348cd2f |
Deepgram nova-2 zh |
Azure Xiaoxiao |
| Registration EN | registration-en | 9fcfd00d |
Deepgram nova-2 en |
ElevenLabs |
| Registration ZH | registration-zh | ce50df43 |
Deepgram nova-2 zh |
Azure Xiaoxiao |
Tool request-start Configuration¶
3 of the 14 tools have audible request-start messages (from YAML source of truth):
| Tool | request-start | Audible? |
|---|---|---|
| search_patient_by_phone | "Let me pull up your file." |
Yes |
| find_earliest_appointment | "Let me check what's available." |
Yes |
| check_appointments | "Let me look that up." |
Yes |
| All 11 other tools | "" |
No (silent) |
This replaces the previous strategy of LLM-generated filler phrases, which caused timing issues. The FILLER PHRASE RULES sections were deleted from Booking and Modification prompts.
Prompt Engineering Notes¶
P0 Fixes Applied (v4.0.1)¶
- Router maxTokens: Increased from 150 to 400 (GPT-4o tool-call JSON alone = 80-120 tokens)
- Router greeting: Dynamic via
get_clinic_info, not hardcoded "Vitara Clinic" - Patient-ID: firstMessage removed from squad members (was causing 16s silence)
- All agents:
transferAssistantreferences replaced with actualhandoff_to_Xfunction names - Booking/Modification: FILLER PHRASE RULES sections deleted (replaced by tool-level request-start)
Key Constraints¶
- Tools are referenced by slug in assistant YAML frontmatter (e.g.,
find-earliest-appointment-7fc7534d), not by UUID - Squad YAML defines handoff routing between agents using
tools:appendwith handoff tool type - Each agent's prompt must reference the correct
handoff_to_Xtool names - Router must have maxTokens >= 400 to avoid GPT-4o silent truncation
- All tools use
blocking: falsein their request-start messages
File Format¶
- Assistants:
.mdfiles with YAML frontmatter (config) and Markdown body (system prompt) - Tools:
.ymlfiles with Vapi function tool schema - Squads:
.ymlfiles defining member routing and handoff tools
Processing Order¶
Push (dependency order): Tools → Structured Outputs → Assistants → Squads → Personalities → Scenarios → Simulations → Simulation Suites
Delete (reverse): Simulation Suites → Simulations → Scenarios → Personalities → Squads → Assistants → Structured Outputs → Tools
Reference Resolution¶
The engine resolves human-readable slugs to Vapi UUIDs at push time. You write filenames; the API receives UUIDs.
Source: vapi-gitops/src/resolver.ts:157-303
WHAT YOU WRITE (YAML) WHAT THE API RECEIVES
───────────────────── ─────────────────────
toolIds: toolIds:
- search-patient-by-phone-8474536c - "8474536c-663f-4a94-91ae-..."
members: members:
- assistantId: router-v3 - assistantId: "4f70e214-6111-..."
assistantDestinations: assistantDestinations:
- assistantId: patient-id-en - assistantId: "7d054785-9074-..."
Resolution Rules¶
| Rule | Behavior | Source |
|---|---|---|
| Slug → UUID | Look up slug in .vapi-state.{env}.json, return UUID |
resolver.ts:26-31 |
| Already a UUID | Pass through unchanged (regex match ^[0-9a-f]{8}-...) |
resolver.ts:22-24 |
| YAML comments | Strip ## Comment suffix before lookup (id.split("##")[0]) |
resolver.ts:19 |
| Not found | Emit ⚠️ Reference not found warning, skip the ID |
resolver.ts:28-29 |
Resolved Reference Points¶
The resolver walks the full resource tree. These are all the locations where slug → UUID resolution occurs:
| Location | Field | Applies To |
|---|---|---|
model.toolIds[] |
Tool slugs → UUIDs | Assistants |
artifactPlan.structuredOutputIds[] |
Structured output slugs | Assistants |
members[].assistantId |
Assistant slugs | Squads |
members[].assistantDestinations[].assistantId |
Handoff targets | Squads |
destinations[].assistantId |
Transfer targets | Handoff tools |
hooks[].do[].toolId |
Hook action tools | Assistants |
personalityId |
Personality slug | Simulations |
scenarioId |
Scenario slug | Simulations |
simulationIds[] |
Simulation slugs | Simulation Suites |
evaluations[].structuredOutputId |
Eval output slugs | Scenarios |
Dev vs Prod Environments¶
The engine supports multiple environments through separate state files and env files. Same resource YAML, different Vapi accounts.
SAME YAML FILES
┌──────────────┐
│ resources/ │
│ assistants/ │
│ tools/ │
│ squads/ │
└──────┬───────┘
│
┌────────────┴────────────┐
│ │
push:dev push:prod
│ │
.env.dev (token A) .env.prod (token B)
│ │
.vapi-state.dev.json .vapi-state.prod.json
(dev UUIDs) (prod UUIDs)
│ │
Vapi Dev Account Vapi Prod Account
| Aspect | Dev | Prod |
|---|---|---|
| Env file | .env.dev |
.env.prod |
| State file | .vapi-state.dev.json |
.vapi-state.prod.json |
| Push command | npm run push:dev |
npm run push:prod |
| Pull command | npm run pull:dev |
npm run pull:prod |
| Force flag | npm run push:dev:force |
npm run push:prod:force |
| Apply | npm run apply:dev |
npm run apply:prod |
| Call test | npm run call:dev |
npm run call:prod |
State Files Are Environment-Specific
The same slug (e.g., router-v3) maps to different UUIDs in dev vs prod. Never copy state files between environments — push creates them automatically.
Current State (Dev)¶
As of v4.3.0, the dev state file tracks:
| Resource Type | Count | Examples |
|---|---|---|
| Assistants | 9 | router-v3 → 4f70e214, booking-en → ac25775b |
| Tools | 14 | search-patient-by-phone-8474536c → 8474536c |
| Squads | 1 | vitaravox-v3 → 13fdfd19 |
Simulation Framework¶
Vapi simulations enable automated testing of voice agent flows. Resources are organized in simulations/ subdirectories.
| Resource | Directory | Purpose |
|---|---|---|
| Personality | simulations/personalities/ |
Define caller personas (e.g., "impatient patient", "Mandarin speaker") |
| Scenario | simulations/scenarios/ |
Define test scripts (e.g., "book appointment happy path") |
| Simulation | simulations/tests/ |
Combine personality + scenario into a test case |
| Suite | simulations/suites/ |
Group simulations into test runs |
File Formats¶
Personality:
name: Impatient Patient
description: Caller in a hurry, gives short answers
prompt: You are impatient and want to book quickly. Give minimal information.
Scenario:
name: Happy Path - Booking
description: New patient books a checkup
prompt: |
You are calling to book a general checkup.
Your name is John Smith, phone 604-555-1234.
evaluations:
- structuredOutputId: booking-eval # resolved to UUID at push
Simulation:
name: Booking Test 1
personalityId: impatient-patient # slug, resolved to UUID
scenarioId: happy-path-booking # slug, resolved to UUID
Suite:
Status
The simulation directories are currently empty. Population is planned for the v3.0 E5 Testing milestone (76 test cases).
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| "Reference not found" | Resource file missing or misnamed | Check filename matches exactly (case-sensitive), no extension in reference |
| "Cannot delete resource" | Still referenced by another resource | Remove references first, then push, then delete file |
| Resource not updating | Stale UUID in state file | Delete entry from .vapi-state.dev.json, re-push |
| Silent auth failure | Wrong env var name | Use VAPI_TOKEN, not VAPI_API_KEY |
| API "property X should not exist" | Immutable field in update | Add to UPDATE_EXCLUDED_KEYS in src/config.ts |
Related Documentation¶
- Tool Inventory — All 14 tools with params, UUIDs, and distribution matrix
- Squad Architecture — Agent topology, handoff matrix, voice config
- Prompt Engineering — Tool ordering, emergency keywords, consent UX
- Agent Behaviors — Per-agent step-by-step behavior