Manage Vapi resources (Assistants, Structured Outputs, and Tools) via Git using YAML 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 + apply |
| 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 — Git is the truth; manual console changes get overwritten
- Automation Ready — Plug into CI/CD pipelines
⚠️ Note: This project currently supports:
- ✅ Assistants
- ✅ Tools
- ✅ Structured Outputs
Want to manage other Vapi resources? The codebase is designed to be extensible. Add support for Squads, Phone Numbers, Files, Knowledge Bases, and more by following the patterns in
src/.
- Node.js installed
- Vapi API token
cd vapi-gitops
npm installThis installs all dependencies including Bun locally (no global install needed).
# 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 resources from Vapi to local YAML files |
npm run pull:prod |
Pull resources from prod |
npm run apply:dev |
Push local YAML files to Vapi (dev) |
npm run apply:prod |
Push local YAML files to Vapi (prod) |
# Pull existing resources from Vapi
npm run pull:dev
# Make changes to YAML files...
# Push changes back to Vapi
npm run apply:devStep 1: Create a new YAML file in resources/tools/
touch resources/tools/my-new-tool.ymlStep 2: Define the tool configuration
# resources/tools/my-new-tool.yml
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/weatherStep 3: Apply the changes
npm run apply:devThe tool will be created and its UUID saved to .vapi-state.dev.json.
Step 1: Note the tool's filename (without extension) - this is its resource ID
resources/tools/my-new-tool.yml → resource ID: "my-new-tool"
Step 2: Edit your assistant YAML and add the tool reference
# resources/assistants/my-assistant.yml
name: My Assistant
model:
provider: openai
model: gpt-4o
messages:
- role: system
content: You are a helpful assistant.
toolIds:
- my-new-tool # ← Reference by filename (without .yml)
- transfer-call # ← You can add multiple tools
firstMessage: Hello! How can I help you?Step 3: Apply the changes
npm run apply:devThe apply engine will:
- Create/update the tool first
- Resolve
my-new-tool→ actual Vapi UUID - Create/update the assistant with the resolved UUID
Step 1: Create a structured output in resources/structuredOutputs/
# resources/structuredOutputs/call-summary.yml
name: Call Summary
type: ai
description: Summarizes the key points of a call
schema:
type: object
properties:
summary:
type: string
description: Brief summary of the call
sentiment:
type: string
enum: [positive, neutral, negative]
actionItems:
type: array
items:
type: string
model:
provider: openai
model: gpt-4o
assistant_ids:
- my-assistant # ← Links to the assistant
workflow_ids: []Step 2: Reference it in your assistant's artifact plan
# resources/assistants/my-assistant.yml
name: My Assistant
model:
provider: openai
model: gpt-4o
# ... other config
artifactPlan:
structuredOutputIds:
- call-summary # ← Reference by filenameStep 3: Apply
npm run apply:devStep 1: Remove any references to the resource first
If deleting a tool, remove it from all assistants' toolIds:
# Before - resources/assistants/my-assistant.yml
model:
toolIds:
- transfer-call
- my-tool-to-delete # ← Remove this line
# After
model:
toolIds:
- transfer-callStep 2: Delete the resource file
rm resources/tools/my-tool-to-delete.ymlStep 3: Apply
npm run apply:devThe apply engine will:
- Detect the resource is in state but not in filesystem
- Check for orphan references (will error if still referenced)
- Delete the resource from Vapi
- Remove it from the state file
⚠️ Important: If you try to delete a resource that's still referenced, you'll get an error like:Cannot delete tool "my-tool" - still referenced by: assistants/my-assistant
Renaming requires deleting the old and creating a new resource.
Step 1: Update all references to use the new name
# resources/assistants/my-assistant.yml
model:
toolIds:
- new-tool-name # ← Update to new nameStep 2: Rename the file
mv resources/tools/old-tool-name.yml resources/tools/new-tool-name.ymlStep 3: Apply
npm run apply:devThis will:
- Delete the old resource (old-tool-name)
- Create the new resource (new-tool-name)
- Update state file with new mapping
Step 1: Test in dev first
npm run apply:devStep 2: Verify everything works, then apply to prod
npm run apply:prodEach environment has its own:
.env.{env}- API token.vapi-state.{env}.json- Resource UUID mappings
Example: Adding a staging environment.
Step 1: Add the environment to the valid environments list
Edit src/types.ts:
export type Environment = "dev" | "staging" | "prod";
export const VALID_ENVIRONMENTS: readonly Environment[] = ["dev", "staging", "prod"];Step 2: Add a new npm script
Edit package.json:
{
"scripts": {
"apply:dev": "tsx src/apply.ts dev",
"apply:staging": "tsx src/apply.ts staging",
"apply:prod": "tsx src/apply.ts prod"
}
}Step 3: Create the environment secrets file
echo "VAPI_TOKEN=your-staging-token" > .env.stagingStep 4: Initialize the state file (optional - created automatically on first run)
echo '{"assistants":{},"structuredOutputs":{},"tools":{}}' > .vapi-state.staging.jsonStep 5: Apply to the new environment
npm run apply:stagingThis creates all resources in the staging Vapi account and populates .vapi-state.staging.json with the new UUIDs.
You can create subdirectories to organize resources by tenant, team, or feature. The folder path becomes part of the resource ID.
Example: Multi-tenant setup with company-specific assistants and tools.
Step 1: Create folder structure
resources/
├── assistants/
│ ├── inbound-support.yml # Shared/base assistant
│ └── company-1/
│ └── inbound-support.yml # Company-specific assistant
├── tools/
│ ├── transfer-call.yml # Shared tool
│ └── company-1/
│ └── transfer-call.yml # Company-specific tool
└── structuredOutputs/
└── customer-sentiment.yml # Shared structured output
Step 2: Reference nested resources using their full path
In a nested assistant (resources/assistants/company-1/inbound-support.yml):
name: Company 1 Support
model:
provider: openai
model: gpt-4o
toolIds:
- company-1/transfer-call # ← Full path for nested tool
- get-user # ← Root-level tool (no path)
artifactPlan:
structuredOutputIds:
- customer-sentiment # ← Root-level structured outputStep 3: Link structured outputs to nested assistants
In resources/structuredOutputs/customer-sentiment.yml:
name: Customer Sentiment
type: ai
schema:
type: string
enum: [positive, neutral, negative]
assistant_ids:
- inbound-support # ← Root-level assistant
- company-1/inbound-support # ← Nested assistant (full path!)Step 4: Apply
npm run apply:devThe state file will track both:
{
"assistants": {
"inbound-support": "uuid-1111",
"company-1/inbound-support": "uuid-2222"
},
"tools": {
"transfer-call": "uuid-3333",
"company-1/transfer-call": "uuid-4444"
}
}
⚠️ Important: When referencing nested resources from any YAML file, always use the full path (e.g.,company-1/transfer-call), not just the filename.
You can add inline comments to document references:
model:
toolIds:
- transfer-call ## Transfers to human support
- get-weather ## Fetches weather data from API
artifactPlan:
structuredOutputIds:
- call-summary ## Generated at end of each callThe apply engine strips everything after ## when resolving references.
vapi-gitops/
├── src/
│ ├── apply.ts # Apply entry point & functions
│ ├── pull.ts # Pull entry point & functions
│ ├── types.ts # TypeScript interfaces
│ ├── config.ts # Environment & configuration
│ ├── api.ts # Vapi HTTP client
│ ├── state.ts # State file management
│ ├── resources.ts # Resource loading
│ ├── resolver.ts # Reference resolution
│ └── delete.ts # Deletion & orphan checks
├── resources/
│ ├── assistants/ # Assistant YAML files
│ │ └── {tenant}/ # Optional: nested folders for multi-tenant
│ ├── structuredOutputs/ # Structured output YAML files
│ └── tools/ # Tool YAML files
│ └── {tenant}/ # Optional: nested folders for multi-tenant
├── .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
| Variable | Required | Description |
|---|---|---|
VAPI_TOKEN |
✅ | API authentication token |
VAPI_BASE_URL |
❌ | API base URL (defaults to https://api.vapi.ai) |
Some properties can't be sent during updates. Configure in src/config.ts:
export const UPDATE_EXCLUDED_KEYS: Record<ResourceType, string[]> = {
tools: ["type"], // 'type' can't be changed after creation
assistants: [],
structuredOutputs: ["type"],
};State files track the mapping between resource IDs and Vapi UUIDs:
{
"assistants": {
"my-assistant": "uuid-1234-5678"
},
"structuredOutputs": {
"call-summary": "uuid-abcd-efgh"
},
"tools": {
"transfer-call": "uuid-wxyz-1234"
}
}- Load resource files from
/resources - Load environment-specific state file
- Delete orphaned resources (in state but not in filesystem)
- For each resource:
- If resource ID exists in state → UPDATE using stored UUID
- If not → CREATE new resource, save UUID to state
- Resolve cross-references (tool IDs → UUIDs)
- Save updated state file
Deletions (reverse dependency order):
- Assistants → 2. Structured Outputs → 3. Tools
Creates/Updates (dependency order):
- Tools → 2. Structured Outputs → 3. Assistants → 4. Link outputs to assistants
See Vapi Assistants API for all available properties.
See Vapi Structured Outputs API for all available properties.
See Vapi Tools API for all available properties.
The resource you're referencing doesn't exist yet. Make sure:
- The referenced file exists in the correct folder
- The filename matches exactly (case-sensitive)
- You're using the filename without the
.ymlextension - For nested resources, use the full path (e.g.,
company-1/transfer-callnot justtransfer-call)
When using folders, structured outputs must reference assistants by their full path:
# ❌ Wrong - won't find the nested assistant
assistant_ids:
- inbound-support
# ✅ Correct - uses full path for nested assistant
assistant_ids:
- inbound-support # Root-level
- company-1/inbound-support # Nested (full path)Remove the reference from other resources before deleting:
- Find which resources reference it (shown in error message)
- Edit those files to remove the reference
- Apply again
- Then delete the resource file
Some properties can't be updated after creation. Add them to UPDATE_EXCLUDED_KEYS in src/config.ts.
Check the state file has the correct UUID:
- Open
.vapi-state.{env}.json - Find the resource entry
- If incorrect, delete the entry and re-run apply