vapi-bootstrap-framework

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vapi bootstrap framework

Vapi Bootstrap框架

Experimental 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.
Scaffold an entire Vapi voice-agent project from a single
ROUGH_DRAFT.md
spec. Works in an empty folder or alongside an existing Bun + TypeScript project. One
bun run bootstrap
after this skill finishes puts the whole fleet live in
dashboard.vapi.ai
.
实验性参考工作流: 本技能用于重建Vapi着陆页代理所使用的架构。它并非构建Vapi助手的标准工作流,仅在明确请求时使用。
从单个
ROUGH_DRAFT.md
规范快速搭建完整的Vapi语音代理项目。可在空文件夹或现有Bun + TypeScript项目中运行。技能执行完成后,只需运行一次
bun run bootstrap
,即可在
dashboard.vapi.ai
上部署所有代理。

What this skill produces

本技能生成的内容

Project scaffolding (only created if absent — never overwritten):
  • package.json
    — Bun +
    @vapi-ai/server-sdk
    + a
    bootstrap
    script
  • tsconfig.json
    — strict no-emit TypeScript validation
  • .env.example
    VAPI_API_KEY
    placeholder + one slot per
    (scenario × language)
    tuple
  • .gitignore
    node_modules
    ,
    .env*.local
    , OS junk
Framework spine (always created — architectural; every later change extends a slot here, never the spine itself):
  • src/assistants/languages.ts
    — per-language voice + transcriber stack
  • src/assistants/loadPrompt.ts
    loadPrompt(scenarioId, languageId)
    composer
  • src/assistants/scenarios/index.ts
    — the scenario registry
  • src/assistants/buildAssistant.ts
    — composes a full Vapi assistant body for any
    (scenarioId, languageId)
    tuple
  • src/assistants/prompts/shared/preambles/es.md
    — Spanish language preamble (extend with more languages later)
Content (always created — one set per scenario detected in the rough draft):
  • src/assistants/scenarios/<scenarioId>.ts
    — id, name, language-keyed first message
  • src/assistants/prompts/<scenarioId>/body.md
    — rough first-draft system prompt
  • src/assistants/prompts/<scenarioId>/off-topic-es.md
    — Spanish redirect lines
Entry point (always created):
  • src/bootstrap.ts
    — idempotent double loop over scenarios × languages
Defaults: languages
en
+
es
. Model
openai gpt-4.1
temperature
0.5
. Voices ElevenLabs
eleven_turbo_v2
(EN) /
eleven_multilingual_v2
(ES). Transcriber Deepgram
nova-3
(EN) / Soniox
stt-rt-v4
(ES). Override these only if the user explicitly asks.
项目脚手架(仅在文件不存在时创建,绝不会覆盖现有文件):
  • package.json
    — 包含Bun、
    @vapi-ai/server-sdk
    bootstrap
    脚本
  • tsconfig.json
    — 严格的无输出TypeScript验证配置
  • .env.example
    VAPI_API_KEY
    占位符,以及每个
    (场景 × 语言)
    元组对应的配置项
  • .gitignore
    — 忽略
    node_modules
    .env*.local
    和系统临时文件
框架核心(始终创建,属于架构层;后续所有更改仅扩展此处的配置项,绝不修改核心本身):
  • src/assistants/languages.ts
    — 各语言对应的语音+转录器栈
  • src/assistants/loadPrompt.ts
    loadPrompt(scenarioId, languageId)
    提示编辑器
  • src/assistants/scenarios/index.ts
    — 场景注册表
  • src/assistants/buildAssistant.ts
    — 为任意
    (scenarioId, languageId)
    元组生成完整的Vapi助手配置
  • src/assistants/prompts/shared/preambles/es.md
    — 西班牙语前置提示(后续可扩展更多语言)
场景内容(始终创建,每个从草稿中识别的场景对应一套内容):
  • src/assistants/scenarios/<scenarioId>.ts
    — 场景ID、名称、多语言初始消息
  • src/assistants/prompts/<scenarioId>/body.md
    — 初稿系统提示
  • src/assistants/prompts/<scenarioId>/off-topic-es.md
    — 西班牙语话题重定向语句
入口文件(始终创建):
  • src/bootstrap.ts
    — 遍历场景×语言的幂等双循环脚本
默认配置:语言为
en
(英文)+
es
(西班牙语);模型为OpenAI GPT-4.1,温度值0.5;语音使用ElevenLabs的
eleven_turbo_v2
(英文)/
eleven_multilingual_v2
(西班牙语);转录器使用Deepgram的
nova-3
(英文)/ Soniox的
stt-rt-v4
(西班牙语)。仅当用户明确要求时才修改这些默认配置。

