Loading...
Loading...
Opt-in recreation of the Bun + TypeScript framework used to build Vapi's landing-page voice agents from a ROUGH_DRAFT.md spec, including a scenario registry, language stacks, prompt composer, assistant builder, and idempotent bootstrap script. Use only when the user explicitly invokes vapi-bootstrap-framework or specifically asks to reproduce or experiment with the landing-page-agent architecture. Do not use for ordinary Vapi assistant builds; use create-assistant, create-tool, create-squad, and vapi-prompt-builder instead. Targets Bun + TypeScript + @vapi-ai/server-sdk.
npx skill4agent add vapiai/skills vapi-bootstrap-frameworkExperimental reference workflow: This skill recreates the architecture used for Vapi's landing-page agents. It is not the standard workflow for building Vapi assistants and should only be used when explicitly requested.
ROUGH_DRAFT.mdbun run bootstrapdashboard.vapi.aipackage.json@vapi-ai/server-sdkbootstraptsconfig.json.env.exampleVAPI_API_KEY(scenario × language).gitignorenode_modules.env*.localsrc/assistants/languages.tssrc/assistants/loadPrompt.tsloadPrompt(scenarioId, languageId)src/assistants/scenarios/index.tssrc/assistants/buildAssistant.ts(scenarioId, languageId)src/assistants/prompts/shared/preambles/es.mdsrc/assistants/scenarios/<scenarioId>.tssrc/assistants/prompts/<scenarioId>/body.mdsrc/assistants/prompts/<scenarioId>/off-topic-es.mdsrc/bootstrap.tsenesopenai gpt-4.10.5eleven_turbo_v2eleven_multilingual_v2nova-3stt-rt-v4ROUGH_DRAFT.md## N. <scenario name>scenarioIdqualificationappointment**On the page****Opening****Greeting**firstMessage.en**What happens**body.mdpackage.jsontsconfig.json.env.example.gitignore.env.exampleVAPI_ASSISTANT_<SCENARIO>_<LANG>package.json@vapi-ai/server-sdkbootstraptypecheckscenarios/<id>.tsprompts/<id>/body.mdprompts/<id>/off-topic-es.mdscenarios/index.tsSCENARIOSScenarioIdSCENARIO_IDSscenarioForsrc/bootstrap.tsclientToolsbody.md<PROJECT_NAME># Rough draft — <X>vapi-voice-agents<SCENARIO_ID>qualification<SCENARIO_NAME>Lead Qualification<FIRST_MESSAGE_EN><FIRST_MESSAGE_ES><BODY_DRAFT><SCENARIO_IMPORTS>import { <id> } from "./<id>.ts";<SCENARIO_KEYS>SCENARIOS = { ... }<ENV_ASSISTANT_SLOTS>(scenario × language)# VAPI_ASSISTANT_<SCENARIO>_<LANG>=package.json{
"name": "<PROJECT_NAME>",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"bootstrap": "bun run src/bootstrap.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@vapi-ai/server-sdk": "^1.2.0"
},
"devDependencies": {
"@types/bun": "^1.3.13",
"typescript": "^5.9.3"
},
"packageManager": "bun@1.3.1"
}tsconfig.json{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"types": ["bun"]
},
"include": ["src/**/*.ts"]
}.env.example# Get this from https://dashboard.vapi.ai/keys
VAPI_API_KEY=
# One slot per (scenario × language). First `bun run bootstrap` prints the
# ids; paste them here, then re-run for idempotent updates.
<ENV_ASSISTANT_SLOTS>.gitignore# dependencies
node_modules
# env (local-only secrets)
.env*.local
# os junk
.DS_Store
# bun
*.tsbuildinfosrc/assistants/languages.ts/**
* Per-language voice + transcriber stack. Adding a 3rd language is one
* entry in each record below.
*/
import type { LanguageId } from "./loadPrompt.ts";
export type { LanguageId };
interface VoiceConfig {
provider: "11labs";
model: string;
voiceId: string;
}
interface TranscriberConfig {
provider: "deepgram" | "soniox";
model: string;
language: string;
}
const VOICE_BY_LANGUAGE: Record<LanguageId, VoiceConfig> = {
en: {
provider: "11labs",
model: "eleven_turbo_v2",
voiceId: "ZoiZ8fuDWInAcwPXaVeq",
},
es: {
provider: "11labs",
model: "eleven_multilingual_v2",
voiceId: "JYyJjNPfmNJdaby8LdZs",
},
};
const TRANSCRIBER_BY_LANGUAGE: Record<LanguageId, TranscriberConfig> = {
en: { provider: "deepgram", model: "nova-3", language: "en" },
es: { provider: "soniox", model: "stt-rt-v4", language: "es" },
};
export const voiceFor = (languageId: LanguageId): VoiceConfig =>
VOICE_BY_LANGUAGE[languageId];
export const transcriberFor = (languageId: LanguageId): TranscriberConfig =>
TRANSCRIBER_BY_LANGUAGE[languageId];src/assistants/loadPrompt.ts/**
* EN returns body.md unchanged. ES prepends a Spanish preamble with the
* scenario's off-topic redirects spliced into {{OFF_TOPIC_LINES}}.
* One body.md per scenario drives every language variant.
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
export type LanguageId = "en" | "es";
const PROMPT_DIR = resolve(import.meta.dir, "prompts");
const read = (relativePath: string): string =>
readFileSync(resolve(PROMPT_DIR, relativePath), "utf8");
export const loadPrompt = (
scenarioId: string,
languageId: LanguageId,
): string => {
const body = read(`${scenarioId}/body.md`);
if (languageId === "en") return body;
const offTopic = read(`${scenarioId}/off-topic-${languageId}.md`).trim();
const preamble = read(`shared/preambles/${languageId}.md`).replace(
"{{OFF_TOPIC_LINES}}",
offTopic,
);
return `${preamble}\n\n${body}`;
};src/assistants/scenarios/index.ts/**
* Scenario registry. Adding a new scenario is one entry here plus one new
* file under `./<scenario-id>.ts` and matching prompts under `../prompts/<id>/`.
*/
// <SCENARIO_IMPORTS>
export const SCENARIOS = {
// <SCENARIO_KEYS>
} as const;
export type ScenarioId = keyof typeof SCENARIOS;
export const SCENARIO_IDS = Object.keys(SCENARIOS) as ScenarioId[];
export const scenarioFor = (id: ScenarioId) => SCENARIOS[id];src/assistants/buildAssistant.ts/**
* Compose a full Vapi assistant body for a (scenario, language) tuple.
* Voice + transcriber come from languages.ts; prompt from loadPrompt;
* name + firstMessage + clientTools from the scenario.
*/
import { transcriberFor, voiceFor } from "./languages.ts";
import { loadPrompt, type LanguageId } from "./loadPrompt.ts";
import { scenarioFor, type ScenarioId } from "./scenarios/index.ts";
export type { LanguageId, ScenarioId };
export const buildAssistant = (
scenarioId: ScenarioId,
languageId: LanguageId,
) => {
const scenario = scenarioFor(scenarioId);
const systemPrompt = loadPrompt(scenarioId, languageId);
return {
name: `${languageId.toUpperCase()} - ${scenario.name}`,
firstMessage: scenario.firstMessage[languageId],
voice: voiceFor(languageId),
transcriber: transcriberFor(languageId),
model: {
provider: "openai" as const,
model: "gpt-4.1",
temperature: 0.5,
messages: [{ role: "system" as const, content: systemPrompt }],
tools: scenario.clientTools,
},
};
};src/assistants/scenarios/<SCENARIO_ID>.ts/**
* <SCENARIO_NAME> scenario. Plain data: id, name, language-keyed first
* message. Later steps add `clientTools` (capture tools fire mid-call).
*/
// Rename `scenarioId` to the generated <SCENARIO_ID> identifier.
export const scenarioId = {
id: "<SCENARIO_ID>" as const,
name: "<SCENARIO_NAME>",
firstMessage: {
en: "<FIRST_MESSAGE_EN>",
es: "<FIRST_MESSAGE_ES>",
},
clientTools: [] as const,
};
// Rename this type to the generated <PascalCase scenario id>Scenario name.
export type ScenarioIdScenario = typeof scenarioId;<FIRST_MESSAGE_EN>" + "src/assistants/prompts/<SCENARIO_ID>/body.md# <SCENARIO_NAME> voice agent
You are the <SCENARIO_NAME> voice agent for Vapi. <One sentence on context — who you're talking to and why.>
<One paragraph distilling **What happens** from the rough draft: the questions to ask, the data to collect, the routing logic, the wrap-up.>
Be warm and curious. Ask one question at a time. If they go off-topic, redirect briefly and return to the next missing field. Keep replies short — you are speaking, not typing.## Absolute rulessrc/assistants/prompts/<SCENARIO_ID>/off-topic-es.md- "Buena pregunta — el equipo te puede ayudar con eso. ¿Podemos seguir con <next field>?"
- "Tomo nota, lo vemos después. Mientras tanto, cuéntame <one short ask tied to the scenario>."src/assistants/prompts/shared/preambles/es.md# IDIOMA / LANGUAGE OVERRIDE
The contract that follows this preamble is written in English. **This call is in Spanish.**
Override the language rule of the contract:
- ALL agent speech MUST be in natural, conversational Latin American Spanish. Translate the exact wordings, examples, and acks from the contract — don't switch back to English mid-sentence.
- Read brand and product names in their original form. Don't translate them.
- When you call any capture tool, **always pass free-text fields as a short English summary**, regardless of the call language. Cross-language analytics depend on it.
## Off-topic redirects (use verbatim)
If the visitor goes off-topic, pick one of these and then return to the next missing field:
{{OFF_TOPIC_LINES}}
---src/bootstrap.ts/**
* Idempotent upsert across (scenario × language). One entry per tuple, keyed
* by VAPI_ASSISTANT_<SCENARIO>_<LANG>. First run creates + prints ids;
* subsequent runs update in place.
*
* Run with `bun run bootstrap`. Bun auto-loads .env.local.
*/
import { VapiClient } from "@vapi-ai/server-sdk";
import {
buildAssistant,
type LanguageId,
type ScenarioId,
} from "./assistants/buildAssistant.ts";
import { SCENARIO_IDS } from "./assistants/scenarios/index.ts";
const LANGUAGES: LanguageId[] = ["en", "es"];
const envVarFor = (scenarioId: ScenarioId, languageId: LanguageId): string =>
`VAPI_ASSISTANT_${scenarioId.toUpperCase()}_${languageId.toUpperCase()}`;
const requireEnv = (name: string): string => {
const value = process.env[name];
if (!value) {
console.error(`✗ Missing env var: ${name}. See .env.example.`);
process.exit(1);
}
return value;
};
const main = async () => {
const vapi = new VapiClient({ token: requireEnv("VAPI_API_KEY") });
const created: Array<{ envVar: string; id: string }> = [];
for (const scenarioId of SCENARIO_IDS) {
for (const languageId of LANGUAGES) {
const envVar = envVarFor(scenarioId, languageId);
const existingId = process.env[envVar];
const body = buildAssistant(
scenarioId,
languageId,
) as unknown as Parameters<typeof vapi.assistants.create>[0];
const label = `${scenarioId}/${languageId}`;
let updated = false;
if (existingId) {
try {
await vapi.assistants.update({
id: existingId,
...body,
} as unknown as Parameters<typeof vapi.assistants.update>[0]);
console.log(`✓ Updated ${label} → ${existingId}`);
updated = true;
} catch (err) {
const statusCode = (err as { statusCode?: number })?.statusCode;
if (statusCode === 404) {
console.log(
` ${envVar}=${existingId} not found in this org; creating a new assistant.`,
);
} else {
throw err;
}
}
}
if (!updated) {
const assistant = await vapi.assistants.create(body);
console.log(`✓ Created ${label} → ${assistant.id}`);
created.push({ envVar, id: assistant.id });
}
}
}
if (created.length > 0) {
console.log("\nAdd these to .env.local:");
for (const { envVar, id } of created) {
console.log(` ${envVar}=${id}`);
}
console.log(
"\nThen re-run `bun run bootstrap` to confirm idempotent updates.",
);
} else {
console.log(
`\nAll ${SCENARIO_IDS.length * LANGUAGES.length} assistants updated in place.`,
);
}
};
main().catch((err) => {
console.error("✗ Bootstrap failed:", err);
process.exit(1);
});# Rough draft — <project name>
## 1. <First scenario name>
**On the page**: "<one verbatim opening line the agent will say>"
**What happens**: <One paragraph: the questions the agent asks, the data it collects, what it does with edge cases, how it wraps up.>
## 2. <Second scenario name>
**Opening**: "<one verbatim opening line>"
**What happens**: <One paragraph.>VOICE_BY_LANGUAGETRANSCRIBER_BY_LANGUAGELanguageIdloadPrompt.tsprompts/shared/preambles/<lang>.mdfirstMessage.<lang>LANGUAGESbootstrap.tsVAPI_ASSISTANT_<SCENARIO>_<LANG>.env.examplebuildAssistant.tsVoiceConfigVOICE_BY_LANGUAGElanguages.tsbun install
bun run typecheck # validates locally; creates nothing in Vapi
cp .env.example .env.local # add VAPI_API_KEY from dashboard.vapi.ai/org/api-keys
bun run bootstrap # creates one assistant per (scenario × language)
# paste the printed VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id> lines into .env.local
bun run bootstrap # second run prints "Updated ..." for every tuple