Voice Clients — Master Config
Per-tenant YAML configuration files for all WiseAI voice + chat agents. This directory is the git-versioned single source of truth for each customer's agent configuration.
Directory structure
knowledge/voice-clients/
_schema.yaml ← JSON Schema draft-07 for all per-tenant YAMLs
_defaults/
church.yaml ← Church vertical defaults (floor values)
funeral.yaml ← Funeral vertical defaults
vet.yaml ← (future) Vet vertical defaults
church/
medhanialem.yaml ← Medhanialem Ethiopian Evangelical Church (real customer)
melvindale-cog.yaml ← Melvindale Church of God (real customer)
hope-community.yaml ← Hope Community Church
churchwiseai-demo.yaml ← Demo church (UUID 00000000-...)
funeral/
walker-mortuary-daughenb-e6c1.yaml ← Walker Mortuary & Daughenbaugh (founder demo)
Cascade order
Values are merged in this order (higher overrides lower):
1. Built-in code defaults (TypeScript: defaultOperationalHandoff / defaultSafetyConfig)
2. _defaults/<vertical>.yaml ← vertical floor values
3. <vertical>/<slug>.yaml ← per-tenant overrides (this directory)
4. tenant_voice_agents DB row ← live tweaks via UI (highest precedence)
Lean YAML principle: only store fields that DIVERGE from _defaults/<vertical>.yaml.
A YAML file with only tenant_id, vertical, and display_name is valid — all other
fields inherit from the vertical default.
Secrets never go in YAML: cal_api_key, pco_secret, Stripe keys, etc. stay in
the DB only. The Export to YAML button in the UI omits them. YAML carries null
placeholders for integration fields.
Sync command: pnpm sync-voice-clients
# Validate all YAMLs against _schema.yaml (CI gate — no DB writes)
pnpm sync-voice-clients --check
# Show what WOULD change if pushed (read-only)
pnpm sync-voice-clients --dry-run
# Push all YAMLs to tenant_voice_agents (upsert)
pnpm sync-voice-clients
# Push only funeral vertical
pnpm sync-voice-clients --vertical=funeral
# Push one specific tenant
pnpm sync-voice-clients --tenant=melvindale-cog
# Pull a DB row back to YAML (overwrite file)
pnpm sync-voice-clients --tenant=melvindale-cog --pull
The script reads SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from .env.local.
It cascades _defaults/<vertical>.yaml → <vertical>/<slug>.yaml before validating
and before computing the DB diff.
Dry-run after seeding should show zero diffs. If diffs appear, the seed has bugs.
Master Config UI
Founder-only UI at /founder/[token]/voice-clients:
- List view: all tenants sorted by status (active first) → vertical → slug. Each row shows transfer_enabled badge, notification_phone, last_call_at.
- Edit view:
/founder/[token]/voice-clients/[id]/edit— grouped form for all editable fields. Fields unset by the tenant show an "inherited from default" badge. On save, writes totenant_voice_agents. - Export to YAML: downloads the per-tenant YAML (DB state → YAML format)
for the founder to commit to git. This is the
--pullmode equivalent. - Reset to defaults: clears per-tenant overrides (nullifies non-identity columns).
Inbound phone numbers are read-only in the edit view. They're provisioned via
POST /api/admin/provision-number (Telnyx auto-buy + LiveKit trunk/dispatch setup).
How to provision a new tenant
Step 1 — Buy a phone number (Telnyx auto-provision)
curl -X POST https://churchwiseai.com/api/admin/provision-number \
-H "Content-Type: application/json" \
-d '{
"token": "$FOUNDER_TOKEN",
"churchId": "<church_uuid_from_premium_churches>",
"areaCode": "416",
"countryCode": "CA"
}'
This creates a LiveKit SIP trunk + dispatch rule and updates the tenant_voice_agents
row with the new phone number, trunk ID, and dispatch rule ID.
Step 2 — Create the YAML file
# knowledge/voice-clients/<vertical>/<slug>.yaml
# Source row last updated: 2026-04-28T00:00:00Z
tenant_id: <uuid> # from tenant_voice_agents.tenant_id
vertical: church # or funeral, vet, etc.
display_name: My Church
notification_email: office@mychurch.org
notification_phone: "+1416XXXXXXX"
# Only include fields that DIVERGE from _defaults/church.yaml
operational_handoff:
response_time_promise_text: "within 30 minutes"
inbound_numbers:
- "+1416XXXXXXX"
status: active
Step 3 — Push to DB
pnpm sync-voice-clients --tenant=<slug> --dry-run # verify diff looks right
pnpm sync-voice-clients --tenant=<slug> # push
Step 4 — Set operational overrides via UI
Visit /founder/[token]/voice-clients/<tenant_id>/edit to set:
transfer_target_number(director's direct line)transfer_enabled: true(when the tenant is ready for live transfer)escalation_contact_name(e.g. "Charlie, the on-call director")
Export to YAML when satisfied → commit the diff to git.
Two-track escalation rules
| Track | Type | Tool | Routes to | DB table |
|---|---|---|---|---|
| A | Operational handoff | transfer_to_director / request_callback | notification_phone + notification_email | voice_callback_requests |
| B | Safety/Crisis | flag_safety_event | 988/911/DV (spoken) + support@churchwiseai.com + optional crisis_contact_phone SMS | crisis_events |
Crisis events NEVER live-bridge a human. crisis_contact_phone receives
a post-event SMS only — it is intentionally different from notification_phone.
Most tenants leave crisis_contact_phone: null.
Schema parity contract
The YAML schema (_schema.yaml) is the shared contract between:
src/lib/tenant-config.ts— TypeScript web runtime (chatbot / admin UI)voice-agent-livekit/core/tenant_config.py— Python voice runtime (Pydantic V2)scripts/sync-voice-clients.ts— YAML ↔ DB sync tool
When you add a new field:
- Add to
_schema.yaml - Add to both
tenant-config.ts+tenant_config.py - Add a DB migration for the new column in
tenant_voice_agents - Update
_defaults/<vertical>.yamlwith the default value
Adding a new vertical
- Add the vertical key to
_schema.yaml→vertical.enum - Create
_defaults/<vertical>.yamlwith sensible defaults - Create
src/lib/verticals/<vertical>.ts(VerticalProfile) - Create
voice-agent-livekit/verticals/<vertical>/prompts.py+tools.py - Add
<vertical>/directory here and seed the first tenant YAML - Run
pnpm sync-voice-clients --vertical=<vertical>to push
See knowledge/runbooks/voice-vertical-template.md for the full walkthrough.