Workflow

工作流程

  1. Check for
    ROUGH_DRAFT.md
    at the project root.
    • If present → continue with step 2.
    • If missing → tell the user the skill needs a rough draft to work from. Offer the template under "ROUGH_DRAFT.md template" below; either paste it in for them to fill out, or wait for them to provide their own. Don't proceed past this step without one.
  2. Detect scenarios — every
    ## N. <scenario name>
    heading is one scenario. Derive a snake_case
    scenarioId
    from the name (e.g. "Lead Qualification & Screening" →
    qualification
    ; "Appointment Scheduling" →
    appointment
    ). When in doubt, pick the shortest unambiguous noun. Keep ids short — they become env var names.
  3. Extract the opening line — under each scenario, look for
    **On the page**
    ,
    **Opening**
    ,
    **Greeting**
    , or the first quoted string in the section. That's
    firstMessage.en
    verbatim.
  4. Distill the flow
    **What happens**
    (or equivalent prose) becomes a rough first-draft
    body.md
    . Persona-driven, not a contract — no failure rules, no exact wordings, no scripted off-ramps yet. Keep it short.
  5. Scaffold the project root — for each of
    package.json
    ,
    tsconfig.json
    ,
    .env.example
    ,
    .gitignore
    : if the file is absent, create it from the template below. If
    .env.example
    already exists, append any missing
    VAPI_ASSISTANT_<SCENARIO>_<LANG>
    slots; never reorder or remove existing lines. If
    package.json
    exists, leave it alone but verify it has
    @vapi-ai/server-sdk
    in deps plus
    bootstrap
    and
    typecheck
    scripts — tell the user if any are missing instead of editing.
  6. Generate the framework spine — copy the five spine files verbatim from the templates below. They don't change between projects; only the scenario registry's imports do.
  7. Generate per-scenario files — one
    scenarios/<id>.ts
    , one
    prompts/<id>/body.md
    , one
    prompts/<id>/off-topic-es.md
    per detected scenario.
  8. Wire up the registry
    scenarios/index.ts
    imports every scenario and exports the
    SCENARIOS
    const,
    ScenarioId
    ,
    SCENARIO_IDS
    ,
    scenarioFor
    .
  9. Write
    src/bootstrap.ts
    with the double-loop template below.
  10. Translate first messages to Spanish — natural Latin American Spanish, brand names untranslated.
  11. Tell the user how to run it (see "Verification" below).
Keep each scenario's
clientTools
array empty in this skill — functional capture tools land in a follow-up step. Do not rewrite
body.md
as a contract here either — that's a separate step.
  1. 检查项目根目录下的
    ROUGH_DRAFT.md
    • 若存在 → 继续步骤2
    • 若缺失 → 告知用户本技能需要草稿文件才能运行。提供下方的「ROUGH_DRAFT.md模板」,可直接粘贴供用户填写,或等待用户提供自定义草稿。没有草稿文件则不继续执行后续步骤。
  2. 识别场景 — 每个
    ## N. <场景名称>
    标题对应一个场景。从场景名称中派生蛇形命名法的
    scenarioId
    (例如:"Lead Qualification & Screening" →
    qualification
    ;"Appointment Scheduling" →
    appointment
    )。如有疑问,选择最短的明确名词。ID需简短,因为它们会成为环境变量名称。
  3. 提取初始语句 — 在每个场景下,查找
    **On the page**
    **Opening**
    **Greeting**
    或该部分的第一个引用字符串,将其直接作为
    firstMessage.en
  4. 提炼流程
    **What happens**
    (或类似描述)将成为初稿
    body.md
    的内容。以角色为导向,而非严格契约 — 暂不包含失败规则、精确措辞或脚本化退出流程。内容需简洁。
  5. 搭建项目根目录文件 — 针对
    package.json
    tsconfig.json
    .env.example
    .gitignore
    :若文件不存在,则从下方模板创建;若
    .env.example
    已存在,则追加缺失的
    VAPI_ASSISTANT_<SCENARIO>_<LANG>
    配置项,绝不重新排序或删除现有行;若
    package.json
    已存在,则保持原样,但需验证其依赖中是否包含
    @vapi-ai/server-sdk
    ,以及是否有
    bootstrap
    typecheck
    脚本 — 若缺失则告知用户,而非直接编辑。
  6. 生成框架核心文件 — 从下方模板直接复制5个核心文件,这些文件在不同项目中不会改变;仅场景注册表的导入内容会变化。
  7. 生成场景专属文件 — 每个识别出的场景对应一个
    scenarios/<id>.ts
    、一个
    prompts/<id>/body.md
    和一个
    prompts/<id>/off-topic-es.md
  8. 关联场景注册表
    scenarios/index.ts
    导入所有场景并导出
    SCENARIOS
    常量、
    ScenarioId
    类型、
    SCENARIO_IDS
    数组和
    scenarioFor
    函数。
  9. 编写
    src/bootstrap.ts
    — 使用下方的双循环模板。
  10. 翻译初始语句为西班牙语 — 使用自然的拉美西班牙语,品牌名称不翻译。
  11. 告知用户运行方式(见下方「验证步骤」)。
