Manage Vapi resources via Git using YAML/Markdown as the source-of-truth.
| Dashboard / Ad-hoc API | GitOps | |
|---|---|---|
| History | Limited visibility of who changed what | Full git history with blame |
| Review | Changes go live immediately (can break things) | PR review before deploy |
| Rollback | Manual recreation | git revert + push |
| Environments | Tedious to copy-paste between envs | Same config, different state files |
| Collaboration | One person at a time. Need to duplicate assistants, tools, etc. | Team can collaborate and use git branching |
| Reproducibility | "It worked on my assistant!" | Declarative, version-controlled |
| Disaster Recovery | Hope you have backups | Re-apply from git |
- Audit Trail — Every change is a commit with author, timestamp, and reason
- Code Review — Catch misconfigurations before they hit production
- Environment Parity — Dev, staging, and prod stay in sync
- No Drift — Pull merges platform changes; push makes git the truth
- Automation Ready — Plug into CI/CD pipelines
| Resource | Status | Format |
|---|---|---|
| Assistants | ✅ | .md (with system prompt) or .yml |
| Tools | ✅ | .yml |
| Structured Outputs | ✅ | .yml |
| Squads | ✅ | .yml |
| Personalities | ✅ | .yml |
| Scenarios | ✅ | .yml |
| Simulations | ✅ | .yml |
| Simulation Suites | ✅ | .yml |
- Node.js installed
- Vapi API token
npm install# Create your .env file with your Vapi token
echo "VAPI_TOKEN=your-token-here" > .env.dev| Command | Description |
|---|---|
npm run build |
Type-check the codebase |
npm run pull:dev |
Pull platform state, preserve local changes |
npm run pull:dev:force |
Pull platform state, overwrite everything |
npm run pull:prod |
Pull from prod, preserve local changes |
npm run pull:prod:force |
Pull from prod, overwrite everything |
npm run push:dev |
Push local files to Vapi (dev) |
npm run push:prod |
Push local files to Vapi (prod) |
npm run apply:dev |
Pull → Merge → Push in one shot (dev) |
npm run apply:prod |
Pull → Merge → Push in one shot (prod) |
npm run push:dev assistants |
Push only assistants (dev) |
npm run push:dev tools |
Push only tools (dev) |
npm run call:dev -- -a <name> |
Start a WebSocket call to an assistant (dev) |
npm run call:dev -- -s <name> |
Start a WebSocket call to a squad (dev) |
# First time: pull all resources from Vapi
npm run pull:dev:force
# Commit the initial state
git add . && git commit -m "initial pull"
# Make changes to YAML/MD files...
# Push changes to Vapi
npm run push:devBy default, pull preserves any files you've locally modified or deleted:
# Edit an assistant locally...
npm run pull:dev
# ⏭️ my-assistant (locally changed, skipping)
# ✨ new-tool -> resources/tools/new-tool.yml
# Your edits are preserved, new platform resources are downloadedWhen you want the platform version of everything, overwriting all local files:
npm run pull:dev:force
# ⚡ Force mode: overwriting all local files with platform state# Pull platform state (your local changes are preserved)
npm run pull:dev
# See what changed on the platform vs your last commit
git diff
# Accept platform changes for a specific file
git checkout -- resources/tools/some-tool.ymlPush only specific resources instead of syncing everything:
npm run push:dev assistants
npm run push:dev tools
npm run push:dev squads
npm run push:dev structuredOutputs
npm run push:dev personalities
npm run push:dev scenarios
npm run push:dev simulations
npm run push:dev simulationSuites# Push a single file
npm run push:dev resources/assistants/my-assistant.md
# Push multiple files
npm run push:dev resources/assistants/booking.md resources/tools/my-tool.yml# Push specific file within a type
npm run push:dev assistants resources/assistants/booking.mdNote: Partial pushes skip deletion checks. Run full npm run push:dev to sync deletions.
vapi-gitops/
├── src/
│ ├── pull.ts # Pull platform state (with git stash/pop merge)
│ ├── push.ts # Push local state to platform
│ ├── apply.ts # Orchestrator: pull → merge → push
│ ├── call.ts # WebSocket call script
│ ├── 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/ # Assistant files (.md or .yml)
│ ├── tools/ # Tool YAML files
│ ├── structuredOutputs/ # Structured output YAML files
│ ├── squads/ # Squad YAML files
│ └── simulations/ # Simulation resources
│ ├── personalities/ # Personality YAML files
│ ├── scenarios/ # Scenario YAML files
│ ├── tests/ # Simulation YAML files
│ └── suites/ # Simulation suite YAML files
├── .env.dev # Dev environment secrets (gitignored)
├── .env.prod # Prod environment secrets (gitignored)
├── .vapi-state.dev.json # Dev state file
└── .vapi-state.prod.json # Prod state file
Assistants with system prompts use Markdown with YAML frontmatter. The system prompt is written as readable Markdown below the config:
---
name: My Assistant
voice:
provider: 11labs
voiceId: abc123
model:
model: gpt-4o
provider: openai
toolIds:
- my-tool
firstMessage: Hello! How can I help you?
---
# Identity & Purpose
You are a helpful assistant for Acme Corp.
# Conversation Flow
1. Greet the user
2. Ask how you can help
3. Resolve their issue
# Rules
- Always be polite
- Never make up informationBenefits:
- System prompts are readable Markdown (not escaped YAML strings)
- Proper syntax highlighting in editors
- Easy to write headers, lists, tables
- Configuration stays cleanly separated at the top
Simple assistants without custom system prompts use plain YAML:
name: Simple Assistant
voice:
provider: vapi
voiceId: Elliot
model:
model: gpt-4o-mini
provider: openai
firstMessage: Hello!type: function
function:
name: get_weather
description: Get the current weather for a location
parameters:
type: object
properties:
location:
type: string
description: The city name
required:
- location
server:
url: https://my-api.com/weathername: Call Summary
type: ai
description: Summarizes the key points of a call
schema:
type: object
properties:
summary:
type: string
sentiment:
type: string
enum: [positive, neutral, negative]
assistant_ids:
- my-assistantname: Support Squad
members:
- assistantId: intake-agent
assistantDestinations:
- type: assistant
assistantId: specialist-agent
message: Transferring you to a specialist.
- assistantId: specialist-agentPersonality (simulations/personalities/):
name: Skeptical Sam
description: A doubtful caller who questions everything
prompt: You are skeptical and need convincing before trusting information.Scenario (simulations/scenarios/):
name: Happy Path - New Customer
description: New customer calling to schedule an appointment
prompt: |
You are a new customer calling to schedule your first appointment.
Be cooperative and provide all requested information.Simulation (simulations/tests/):
name: Booking Test Case 1
personalityId: skeptical-sam
scenarioId: happy-path-new-customerSimulation Suite (simulations/suites/):
name: Booking Flow Tests
simulationIds:
- booking-test-case-1
- booking-test-case-2
- booking-test-case-3Option 1: With System Prompt (recommended)
Create resources/assistants/my-assistant.md:
---
name: My Assistant
voice:
provider: 11labs
voiceId: abc123
model:
model: gpt-4o
provider: openai
toolIds:
- my-tool
---
# Your System Prompt Here
Instructions for the assistant...Option 2: Without System Prompt
Create resources/assistants/my-assistant.yml:
name: My Assistant
voice:
provider: vapi
voiceId: Elliot
model:
model: gpt-4o-mini
provider: openaiThen push:
npm run push:devCreate resources/tools/my-tool.yml:
type: function
function:
name: do_something
description: Does something useful
parameters:
type: object
properties:
input:
type: string
required:
- input
server:
url: https://my-api.com/endpointUse the filename without extension as the resource ID:
# In an assistant
model:
toolIds:
- my-tool # → resources/tools/my-tool.yml
- utils/helper-tool # → resources/tools/utils/helper-tool.yml
artifactPlan:
structuredOutputIds:
- call-summary # → resources/structuredOutputs/call-summary.yml# In a squad
members:
- assistantId: intake-agent # → resources/assistants/intake-agent.md# In a simulation
personalityId: skeptical-sam # → resources/simulations/personalities/skeptical-sam.yml
scenarioId: happy-path # → resources/simulations/scenarios/happy-path.yml- Remove references to the resource from other files
- Delete the file:
rm resources/tools/my-tool.yml - Push:
npm run push:dev
The engine will:
- Detect the resource is in state but not in filesystem
- Check for orphan references (will error if still referenced)
- Delete from Vapi
- Remove from state file
Create subdirectories for multi-tenant or feature organization:
resources/
├── assistants/
│ ├── shared/
│ │ └── fallback.md
│ └── client-a/
│ └── support.md
├── tools/
│ ├── shared/
│ │ └── transfer-call.yml
│ └── client-a/
│ └── custom-api.yml
Reference using full paths:
model:
toolIds:
- shared/transfer-call
- client-a/custom-apiYour local files are the source of truth. The engine respects that:
pull (default) pull --force push
───────────── ───────────── ─────────────
Download from Download from Upload local
platform, skip platform, overwrite files to
locally changed everything platform
files
pull downloads platform state. In default mode (git repo required), it detects locally modified or deleted files and skips them — your local work is preserved. New platform resources are still downloaded. Use --force to overwrite everything.
push is the engine — reads local files and syncs them to the platform. Deleted files are removed from the platform.
apply is the convenience wrapper — runs pull then push in sequence.
Note: The "skip locally changed files" feature requires a git repo with at least one commit. Without git, pull always overwrites (same as
--force).
Pull (dependency order):
- Tools
- Structured Outputs
- Assistants
- Squads
- Personalities
- Scenarios
- Simulations
- Simulation Suites
Push (dependency order):
- Tools → 2. Structured Outputs → 3. Assistants → 4. Squads
- Personalities → 6. Scenarios → 7. Simulations → 8. Simulation Suites
Delete (reverse dependency order):
- Simulation Suites → 2. Simulations → 3. Scenarios → 4. Personalities
- Squads → 6. Assistants → 7. Structured Outputs → 8. Tools
The engine automatically resolves resource IDs to Vapi UUIDs:
# You write:
toolIds:
- my-tool
# Engine sends to API:
toolIds:
- "uuid-1234-5678-abcd"Tracks mapping between resource IDs and Vapi UUIDs:
{
"tools": {
"my-tool": "uuid-1234"
},
"assistants": {
"my-assistant": "uuid-5678"
},
"squads": {
"my-squad": "uuid-abcd"
},
"personalities": {
"skeptical-sam": "uuid-efgh"
}
}| Variable | Required | Description |
|---|---|---|
VAPI_TOKEN |
✅ | API authentication token |
VAPI_BASE_URL |
❌ | API base URL (defaults to https://api.vapi.ai) |
Some fields are excluded when writing to files (server-managed):
id,orgId,createdAt,updatedAtanalyticsMetadata,isDeletedisServerUrlSecretSet,workflowIds
The referenced resource doesn't exist. Check:
- File exists in correct folder
- Filename matches exactly (case-sensitive)
- Using filename without extension
- For nested resources, use full path (
folder/resource)
- Find which resources reference it (shown in error)
- Remove the references
- Push again
- Then delete the resource file
Check the state file has correct UUID:
- Open
.vapi-state.{env}.json - Find the resource entry
- If incorrect, delete entry and re-run push
Some properties can't be updated after creation. Add them to UPDATE_EXCLUDED_KEYS in src/config.ts.