在本技能中,每个场景的
clientTools
数组需保持为空 — 功能捕获工具将在后续步骤中添加。同时,请勿将
body.md
重写为严格契约,这是单独的后续步骤。

File templates

文件模板

Templates use these placeholders that you substitute per project:
  • <PROJECT_NAME>
    — slug from the rough draft title (
    # Rough draft — <X>
    → snake/kebab-case of X). Fallback:
    vapi-voice-agents
    .
  • <SCENARIO_ID>
    — snake_case scenario id (e.g.
    qualification
    )
  • <SCENARIO_NAME>
    — human-readable name (e.g.
    Lead Qualification
    )
  • <FIRST_MESSAGE_EN>
    — verbatim opening line from the rough draft
  • <FIRST_MESSAGE_ES>
    — natural Spanish translation
  • <BODY_DRAFT>
    — distilled rough first-draft prompt
  • <SCENARIO_IMPORTS>
    — one
    import { <id> } from "./<id>.ts";
    per scenario, alphabetized
  • <SCENARIO_KEYS>
    — comma-separated scenario ids inside
    SCENARIOS = { ... }
  • <ENV_ASSISTANT_SLOTS>
    — one commented line per
    (scenario × language)
    :
    # VAPI_ASSISTANT_<SCENARIO>_<LANG>=
模板使用以下占位符,需根据项目替换:
  • <PROJECT_NAME>
    — 从草稿标题中提取的短名称(
    # Rough draft — <X>
    → X的蛇形/短横线命名法)。默认值:
    vapi-voice-agents
  • <SCENARIO_ID>
    — 蛇形命名法的场景ID(例如:
    qualification
  • <SCENARIO_NAME>
    — 易读的场景名称(例如:
    Lead Qualification
  • <FIRST_MESSAGE_EN>
    — 从草稿中提取的原始英文初始语句
  • <FIRST_MESSAGE_ES>
    — 自然的西班牙语翻译
  • <BODY_DRAFT>
    — 提炼后的初稿提示内容
  • <SCENARIO_IMPORTS>
    — 每个场景对应一行
    import { <id> } from "./<id>.ts";
    ,按字母顺序排列
  • <SCENARIO_KEYS>
    — 逗号分隔的场景ID,放在
    SCENARIOS = { ... }
    内部
  • <ENV_ASSISTANT_SLOTS>
    — 每个
    (场景 × 语言)
    对应一行注释:
    # VAPI_ASSISTANT_<SCENARIO>_<LANG>=

package.json
(only if absent)

package.json
(仅当不存在时创建)

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"
}
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
(only if absent)

tsconfig.json
(仅当不存在时创建)

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "types": ["bun"]
  },
  "include": ["src/**/*.ts"]
}
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "types": ["bun"]
  },
  "include": ["src/**/*.ts"]
}

.env.example
(create if absent; extend if present)

.env.example
(不存在则创建,已存在则扩展)

dotenv
undefined
dotenv
undefined
VAPI_API_KEY=
VAPI_API_KEY=

One slot per (scenario × language). First
bun run bootstrap
prints the

每个(场景 × 语言)对应一个配置项。首次运行
bun run bootstrap
会打印ID;

ids; paste them here, then re-run for idempotent updates.

将ID粘贴到此处,然后重新运行以实现幂等更新。

<ENV_ASSISTANT_SLOTS>
undefined
<ENV_ASSISTANT_SLOTS>
undefined

.gitignore
(only if absent)

.gitignore
(仅当不存在时创建)

gitignore
undefined
gitignore
undefined

dependencies

依赖

node_modules
node_modules

env (local-only secrets)

环境变量(本地私密文件)

.env*.local
.env*.local

os junk

系统临时文件

.DS_Store
.DS_Store

bun

Bun相关

*.tsbuildinfo
undefined
*.tsbuildinfo
undefined

src/assistants/languages.ts

src/assistants/languages.ts

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];
ts
/**
 * 各语言对应的语音+转录器栈。添加第三种语言只需在下方每个对象中新增一项。
 */
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

src/assistants/loadPrompt.ts

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}`;
};
ts
/**
 * 英文直接返回body.md内容。西班牙语会在body.md前添加西班牙语前置提示,并将场景的话题重定向语句插入{{OFF_TOPIC_LINES}} * 每个场景的body.md驱动所有语言变体。
 */
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

src/assistants/scenarios/index.ts

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];
ts
/**
 * 场景注册表。添加新场景只需在此处新增一项,同时在`./<scenario-id>.ts`下创建新文件,并在`../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

src/assistants/buildAssistant.ts

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,
    },
  };
};
ts
/**
 * 为(场景, 语言)元组生成完整的Vapi助手配置。
 * 语音+转录器来自languages.ts;提示来自loadPrompt;
 * 名称+初始消息+clientTools来自场景配置。
 */
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

src/assistants/scenarios/<SCENARIO_ID>.ts

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;
If
<FIRST_MESSAGE_EN>
is long, break it across concatenated string segments for readability (one logical clause per line, joined with
" + "
).
ts
/**
 * <SCENARIO_NAME>场景。纯数据:ID、名称、多语言初始消息。
 * 后续步骤会添加`clientTools`(通话中触发的捕获工具)。
 */
// 将`scenarioId`重命名为生成的<SCENARIO_ID>标识符。
export const scenarioId = {
  id: "<SCENARIO_ID>" as const,
  name: "<SCENARIO_NAME>",
  firstMessage: {
    en: "<FIRST_MESSAGE_EN>",
    es: "<FIRST_MESSAGE_ES>",
  },
  clientTools: [] as const,
};

// 将此类型重命名为生成的<帕斯卡命名法场景ID>Scenario。
export type ScenarioIdScenario = typeof scenarioId;
<FIRST_MESSAGE_EN>
较长,可拆分为多个字符串片段以提高可读性(每个逻辑分句一行,用
" + "
连接)。

src/assistants/prompts/<SCENARIO_ID>/body.md

src/assistants/prompts/<SCENARIO_ID>/body.md

Rough first draft — persona-driven, not a contract. Suggested shape:
md
undefined
初稿内容 — 以角色为导向,而非严格契约。建议格式:
md
undefined

<SCENARIO_NAME> voice agent

<SCENARIO_NAME>语音代理

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.

No `## Absolute rules`, no failure rules, no scripted off-ramps. Those are a later step.
你是Vapi的<SCENARIO_NAME>语音代理。<一句话说明上下文:对话对象和目的。>
<一段提炼自草稿中What happens的内容:需要询问的问题、收集的数据、路由逻辑、结束方式。>
语气热情且好奇。一次只问一个问题。若用户偏离话题,简要引导后回到下一个待收集的信息点。回复需简短 — 你是在说话,而非打字。

暂不包含`## 绝对规则`、失败规则或脚本化退出流程,这些将在后续步骤中添加。

src/assistants/prompts/<SCENARIO_ID>/off-topic-es.md

src/assistants/prompts/<SCENARIO_ID>/off-topic-es.md

Two short Spanish redirect lines specific to this scenario:
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>."
两条针对该场景的简短西班牙语重定向语句:
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

src/assistants/prompts/shared/preambles/es.md

md
undefined
md
undefined

IDIOMA / LANGUAGE OVERRIDE

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}}

undefined
若访问者偏离话题,选择以下语句之一,然后回到下一个待收集的信息点:
{{OFF_TOPIC_LINES}}

undefined

src/bootstrap.ts

src/bootstrap.ts

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);
});
ts
/**
 * 针对(场景 × 语言)的幂等更新/插入操作。每个元组对应一个条目,键为VAPI_ASSISTANT_<SCENARIO>_<LANG>。
 * 首次运行会创建并打印ID;后续运行会原地更新。
 *
 * 使用`bun run bootstrap`运行。Bun会自动加载.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(`✗ 缺失环境变量:${name}。请查看.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(`✓ 更新${label}${existingId}`);
          updated = true;
        } catch (err) {
          const statusCode = (err as { statusCode?: number })?.statusCode;
          if (statusCode === 404) {
            console.log(
              `  ${envVar}=${existingId}在当前组织中不存在;将创建新助手。`,
            );
          } else {
            throw err;
          }
        }
      }

      if (!updated) {
        const assistant = await vapi.assistants.create(body);
        console.log(`✓ 创建${label}${assistant.id}`);
        created.push({ envVar, id: assistant.id });
      }
    }
  }

  if (created.length > 0) {
    console.log("\n请将以下内容添加到.env.local:");
    for (const { envVar, id } of created) {
      console.log(`  ${envVar}=${id}`);
    }
    console.log(
      "\n然后重新运行`bun run bootstrap`以确认幂等更新。",
    );
  } else {
    console.log(
      `\n所有${SCENARIO_IDS.length * LANGUAGES.length}个助手已原地更新。`,
    );
  }
};

main().catch((err) => {
  console.error("✗ 启动失败:", err);
  process.exit(1);
});

ROUGH_DRAFT.md template

ROUGH_DRAFT.md模板

If the user has no rough draft yet, offer this skeleton (they fill in the angle-brackets and the skill scaffolds the project from it):
md
undefined
若用户尚无草稿,提供以下框架(用户填写尖括号中的内容后,技能即可搭建项目):
md
undefined

Rough draft — <project name>

Rough draft — <project name>

1. <First scenario 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.>
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>

2. <Second scenario name>

Opening: "<one verbatim opening line>"
What happens: <One paragraph.>

One scenario is fine. Three is fine too. The skill scales the bootstrap loop to whatever's in the rough draft.
Opening: "<one verbatim opening line>"
What happens: <One paragraph.>

单个场景或三个场景均可。技能会根据草稿中的场景数量扩展启动循环。

Adapting the defaults

修改默认配置

If the user asks for different defaults, change only the matching slot — never the surrounding shape:
  • Different languages: add an entry to
    VOICE_BY_LANGUAGE
    and
    TRANSCRIBER_BY_LANGUAGE
    , extend the
    LanguageId
    union in
    loadPrompt.ts
    , add a
    prompts/shared/preambles/<lang>.md
    , add a
    firstMessage.<lang>
    per scenario, add the lang to
    LANGUAGES
    in
    bootstrap.ts
    , and add the matching
    VAPI_ASSISTANT_<SCENARIO>_<LANG>
    slots to
    .env.example
    .
  • Different model / temperature: change
    buildAssistant.ts
    only.
  • Different voice provider: update
    VoiceConfig
    +
    VOICE_BY_LANGUAGE
    in
    languages.ts
    only.
若用户要求修改默认配置,仅修改对应的配置项 — 绝不改变整体结构:
  • 添加新语言:在
    VOICE_BY_LANGUAGE
    TRANSCRIBER_BY_LANGUAGE
    中新增条目,扩展
    loadPrompt.ts
    中的
    LanguageId
    联合类型,添加
    prompts/shared/preambles/<lang>.md
    ,为每个场景添加
    firstMessage.<lang>
    ,在
    bootstrap.ts
    LANGUAGES
    数组中添加该语言,并在
    .env.example
    中添加对应的
    VAPI_ASSISTANT_<SCENARIO>_<LANG>
    配置项。
  • 修改模型/温度值:仅修改
    buildAssistant.ts
  • 修改语音提供商:仅更新
    languages.ts
    中的
    VoiceConfig
    VOICE_BY_LANGUAGE

Verification

验证步骤

After generating files, tell the user to run:
bash
bun 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)
生成文件后,告知用户运行以下命令:
bash
bun install
bun run typecheck              # 本地验证;不会在Vapi中创建任何内容
cp .env.example .env.local     # 从dashboard.vapi.ai/org/api-keys添加VAPI_API_KEY
bun run bootstrap              # 为每个(场景 × 语言)创建一个助手

paste the printed VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id> lines into .env.local

将打印的VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id>行粘贴到.env.local

bun run bootstrap # second run prints "Updated ..." for every tuple

For N scenarios × 2 languages they should see N × 2 assistants in [dashboard.vapi.ai](https://dashboard.vapi.ai). Each one has a green "Talk to assistant" button — the agents will walk the flow but are still rough (no failure rules, no capture tools yet). That's expected; later prompts harden them.
bun run bootstrap # 第二次运行会为每个元组打印"Updated ..."

对于N个场景 × 2种语言,用户应在[dashboard.vapi.ai](https://dashboard.vapi.ai)中看到N×2个助手。每个助手都有绿色的"Talk to assistant"按钮 — 代理会执行流程,但仍为初稿(尚无失败规则和捕获工具)。这是预期情况;后续步骤会优化提示内容。