Appearance
Avoca Engineering Onboarding
Captured from app.avoca.ai/admin/engineering-onboarding on 2026-04-30. Internal Avoca reference — do not redistribute.
Index
- 1. Welcome to Avoca
- 2. Architecture Overview
- 3. Codebase Organization
- 4. Auth & Permissions
- 5. Dashboard API-First Migration
- 6. Database Overview
- 7. Inngest & Background Jobs
- 8. Prompt & Assistant System
- 9. Responder — Inbound AI Calls
- 10. Outbound Campaigns
- 11. Texting & Messaging
- 12. Dispatching & Capacity
- 13. Coach & Oversight
- 14. Avo — AI Chat Assistant
- 15. Simple Scheduler
- 16. Leads & Speed-to-Lead
- 17. Capacity & Availability
- 18. CRMs & ServiceTitan
- 19. Voice AI & VAPI — Deep Dive
- 20. Hands-On Exercise
Section 1: Welcome to Avoca
Avoca is an AI-powered voice assistant platform for the home services industry. We help HVAC, plumbing, electrical, and other service companies handle phone calls, book appointments, and manage customer interactions using AI.
Products Responder AI answers inbound calls, books appointments, handles emergencies Outbound Campaigns Automated voice & text outreach to leads and customers Texting SMS, Google LSA, and Yelp message handling Dispatching Smart technician assignment based on skills, location, availability Coach & Oversight AI-powered call quality analysis and agent evaluation Avo Text-based AI assistant for dashboard users Simple Scheduler Embeddable booking widget for company websites Leads Multi-source lead ingestion and speed-to-lead tracking Search Optimization Website traffic tracking, SEO analytics, and search performance monitoring Key integrations VAPI Voice AI infrastructure ServiceTitan Primary CRM — booking, dispatch, customer data Twilio Phone numbers & SMS Supabase Database & auth Inngest Background jobs & workflows Anthropic / OpenAI LLMs for AI features ElevenLabs Voice synthesis & pronunciation dictionaries Statsig Feature flags & A/B experiments Mapbox Geocoding, drive time, address resolution Hamming Automated test call infrastructure
Additional CRMs: Jobber, Salesforce, FieldRoutes, PestPac, HouseCall Pro, Workiz, AccuLynx, JobNimbus, Clypboard, ServPro, IAA
Section 2: Architecture Overview
Avoca is a monorepo with multiple apps and shared packages. All business logic lives in apps/web — the other apps are either thin clients or specialized runtimes.
TL;DR for engineers
Avoca is a voice AI platform that answers phone calls for home service companies. When a customer calls, the call goes through Twilio → VAPI (voice AI) → our tool endpoints (for booking, lookups, etc.) → back to the AI. After the call, a webhook fires and we run async jobs for analysis, alerts, and CRM sync.
Everything is a Next.js monorepo with 8+ apps. apps/web is the brain — it owns all business logic, APIs, webhooks, and external integrations. apps/dashboard is primarily a thin UI that proxies most requests to web, though some features (fees, transfer destinations) have direct DB access. apps/avo is the AI chat runtime that calls web for business operations via a service token, but also has direct DB access for some queries. All shared types live in packages/.
The database is Supabase (Postgres). Background jobs run on Inngest. Feature flags and A/B experiments are on Statsig. Deploys go to Vercel.
Avoca integrates with multiple CRMs. ServiceTitan is the primary and most deeply integrated (booking, dispatch, customer data, sync). Other supported CRMs include Jobber, Salesforce, FieldRoutes, PestPac, HouseCall Pro, Workiz, AccuLynx, JobNimbus, Clypboard, ServPro, and IAA. Each has its own workflow module under a shared non-ST workflow framework.
System Architecture
Pan and zoom to explore. Amber = live data flow, blue = internal, gray = external, green = async, purple = data storage.
inbound call forwards tool-calls end-of-call-report process business logic async jobs save call record proxy tool-call-gateway deploy prompts phone/SMS CRM sync DB access cron/events LLM calls Customer Call apps/web :3000 — Business authority apps/dashboard :3002 — Customer UI apps/avo :3004 — AI runtime enterprise-api :3001 — External API Tool Endpoints /api/vapi/tools/* Webhook Handler /api/responder/*/webhook Post-Call Processing end-of-call-report VAPI Voice AI platform Twilio Phone & SMS ServiceTitan CRM Supabase DB & Auth Inngest Background Jobs Anthropic / OpenAI LLMs Stripe Payments Statsig Feature Flags React Flow How it works
The amber path is the inbound call flow — the core of Avoca. A customer calls a Twilio phone number, which forwards to VAPI (the voice AI platform). VAPI runs the conversation and invokes tool endpoints in apps/web for business operations (booking, customer lookup, etc.). When the call ends, VAPI sends an end-of-call-report webhook back to web, which triggers post-call processing — saving the call record to Supabase and kicking off async Inngest jobs (call analysis, email alerts, CRM sync).
Blue connections are internal app communication — the dashboard proxies most requests to apps/web, though some features (fees, transfer destinations) have direct DB access. Apps/avo calls web through a tool-call-gateway using a shared service token for authentication.
Gray connections are external service integrations — apps/web is the single boundary that talks to ServiceTitan (CRM), Twilio (phone/SMS), VAPI (assistant management and prompt deployment), Supabase (database), Inngest (background jobs), Stripe (payments), and Statsig (feature flags).
Purple is the LLM connection — apps/avo calls Anthropic (Claude) and OpenAI directly for AI orchestration, but all business data comes from web via the gateway.
Apps apps/web (:3000) Canonical business authority — API server, webhooks, all business logic apps/dashboard (:3002) Customer-facing UI — proxies most requests to web (some direct DB access) apps/avo (:3004) AI runtime — prompt orchestration, model routing, tool execution enterprise-api (:3001) External API for enterprise customers and integrations apps/thumbtack (:3006) Thumbtack marketplace integration — standalone Next.js with Claude, Twilio, ElevenLabs apps/rwg Reserve with Google — Express server for feed generation and SFTP upload (not Next.js) apps/storybook Storybook 10 — visual testing for @avoca/ui components Extensions apps/internal-extension (Avoca team) + apps/customer-extension (Chrome MV3 side-panel) Shared Packages api-contracts workflows tool-calls leads avoca-ui avoca-fonts
Shared types, schemas, constants, and UI components used across apps. api-contracts is the most critical — API surface types, permissions, feature flags. avoca-ui is a shadcn/ui-based component library with Tailwind config export.
Key rule: apps/web is the canonical server authority for business logic. Dashboard proxies most requests to web (some features have direct DB access as exceptions). Avo calls web for business tool execution via a service token, but also queries the DB directly for some read operations.
Section 3: Codebase Organization
How to navigate the monorepo. Knowing where things live will save you hours of searching.
apps/web — the brain
This is where 90% of your work will be. Here's how it's organized:
app/api/** Canonical API routes (App Router). New APIs go here. Team-scoped routes follow app/api/team/[teamId]/... pattern. pages/api/** Legacy API routes (Pages Router). VAPI tool-call handlers and older endpoints. Still active — do not migrate without a plan. app/(restricted)/(protected)/team/[slug]/** Team-scoped UI pages. Each product has its own sub-route (outbound/, settings/, oversight/, etc.). app/(restricted)/(protected)/admin/** Avoca-internal admin pages (team management, feature flags, this onboarding page). components/** React components organized by feature. components/ui/ has shared primitives, feature folders have their own components. lib/** Business logic, utilities, and integrations — the most important directory. lib/supabase/** Database helpers organized by table (e.g. lib/supabase/calls.ts, lib/supabase/skills.ts). Add new DB queries here. lib/inngest/** Background job definitions. All Inngest functions live here. lib/service-titan/** ServiceTitan API client, sync logic, and generated API docs. lib/vapi/** VAPI integration — the withVapiToolCall wrapper, result helpers, tool handlers. lib/avoca-cap/** Avoca Capacity Platform (ACP) — the capacity computation engine. lib/client/** Client-side API wrappers for browser→server calls (fetch-based, no server actions). docs/** Architecture docs, migration plans, feature specs. Read these before major changes. pages/api vs app/api — the legacy split app/api/* (canonical) App Router routes. New work goes here. Uses export async function GET/POST() pattern. Team-scoped auth via verifyTeamAccess(). pages/api/* (legacy) Pages Router routes. VAPI tool-call handlers (pages/api/vapi/tools/) and older endpoints. Uses handler(req, res) pattern.
Both are active and deployed. When modifying an existing endpoint, use whichever router it's already on. For new endpoints, default to app/api/.
Most-connected files (from codebase graph)
These files are imported by the most other files — touching them has the widest blast radius:
actions.ts Server actions (1600+ dependents) page.tsx Page components (1300+ dependents) utils.ts Shared utilities (1200+ dependents) route.ts API route handlers (800+ dependents) supabase.ts Database client (560+ dependents) api-client.ts API clients (220+ dependents) Key convention: Each AGENTS.md file (in the repo root and in each app) describes the coding standards and architectural rules for that area. Read the relevant AGENTS.md before making changes — it's enforced by all AI coding tools.
Section 4: Auth & Permissions
Every team-scoped API route needs authorization. Avoca has a single canonical auth module — learn it once, use it everywhere.
verifyTeamAccess — the one auth function
verifyTeamAccess(request, teamId) from lib/api/team-access.ts
This is the only way to authorize team-scoped API routes. Do not create alternatives — the team consolidated four separate auth modules into this one after a security gap caused by fragmentation.
What it handles internally:
✓ Session lookup (who is the user?) ✓ Avoca employee detection ✓ Apex Brands whitelist enforcement ✓ Team membership check ✓ Permission resolution (for UI gating) ✓ Role determination Return type: TeamApiAccessResult authorized isEmployee user role permissions error status Member Permissions (RBAC)
Permissions are defined in packages/api-contracts using PermissionKeySchema. Each team member has a role with associated permissions.
Admin Full access — can manage team settings, members, and all features Manager Can view all data and manage most settings, limited team admin Member Standard access — scoped by specific permission grants Important: Backend permission enforcement (blocking requests per permission) was intentionally removed. Permissions are resolved for UI gating only, surfaced to the dashboard via the /context endpoint. Do not add per-route permission checks. Auth flows Email code sign-in Default: send OTP code → verify → session Email/password Via ?auth=password on /signin, with forgot-password flow Google OAuth Via WorkOS, landing in /auth/callback WorkOS SSO Enterprise SSO when team redirect enables it Impersonation Avoca employees can impersonate team members for support Service token Avo→Web communication uses AVO_SERVICE_TOKEN (not user sessions)
Section 5: Dashboard API-First Migration
The dashboard is being migrated from direct Supabase access to proxying all data through canonical apps/web API routes. This is a major ongoing initiative — you will encounter migration-era patterns daily.
Why this migration exists
Originally, apps/dashboard had direct Supabase access and its own query logic. This caused: duplicated business logic across web and dashboard, inconsistent auth enforcement, and difficulty maintaining a single source of truth. The migration makes web the single canonical server for all business logic.
Two-stage approach Stage 1 — Web API Canonicalization Build canonical REST routes in apps/web/app/api/ for each feature slice. Define shared request/response types in packages/api-contracts. This stage doesn't change the dashboard — it just creates the API surface that dashboard will later consume. Stage 2 — Dashboard Client Adoption Dashboard creates proxy routes (apps/dashboard/app/api/) that forward to web's canonical routes. Dashboard UI switches from direct Supabase queries to calling the proxy. Eventually, direct DB access is removed. Patterns you'll encounter Proxy routes Dashboard API routes that just forward to web — transport-only, no business logic Temporary seams Places where old and new patterns coexist. Tracked in docs/plans/dashboard-migration-temporary-seams.md Dual write paths Some features still have both direct DB and proxy paths during transition api-contracts types Shared DTOs in packages/api-contracts/src/*.ts — the single source of truth for request/response shapes Rules for new work: When building new features, always follow the migrated pattern — build the canonical API in apps/web first, then have dashboard consume it via proxy. Never add new direct Supabase access to the dashboard for migrated slices. Read docs/plans/dashboard-api-first-migration.md for the full plan.
Section 6: Database Overview
Avoca uses Supabase (Postgres). Key tables are spread across the public schema (Avoca data) and the crm_service_titan schema (synced CRM data).
Critical tables (public schema) calls Every AI call record — transcript, outcome, tool calls, analysis. The core table. Never use call_conversations. responder_webhook_configs Per-team responder settings — avoca_capacity_enabled, transfer rules, booking mode, feature flags. One row per team. system_prompt_configs Knowledge Base / prompt configs. Each team has one or more configs defining the AI assistant behavior. capacity_configs ACP settings per team — use_skills, use_business_units, tech filtering flags. workflow_campaigns Outbound campaign definitions — audience, schedule, type, status. assistant_variables / assistant_variables_versions Prompt template variables with optimistic locking versioning. audience_filters_v3 / _groups / _resource_filters Normalized audience filter definitions (3-table save format). insights_call_analyses AI-generated call analysis — intent, fulfillment, rubric scores. team_alert_configurations Configure Alerts rules — which alerts fire for which service areas. skills / tech_skills_links Technician skill definitions and assignments (synced via Simplex). crm_service_titan schema (synced data)
22 entity tables synced from ServiceTitan via the unified sync orchestrator. This data powers analytics, audience filters, and post-call processing — it is not used for live capacity/booking (which fetches from ST API directly).
customers locations jobs appointments bookings calls invoices estimates memberships membership_types technicians business_units job_types tag_types campaigns installed_equipment Database conventions Use Supabase SDK over raw SQL Prefer createServiceClient() and helpers in lib/supabase/. Raw SQL only for complex joins, CTEs, aggregations. Add helpers to lib/supabase/<table>.ts e.g. lib/supabase/calls.ts, lib/supabase/skills.ts. One file per table family. Types come from @global.d.ts Database row types (insert, update, etc.) are auto-generated. Import from there. Use DateTime (Luxon), not Date All date/time operations use Luxon DateTime. Never use new Date() or Date.now().
Section 7: Inngest & Background Jobs
Inngest is Avoca's background job system. Almost every async operation runs through it — call analysis, CRM sync, campaign execution, alert delivery, daily reports.
How Inngest works
Inngest is event-driven. You send an event (e.g. "app/call.ended"), and Inngest triggers all functions subscribed to that event. Functions can have steps (retryable units of work), delays, fan-out, and concurrency controls.
Functions are defined in lib/inngest/functions/ and registered in lib/inngest/client.ts.
Key Inngest functions Call Analysis Pipeline app/call.ended Runs LLM analysis on every completed call — extracts intent, scores rubrics, generates insights. ST Sync Orchestrator Cron: every minute Dispatches sync events for all enabled ST entity configs. Runs 22 entity types across all teams. ST Sync Unified app/st-sync.entity Syncs a single entity type for a single team. Cursor-based pagination. 180 global / 4 per-team concurrency. Campaign Driver app/campaign.activated Processes each contact in an outbound campaign — initiates voice calls or texts. Alert Delivery app/call.ended Routes email/Slack alerts based on call outcome and configured alert rules. Daily Reports Cron: daily Generates daily transcript analysis and performance reports per team. Skills Sync Cron: daily at midnight ET Simplex RPA-based sync of tech skills from ServiceTitan web UI. Writing an Inngest function
Pattern:
- Define the function in lib/inngest/functions/your-function.ts
- Use inngest.createFunction() with event trigger and optional concurrency/rate limits
- Break work into step.run() calls — each step is independently retryable
- Register in lib/inngest/client.ts
- Send events via await inngest.send() Concurrency matters: The ST sync orchestrator runs at 180 global / 4 per-team concurrency. Campaign drivers also have concurrency controls. Always set appropriate limits to avoid overwhelming external APIs or the database.
Section 8: Prompt & Assistant System
How AI assistant prompts are built, versioned, and deployed to VAPI. This is the system that turns a Knowledge Base configuration into a live voice agent.
The pipeline Knowledge Base (UI) → Assistant Variables (DB) → Liquid Template → Compiled Prompt → VAPI Deploy Assistant Variables & Versioning
The Knowledge Base UI saves structured data into the assistant_variables table. Each save creates a version in assistant_variables_versions with optimistic locking (prevents concurrent edits from overwriting each other).
assistant_variables Current state — company info, job types, BU mappings, booking windows, FAQ, etc. assistant_variables_versions Version history with optimistic locking — every save creates a new version assistant_config_versions Voice assistant configs — model, voice, tools, linked to a specific prompt version prompt-variables.ts Template compiler — reads variables, applies Liquid templating, outputs final prompt Voice Assistant hierarchy
Each team can have multiple voice assistants:
Team → System Prompt Config → Voice Assistant(s) → Assistant Config (version) → VAPI Assistant
A team has a Knowledge Base (system_prompt_configs) which generates the prompt. Each voice assistant is linked to an assistant_config that specifies model, voice provider, and tools. Deploying compiles the prompt and pushes it to the VAPI assistant ID.
Audience Filters V3
Outbound campaigns target audiences using V3 filters — a complex system that queries ServiceTitan synced data.
3-table normalized save format:
audience_filters_v3 (definition) audience_filters_v3_groups (OR groups) audience_filters_v3_resource_filters (AND conditions)
Unsaved edits compile directly from in-memory nested definitions rather than writing temporary rows. The preview count uses service_area_zip_rules_mv for service-area matching.
Workflow Engine
Outbound campaigns are powered by a visual workflow engine — a directed graph of nodes (actions) and edges (conditions).
Nodes Actions: voice call, send text, delay, check condition, split A/B Edges Transitions: on success, on failure, on no answer, on timeout Presets Template workflows: maintenance, reschedule, speed-to-lead, dropped responder Execution Inngest-driven: campaign activated → driver processes each contact through the graph
The visual editor lives in components/outbound-workflows/. Workflow CRUD API is at app/api/workflows/*.
Section 9: Responder — Inbound AI Calls
The core product. AI agents answer inbound customer calls, book appointments, handle emergencies, and transfer to humans when needed.
inbound forwards tool-calls booking/lookup end-of-call-report parse transcript check rules book if eligible save call trigger async send summary analyze call save analysis Customer Call Inbound Twilio Phone number VAPI Voice AI Tool Endpoints /api/vapi/tools/* Responder Webhook /api/responder/*/webhook Data Extraction LLM extract customer info Bookability Check Rules + tag exclusions ServiceTitan Book job / lookup calls table Supabase Post-Call Jobs Inngest async Email Summary Resend Call Analysis LLM scoring React Flow How a call works
- Customer calls a Twilio number → forwarded to VAPI assistant
- VAPI runs the conversation using the system prompt (from Knowledge Base)
- During the call, VAPI invokes tools — booking, customer lookup, availability, fees, transfers
- Call ends → VAPI sends an end-of-call-report webhook
- Webhook extracts customer data (LLM-powered), checks bookability rules
- Books in ServiceTitan if eligible, or logs as a dropped call
- Async: call analysis (AI scoring), email summary, dropped lead creation Integrations VAPI Twilio ServiceTitan Inngest Anthropic Resend (email)
Section 10: Outbound Campaigns
Workflow-driven outbound calling and texting. Target filtered audiences from CRM data, execute calls/texts, track results.
create campaign validate & save store build audience query filters activate voice calls text campaigns log results experiment metrics Dashboard Campaign creator Campaign API /api/outbound/campaigns Campaign Service service.ts Audience Filter V3 SQL filter engine ServiceTitan Customer/job data workflow_campaigns Supabase Campaign Driver Inngest orchestrator VAPI Outbound call Twilio Outbound SMS Call Logs workflow_call_logs Statsig A/B experiments React Flow Campaign types Maintenance Reschedule Estimate Follow-Up Speed-to-Lead Dropped Responder Unsold Estimates Expiring Memberships Survey How it works
- User creates campaign → selects audience (filter, import, or ST list)
- Audience built via V3 filters against ServiceTitan data
- Campaign activated → Inngest driver processes each contact
- Voice calls via VAPI, texts via Twilio (with A/B experiment support)
- Results logged, Statsig events emitted for tracking
Section 11: Texting & Messaging
Multi-channel text conversations — SMS, Google Local Service Ads, and Yelp messaging. AI-powered responses with TCPA compliance.
webhook poll/webhook webhook upsert thread check opt-out generate reply queue send SMS LSA reply log message schedule follow-up Twilio Webhook Inbound SMS Google LSA Business Messages Yelp Yelp Messages Message Ingestion processIncomingText text_conversations Supabase AI Response Anthropic / OpenAI Outbound Handler prepareOutgoingText Twilio Send SMS Google API Send LSA reply Nudge Scheduler Follow-up timing Opt-Out Check TCPA compliance React Flow Channels SMS (Twilio) Two-way SMS via Twilio webhooks Google LSA Business Messages from local service ads Yelp Yelp conversation message handling How it works
- Inbound message via webhook (Twilio) or polling (LSA)
- Conversation thread created/updated
- Opt-out check (TCPA compliance) before responding
- AI generates response, queued for sending
- Sent via appropriate channel, follow-up nudges scheduled
Section 12: Dispatching & Capacity
Automated technician dispatching — assigns jobs based on skills, availability, drive time, and performance.
new job event trigger filter techs skills/shifts rank candidates drive time top candidate assign in CRM log decision ServiceTitan New job created ST Sync Inngest orchestrator Auto-Dispatch Eligibility + scoring Eligibility Engine Skills, shifts, zones Scoring Engine Drive time, callback, perf Mapbox Drive time routing Technicians DB Skills & availability Assign Tech ST API dispatch ServiceTitan Job assigned dispatch_generator_calls Decision log React Flow Scoring factors Drive time Mapbox routing to job site Callback bonus Same tech for return visits Skill match Tech skills vs job requirements Shift compliance Within scheduled hours Zone / territory Service area assignment Performance On-time rate, quality score Configuration modes
Each team configures shift, zone, territory, and callback modes as strict (enforce), prefer (soft weight), or ignore.
Section 13: Coach & Oversight
AI-powered call quality analysis. Every call is analyzed by an LLM for intent, fulfillment, and scored against custom rubrics.
call.completed send transcript extract intent score fulfillment evaluate rubrics load definitions save save save scores display Call Completed calls table Inngest Event analyze-call trigger LLM Analysis Anthropic / OpenAI Intent Detection What customer wanted Fulfillment Score Was it resolved? Rubric Evaluation Tag + eval scoring oversight_tags Team tag definitions insights_call_analyses Analysis results insights_rubric_analyses Rubric scores Dashboard Coach / AI Calls UI React Flow Analysis pipeline
- Call completes → Inngest triggers the analysis function
- LLM receives transcript + team's tag/eval definitions
- Extracts intent, fulfillment score, service category
- Evaluates each rubric — per-tag scores with explanations
- Results shown in Dashboard (Coach / AI Calls views) Tags & Evals
Teams define custom tags (binary: "Was fee waived?") and evals (scored rubrics: "Empathy 1-5"). Can be scoped per voice assistant or apply globally.
Section 14: Avo — AI Chat Assistant
Text-based AI assistant in the dashboard and Slack. Answers questions about team data, calls, revenue, and KPIs.
auth + team context proxy to avo prompt + stream tool call request AVO_SERVICE_TOKEN execute result tool result stream text User Message Dashboard / Slack apps/web /api/avo/chat (auth) apps/avo AI orchestration Anthropic Claude model Tool Gateway tool-call-gateway.ts apps/web /api/internal/avo/tool-call Business Logic Supabase, ST, etc. Stream Response Back to user React Flow Architecture boundary apps/avo — AI orchestration only. Owns prompts and model routing. Has some direct DB reads, but business mutations go through web. apps/web — Business boundary. All data queries and mutations go through web. Service token auth — Avo calls web via a gateway with a shared service token, not user sessions. How it works
- User sends message → web authenticates and proxies to avo
- Avo orchestrates with Claude, streams response
- Tool calls → gateway sends to web for business logic execution
- Results streamed back to user
Section 15: Simple Scheduler
Embeddable booking widget for company websites. Customers can self-service book appointments with real-time ServiceTitan availability.
visit page load config enter zip if serviceable query slots show windows select & submit create job success Customer Visits widget Booking Widget Embedded on site Widget Config simple_scheduler_configs Zip Check Service area validation Availability Engine Query ST slots ServiceTitan Open time slots Book Appointment Create ST job ServiceTitan Job created Confirmation Show to customer React Flow How it works
- Customer visits company website with embedded widget
- Enters zip → service area validated
- Selects service → availability queried from ServiceTitan
- Picks time, enters details → job created in CRM
Section 16: Leads & Speed-to-Lead
Ingest leads from multiple sources, qualify them, and track speed-to-lead (time from arrival to first contact).
poll/webhook webhook submit sync geocode save lead add to list track timing trigger outreach Google LSA Ad leads Yelp Message leads Web Form Form submissions ST Bookings Callback leads Lead Ingestion Normalize & geocode Mapbox Geocode address leads table Supabase Audience Lists Campaign assignment Speed-to-Lead Time tracking Outbound Campaign Auto-trigger React Flow Lead sources Google LSA Local service ad leads Yelp Yelp message requests Web forms Form submissions ST Bookings Callback leads from CRM sync CSV Import Manual upload Dropped Calls Unbooked Responder calls How it works
- Lead arrives → normalized and geocoded (Mapbox)
- Service area eligibility checked
- Saved and added to audience lists
- Optionally triggers outbound campaign for follow-up
Section 17: Capacity & Availability
When a customer wants to book an appointment, the AI agent needs to know which time slots are available. Avoca has 6 availability modes selected by a factory based on team config and CRM type. Two are general-purpose, and four are CRM-specific or customer-specific.
Booking types
Each team is configured with a booking_type that determines how availability is computed:
Non ACP — ServiceTitan Native Capacity Avoca calls ServiceTitan's Dispatch Capacity API directly. ST returns available slots based on its own scheduling engine (technician shifts, existing appointments, dispatch rules). Avoca presents those slots to the customer as-is. Think of it as: "Hey ServiceTitan, what slots do you have open?" → ST answers → Avoca shows them. ACP — Avoca Capacity Platform Avoca computes availability from scratch using raw data: technician shifts, existing booked appointments, arrival windows, skills, service area zones, manual capacity adjustments, and priority overbooking rules. It builds its own view of what's available rather than asking ST. Think of it as: Avoca looks at every tech's shift, subtracts their existing jobs, applies business rules, and figures out what's open — independently of ST's capacity engine. AdCap — ACP with Dispatch Same as ACP, but additionally used for technician-level dispatch assignment (not just "is there a slot?" but "which specific technician should go?"). CRM-specific & customer-specific modes
Some teams have custom availability logic. A factory at lib/availability/availability-fetcher/factory.ts selects the correct fetcher based on team ID, CRM type, and config flags.
HCP — HouseCall Pro For teams on the HouseCall Pro CRM. Fetches availability from HCP's API instead of ServiceTitan. Activated when crm = 'HOUSECALL_PRO' and the HCP client and arrival window config are provided. Call Board — Team 164 (outbound only) Custom capacity logic for a specific customer team. Uses tag-based include/exclude filtering on call board data. Only active in outbound campaign mode. Goettl — Teams [1228, 1425, 1430, 1431, 1433, 1434] Customer-specific capacity for Goettl teams. Uses zip code + job type to query ST with custom availability filtering and slot-to-unified mapping. Nevada Heating — Team [1435] Customer-specific capacity for Nevada Heating. Similar to Goettl — zip-based ST query with custom availability filters and Nevada-specific slot formatting. Factory selection order: The factory checks in this order: (1) HCP CRM with client → hcp, (2) Team 164 + outbound → call_board, (3) Nevada team list → nevada, (4) Goettl team list → goettl, (5) avoca_capacity_enabled = true → avoca (computes from scratch), (6) default → acp (ST passthrough). All fetchers implement UnifiedAvailabilityFetcher and return a common UnifiedAvailability[] format. Which flag controls this? avoca_capacity_enabled on responder_webhook_configs (per team) avoca_capacity_enabled = false → Non-ACP — calls ST Dispatch Capacity API avoca_capacity_enabled = true → ACP — Avoca computes capacity from scratch
Separately, booking_type on system_prompt_configs is the Knowledge Base label ("ACP", "Non ACP", "AdCap") shown in the admin UI. The actual runtime behavior is driven by avoca_capacity_enabled.
Important: Both ACP and Non-ACP book into ServiceTitan. ACP determines which slots to offer; ST is always the final booking destination. The job is created in ST regardless of which capacity mode computed the availability. Arrival windows (Avoca-managed)
Customers don't see exact times — they see arrival windows like "Morning 8am–12pm" or "Afternoon 12pm–5pm". These are entirely configured in Avoca (not from ST) and are the core of Avoca's booking windows system.
Morning 8:00 AM – 12:00 PM Afternoon 12:00 PM – 5:00 PM Evening 5:00 PM – 8:00 PM
How booking windows are configured (3 layers):
- Business Unit Map — maps a combination of service type + appointment type + service area + residence type → to a ServiceTitan Business Unit ID. This determines which BU a booking targets.
- Arrival Windows — each BU map has custom time windows (e.g. Morning 8–12, Afternoon 12–5). Each window has its own lead time (hours before the window closes that it's still bookable) and cutoff hours.
- Day-level config — each window + day combination can be toggled for regular and/or emergency availability, linked to a specific fee, and configured for capacity bypass or emergency auto-assign.
These are fully custom per team — teams can have completely different window configurations for different service types and areas. The admin configures them in the Prompt Builder under "Booking Windows".
Example: Non-ACP booking flow
Scenario: Customer calls to book an AC repair. Team uses Non-ACP (ServiceTitan native).
- AI determines job type = "AC Repair", business unit = "HVAC"
- Avoca calls ST Dispatch Capacity API with job type ID + BU ID + date range
- ST returns: "Tomorrow Morning has 2 open slots, Afternoon has 1 open slot"
- AI offers: "I have tomorrow morning 8–12 or afternoon 12–5, which works better?"
- Customer picks morning → AI books job in ST with that window
- ST assigns a technician based on its own dispatch rules How ServiceTitan's Native Capacity Works
When a team uses Non-ACP, Avoca calls ST's Dispatch V2 Capacity API. Here's what ST considers internally and what Avoca does with the response.
What Avoca sends to ST
jobTypeId — the resolved ST job type
businessUnitIds — the target BU from the matched BU map
startsOnOrAfter / endsOnOrBefore — date range (fetched in 12-day chunks)
skillBasedAvailability — whether ST should filter techs by skills
What ST considers internally (black box)
ST's capacity engine factors in: technician shifts, existing booked appointments, dispatch zones, BU assignments, arrival window definitions, and optionally skills (if skillBasedAvailability = true).
Avoca has no control over ST's filtering logic. If ST decides a tech is unavailable, there's no way to override it from the API.
What ST returns
Per-slot: startUtc, endUtc, isAvailable, openAvailability (number), totalAvailability (number), technicians[] (each with id, name, status: Available/Unavailable)
What Avoca Does After ST Returns Capacity
Even in Non-ACP mode, Avoca doesn't just pass ST's response straight to the bot. There are several processing steps:
1 DST Correction ST has a known bug where UTC timestamps are off by ±60 minutes on DST transition days. Avoca detects this and corrects the offsets. acp-availability-fetcher.ts:32–58 2 Basic slot filtering Removes slots where isAvailable=false AND openAvailability=0 AND totalAvailability=0. Keeps slots that have any capacity. acp-availability-fetcher.ts:108–114 3 Priority overbooking merge For P2 priority jobs, checks if lower-priority appointments exist that could be displaced. Creates synthetic "overbookable" slots and merges them with real availability. priority-overbooking.ts:26–172 4 Unified availability filters Removes past slots, applies emergency bypass logic (totalAvailability > 0 for bypass-enabled windows), and P1 overbooking (always allow if totalAvailability > 0). unified.ts:19–43 5 Arrival window mapping Maps raw ST time slots to Avoca's named arrival windows (e.g., "Morning 8–12"). Only slots that overlap with a configured Avoca arrival window are shown to the customer. helpers.ts:724–728 6 Schedule rules Applies per-window rules: lead hours (how far in advance you can book), cutoff hours, emergency-only windows, and capacity bypass flags. helpers.ts:730–775 7 Fee calculation Looks up the dispatch fee for the matched service type, appointment type, and arrival window. Attaches the fee amount to each slot. bookingWindowsAvailability.ts 8 Holiday filtering Removes slots on holidays (unless holiday booking is enabled). Applies holiday-specific fee overrides if configured. bookingWindowsAvailability.ts 9 Day limiting Limits results to TOTAL_DAYS_TO_SHOW (typically 14 days) even though more may have been fetched. bookingWindowsAvailability.ts 10 Zero-availability check If availabilityCount === 0 after all filtering, returns CALLBACK_MESSAGE ("unable to schedule") or TRANSFER_MESSAGE depending on transfer_on_no_availability config. getBookingInformationByStAtAddV2Tool.ts:93–96 Key takeaway: Even in Non-ACP mode, ST's raw capacity response goes through ~10 Avoca processing steps before reaching the bot. Arrival window mapping, fee lookup, schedule rules, emergency handling, and overbooking logic are all Avoca-side — they apply regardless of which capacity mode is used. Example: ACP booking flow
Scenario: Same customer, same AC repair. Team uses ACP (Avoca Capacity).
- AI determines job type = "AC Repair", business unit = "HVAC"
- Avoca computes capacity from scratch: — Fetch all HVAC tech shifts for the next 7 days — Subtract their already-booked appointments — Apply skill filters (only techs who can do AC Repair) — Apply zone filters (only techs serving this zip code) — Check manual capacity adjustments (admin overrides) — Check priority overbooking rules (can a P1 emergency displace a lower-priority job?)
- Result: "Tomorrow Morning: 3 open capacity (Tech A, Tech B, Tech C available). Afternoon: 1 open (Tech B only)."
- AI offers the same windows to the customer
- Customer picks morning → AI books job in ST with that window
- Avoca's dispatch engine scores Tech A, B, C by drive time, skills, and performance → assigns the best one What gets stored during booking
When availability is presented to a customer, the offered slots are saved to the fetched_availabilities table with metadata:
Arrival window ID Date Emergency flag Job type Equipment age Priority overbooking flag
When the customer confirms a slot, the system looks up the fetched_availabilities record to get the exact arrival window, emergency classification, and other metadata needed for the ST booking.
All 6 availability modes at a glance Mode Availability source Selection trigger Books into ACP (Non-ACP) ST Dispatch V2 Capacity API → Avoca post-processing avoca_capacity_enabled = false (default) ServiceTitan Avoca Computed from scratch (shifts, appts, rules, zones) avoca_capacity_enabled = true ServiceTitan HCP HouseCall Pro API crm = HOUSECALL_PRO + HCP client provided HouseCall Pro Call Board Tag-based include/exclude on call board data Team 164 + outbound mode ServiceTitan Goettl ST API with zip-code + custom filtering Teams [1228, 1425, 1430, 1431, 1433, 1434] ServiceTitan Nevada ST API with zip-code + custom filtering Team [1435] ServiceTitan
Factory: lib/availability/availability-fetcher/factory.ts. All fetchers implement UnifiedAvailabilityFetcher and return UnifiedAvailability[] — downstream code (arrival window mapping, fee calc, schedule rules) works the same regardless of mode.
Managed vs Non-Managed Technicians
These are ServiceTitan license types, not Avoca concepts. They determine what a tech can do in ST and whether they cost a license seat.
Managed Tech
Paid ST license seat (~$125–500/mo).
Can be dispatched, assigned to jobs, arrive on-site, and complete jobs independently.
Shows on the ST dispatch board with full scheduling.
Typical: Field technicians, lead techs, installers Non-Managed Tech
Free/limited profile — no license cost.
Can be assigned and dispatched, but cannot complete a job without a Managed tech also assigned.
Typical: Apprentices, helpers, office staff, salespeople Why it matters for Avoca: When filter_out_non_managed_techs = true in capacity_configs, Avoca excludes all Non-Managed techs from capacity calculations. Only Managed techs can be booked. This is checked at capacity.ts:664.
On-Call is separate — it's a shift type, not a license type. Any tech (managed or non-managed) can have an on-call shift. On-call shifts show as orange on the ST dispatch board and represent after-hours emergency availability.
Business Units & Technician Eligibility
In ServiceTitan, each technician can optionally be assigned to a Business Unit (e.g., "HVAC", "Plumbing", "Electrical"). Many techs have no BU assigned — they show as blank in ST's technicians page.
How Avoca uses the BU assignment (when use_business_units = true):
✓ Tech has matching BU (e.g., tech is in "HVAC", booking is for HVAC BU) → eligible ✗ Tech has a different BU (e.g., tech is in "Plumbing", booking is for HVAC BU) → filtered out ? Tech has no BU assigned → depends on unassigned_bu_techs_universally_eligible:true = eligible for all BUs,false = filtered out
This is common in smaller companies: most techs have no BU in ST and are kept universally eligible. Larger companies with specialized teams (dedicated HVAC crew vs plumbing crew) assign BUs to enforce routing.
computeAvocaCapacity — Data Sources
Every time the bot tries to book, this function fetches data from two sources in parallel and computes availability in-memory. Nothing is cached between calls.
From ST API (live, every booking)
shifts — technician shifts (who works when)
appointments — already-booked appointments
arrivalWindows — ST arrival window definitions
technicians — profiles with skills, BU, managed status
jobs — job details for appointments
nonJobAppts — meetings, training, time-off blocks
jobType — the target job type's required skills
assignments — which tech is on which appointment
From Avoca DB (Supabase)
capacitySettings — capacity_configs flags
capacityRulesets — rules that block/boost capacity
skills — skill definitions (for ID mapping)
groupedBuSTIds — BU group configuration
timezone — team timezone
adjustments — manual capacity overrides
serviceAreaSTZoneIds — zone-to-area links
File: apps/web/lib/avoca-cap/capacity.ts:198–406
Tech Filtering Chain (filterRelevantTechs)
All technicians from ST go through these filters sequentially. A tech must pass every filter to be counted as available capacity.
1 Exclusion list Is this specific tech explicitly excluded? excluded_tech_st_ids 2 Business Unit Does the tech belong to the target BU? (techs with no BU pass if unassigned_bu_techs_universally_eligible = true) use_business_units 3 Managed status Is the tech a Managed Tech (paid ST license)? filter_out_non_managed_techs 4 Skills Does the tech have ALL skills required by the job type? (reads t.skills from ST Technicians API) use_skills 5 Service area zones Is the tech in a zone that matches the service area? use_service_area_eligibility
After filtering, capacity is computed per arrival window per day: shift hours − booked time − non-job time × ruleset multipliers. File: capacity.ts:628–697
Capacity Rulesets
Rulesets are custom rules per team that adjust capacity. Each ruleset has an action_percent_multiplier that scales capacity:
0% Block all capacity 75% Reduce to 75% 125% Boost by 25%
Rulesets can target specific BUs, job types, techs, arrival windows, weekdays, and date ranges. Activation modes: CONSTANT (always active) or FIXED_RANGE (active within a date range).
ServiceTitan Data Sync
ST data is synced to Avoca's DB for analytics, UI display, and post-call processing. The synced data is NOT used by computeAvocaCapacity (which fetches live from ST on every booking).
Unified Sync Orchestrator
Schedule: Every minute (* * * * *)
Config table: crm_service_titan.sync_config (team_id, entity, is_enabled)
Concurrency: 180 global, 4 per team
Cursor: Auto-resumes from last successful batch using continueFromToken (Export API) or modifiedOnOrAfter (List API)
22 entities synced (all to crm_service_titan schema):
Core Jobs, Appointments, Customers, Locations, Customer Contacts, Job Histories Telecom Calls, Bookings Financial Invoices, Estimates Services Memberships, Membership Types, Recurring Services, Recurring Service Events, Recurring Service Types Config Business Units, Job Types, Tag Types, Technicians, Campaigns Assets Installed Equipment, Job Canceled Logs
Files: lib/inngest/functions/service-titan-sync-orchestrator.ts, lib/inngest/functions/service-titan-sync-unified.ts, lib/service-titan/sync/*.ts
Skills Sync (Simplex)
Skills sync is separate from the unified ST sync. It uses Simplex, an RPA/browser automation tool that logs into ServiceTitan's web UI and scrapes the skill-to-technician mapping.
Flag: skills_sync_enabled on responder_webhook_configs
Schedule: Daily at midnight ET
Writes to: skills and tech_skills_links tables
Important nuance: The tech_skills_links table is NOT used by computeAvocaCapacity. The capacity engine reads t.skills directly from the ST Technicians API response (which now includes skills natively). The tech_skills_links table powers the Skills management UI in the dashboard.
Files: lib/skills/TechSkillsSyncCronInngestFunction.ts, lib/skills/st-sync.ts, lib/simplex/client.ts
Config Flags Reference capacity_configs table Flag Default Effect use_skills false Filter techs by job type required skills use_business_units true Filter techs by BU assignment filter_out_non_managed_techs false Exclude Non-Managed Tech license types unassigned_bu_techs_universally_eligible false Techs with no BU can work any BU use_service_area_eligibility false Filter techs by service area zones excluded_tech_st_ids [] Explicitly exclude specific techs by ST ID include_on_call_shifts false Count on-call shifts as available capacity threshold_final_tech_capacity false Apply availability threshold after all filtering ignore_window_end_cutoff false Allow booking past arrival window end time non_job_events_require_capacity_flag true Only count non-job events with ST's "remove from capacity" flag non_job_events_require_timesheets true Only count non-job events with timesheet codes responder_webhook_configs table (key flags) Flag Effect avoca_capacity_enabled true = ACP (Avoca capacity); false = Non-ACP (ST native) skills_sync_enabled Enable daily Simplex-based skills sync from ST transfer_on_no_availability true = transfer call when no slots; false = take message transfer_on_no_same_day_emergency Transfer emergency calls with no same-day slots pre_booking_checklist_enabled Enable pre-booking checklist validation before booking technician_assignment_mode How techs are assigned post-call (EMERGENCY, ALWAYS, NEVER, ST_NONEMERGENCY, ST_ACP_ALWAYS)
Section 18: CRMs & ServiceTitan
Avoca integrates with multiple CRMs, but ServiceTitan (ST) is the primary and most deeply integrated. Understanding ST's data model is critical — almost every Avoca product touches it.
What is ServiceTitan?
ServiceTitan is the leading software platform for home services companies (HVAC, plumbing, electrical, pest control, etc.). It's their all-in-one system — CRM, dispatching, scheduling, invoicing, marketing attribution, and field service management. Think of it as the "operating system" for a home services business.
Every Avoca customer on ServiceTitan has a Tenant — their unique ST instance. All API calls are scoped to a tenant. Avoca syncs data from each tenant and uses ST's APIs to book jobs, look up customers, check availability, and dispatch technicians.
ServiceTitan concept topology
How ST entities relate. Amber = booking flow path, blue = organizational, green = customer hierarchy, purple = job structure, gray = field operations. Animated amber shows where Avoca layers on top (Service Area → Booking Window).
has many has many defines has job types serves zones has locations has contacts may have has equipment works shifts assigned zone has appointments may have may have bill-to work site categorizes owns attributed to assigned to maps to (Avoca) determines available slots Tenant Company instance Business Unit Plumbing, HVAC, etc. Campaign Marketing attribution Job Type AC Repair, Drain Clean Tag Type VIP, Commercial, etc. Customer Bill-to party Location Service address Contact Phone, email Membership Recurring service plan Technician Field employee Shift Work schedule Dispatch Zone Territory (ST) Job Work order Appointment Time slot + tech Estimate Quoted price Invoice Final billing Installed Equipment AC unit, furnace, etc. Service Area Avoca zip → zone map Booking Window Avoca availability slots React Flow ServiceTitan business topology
How a home services company is organized in ST — and how Avoca maps to each layer:
Tenant → Business Units → Service Areas / Zones → Technicians & Shifts → Customers & Locations → Jobs & Appointments
A tenant has business units (e.g. "Plumbing", "HVAC"). Each BU serves certain zones/areas. Technicians are assigned shifts and territories. When a customer calls, Avoca resolves the right BU, finds available slots, and books a job at their location.
Core ServiceTitan concepts & how Avoca uses them Customer What it is: A person or company — the bill-to party responsible for payment. Has name, phone, email. Can have multiple locations. How Avoca uses it: When a call comes in, Avoca searches by phone number first, then can filter by address when multiple matches are found. If not found, creates a new customer. This is the first step in every booking flow. Location What it is: A service address tied to a customer. This is WHERE the work happens. Includes street address, city, state, zip. Customers can have many locations. How Avoca uses it: Avoca resolves or creates the location as part of booking. The location's zip code determines which Service Area and Business Unit apply. Business Unit (BU) What it is: An organizational segment — typically by trade (Plumbing, HVAC, Electrical) or region. BUs control which jobs, technicians, and booking windows are available. How Avoca uses it: Every job must belong to a BU. Avoca resolves the target BU based on the caller's zip code, service type, and team configuration. Availability is BU-specific — different BUs have different open slots. Job What it is: A work order — the core record in ST. Has a job type, business unit, customer, location, and one or more appointments. Jobs go through a lifecycle: Created → Scheduled → Dispatched → Completed. How Avoca uses it: When Avoca books an appointment, it creates a Job in ST via the API. The job includes customer ID, location ID, job type, BU, campaign attribution, and a summary of what the customer needs. Appointment What it is: A time slot within a job — when the technician shows up. A job always has at least one appointment (created automatically when booked). Includes arrival window (e.g. 9am-12pm) and assigned technician(s). How Avoca uses it: Avoca manages its own Booking Windows config (which BU + job type + zone gets which time slots). At booking time, it queries ST's availability APIs and cross-references its booking window rules to present open slots. Job Type What it is: Categorizes what kind of work — e.g. "AC Repair", "Drain Cleaning", "Furnace Maintenance". Job types are tied to business units. How Avoca uses it: The AI agent infers the job type from the conversation (what the customer describes). This determines which BU, booking windows, and technician skills are needed. Technician What it is: A field service employee. Has skills, assigned zones/territories, and shift schedules. How Avoca uses it: Avoca's dispatching engine scores technicians by drive time, skill match, territory, callback history, and performance. The top candidate gets assigned to the appointment. Shift What it is: Tracks when technicians are working, off, or on-call for emergencies. Can span one day or go overnight. How Avoca uses it: Dispatch uses shifts to filter eligible technicians — only those on-shift (or on-call for emergencies) are considered. Dispatch Zone (ST) → Service Area (Avoca) What it is: ST has Dispatch Zones — geographic territories assigned to technicians for field routing. Service Area is an Avoca concept built on top: we map zip codes to zones to determine coverage, BU, and booking windows. How Avoca uses it: When a customer gives their zip code, Avoca looks up which Service Area it belongs to (our mapping, not ST's). This determines the BU and available booking windows. Teams can auto-sync ST dispatch zones to populate Avoca Service Areas. Tag / Tag Type What it is: Labels attached to customers, locations, or jobs for classification — e.g. "VIP", "Commercial", "Membership Holder". How Avoca uses it: Avoca uses tags for audience filtering in outbound campaigns (e.g. target all customers with tag "Maintenance Due") and for bookability rules (e.g. exclude jobs with certain tags). Membership What it is: Recurring service plans with billing and discount logic. Memberships have statuses (Active, Expired, Pending). How Avoca uses it: The AI agent can check membership status during a call and offer membership-specific pricing or priority scheduling. Campaign What it is: Marketing source tracking — labels calls and jobs for ROI measurement (e.g. "Google Ads Q1", "Direct Mail"). How Avoca uses it: When booking, Avoca includes the campaign ID on the job for attribution. Post-booking, a handling note is written to the job documenting the call outcome and campaign source. Booking What it is: An incoming inquiry that appears on the Calls screen in ST. Lighter than a full Job — used for initial lead capture from web widgets or external sources. How Avoca uses it: Bookings appear on the ST Calls screen for CSR follow-up. The Simple Scheduler widget actually creates full Jobs (not Bookings) directly in ServiceTitan. Estimate / Invoice What it is: Financial documents attached to jobs. Estimates precede work; invoices follow completion. How Avoca uses it: Synced for analytics and used in outbound campaigns (e.g. "Unsold Estimates" campaign targets customers with open estimates that never converted to jobs). How Avoca books a job in ServiceTitan
- Find or create customer — search by phone, filter by address if multiple matches; create if new
- Resolve location — verify service address exists or create it under the customer
- Match business unit — based on zip code + service type → determines which BU handles the job
- Check availability — query booking windows for the BU + job type + location combination → get open slots
- Assign technician — score eligible techs by drive time, skills, territory, shift, performance
- Create job — API call with customer, location, job type, BU, appointment slot, tech, campaign ID, and job summary
- Post-booking writeback — async: append handling note to the job documenting call details and outcome Data sync with ServiceTitan
Sync is not automatic for all teams. Each team has per-entity rows in the crm_service_titan.sync_config table with an is_enabled flag. Only enabled configs are synced.
The orchestrator runs every minute via Inngest, fetches all enabled sync configs, and dispatches sync events. Uses cursor-based pagination (Export API for most entities, List API for a few). Concurrency: 180 global, 4 per team. Rate: ~18 API calls/minute per team.
Customers Locations Jobs Appointments Bookings Calls Campaigns Business Units Job Types Tag Types Technicians Memberships Invoices Estimates Equipment
Synced data lives in the crm_service_titan schema in Supabase. Each sync config tracks cursor position for incremental updates — the unified sync function resumes from the last successful batch.
Other CRM integrations
Each non-ST CRM has its own workflow module with booking, customer lookup, and data sync tailored to that platform's API:
Jobber Full integration — GraphQL API for customers, quotes, jobs, scheduling Salesforce Booking opportunity sync, used for 1-800-GOT-JUNK FieldRoutes Pest control CRM — lead creation, job booking PestPac Pest control CRM — lead and service management HouseCall Pro Working integration — booking, customer lookup, post-call processing Workiz Field service CRM integration AccuLynx Roofing/contractor CRM JobNimbus Roofing/contractor CRM
Section 19: Voice AI & VAPI — Deep Dive
Detailed look at the VAPI integration — how calls are handled, what tools exist, and what happens after each call.
VAPI webhook events status-update Call state changes — ringing, in-progress, forwarding, ended end-of-call-report Full call data after disconnect — transcript, duration, recording tool-calls During-call tool invocations — booking, lookup, transfer, etc. transfer-destination-request Warm transfer routing — which human/department to hand off to Tool categories Customer Find customer, get locations, equipment tags Booking Check availability, book appointment, reschedule, cancel Fees Check dispatch fee, fee waiver / manager approval Transfer Warm transfer routing, destination lookup Service Area Zip code check, service area validation Promos Promotional offer validation and info Post-call async jobs Call Analysis LLM scores intent, fulfillment, and custom rubrics Alert Triage Routes email/Slack alerts based on call outcome CRM Sync Syncs call metadata back to ServiceTitan Dropped Lead Creates lead for unbooked calls (missed opportunities) Text Linking Links follow-up SMS conversations to the originating call Prompt deployment pipeline Knowledge Base → Config Variables → Liquid Template → Compiled Prompt → VAPI Deploy
Section 20: Hands-On Exercise
The best way to learn Avoca is to onboard yourself as if you were a new customer. You'll create a company, set up an AI assistant, make real calls, and trace every step through the system.
Goal: By the end, you will have onboarded a fake company onto Avoca, configured an AI voice assistant, called it as a customer, booked an appointment in ServiceTitan's sandbox, and traced the entire call lifecycle from phone ring to post-call analysis. Phase 1 — Create your test team
Pretend you're an HVAC company signing up for Avoca. You'll create a real team in the system.
☐ Log in to the Avoca Admin Panel (app.avoca.ai/admin) ☐ Go to Admin → Teams and click Create Team ☐ Name it something like "[YourName] Test HVAC" — leave enterprise blank, set timezone to America/Los_Angeles ☐ This creates a team record + Twilio subaccount automatically ☐ Ask your buddy to connect this team to the ServiceTitan integration sandbox (ST credentials need to be configured for the team) ☐ Open your team in the Dashboard (dashboard.avoca.ai) — this is what a customer sees ☐ Explore the dashboard: AI Calls, Knowledge Base, Settings — understand the customer experience Note: Alternatively, ask your buddy if there's an existing test team already connected to the ST sandbox that you can reuse. Phase 2 — Study a live team first
Before building your own, see how a real customer's setup works.
☐ Ask your buddy for a good live team to explore ☐ Open their Prompt Builder — read the Knowledge Base (company info, job types, hours, FAQs) ☐ Look at their compiled prompt — notice how Liquid variables get filled in ☐ Check their Voice Assistants — see the VAPI assistant ID, config mode ☐ Open Call Debugger — find a recent call and trace the full lifecycle: — Transcript (user + assistant turns) — Tool calls (booking, customer lookup, availability) — Call outcome and classification — Post-call analysis (insights, tags) ☐ Open the same call in VAPI dashboard — compare the raw VAPI logs Customer requirements — "[YourName] Test HVAC"
This is the team you created in Phase 1, and you’ve already set up the ServiceTitan integration. Now, check what this "customer" has in their ServiceTitan account—your job is to configure Avoca to match these details.
Business Units Plumbing Electrical HVAC Sales HVAC Service HVAC Maintenance HVAC Install Job Types No heat No cool No power Install ceiling fan Technicians (12 active) Adam (Plumbing), Bob (Electrical), Charles (HVAC Sales), David (HVAC Service), Dan (HVAC Maintenance), Frank (HVAC Install), Harry (Plumbing), Ken (Electrical), Mike (HVAC Sales), Matt (HVAC Service), Will (HVAC Service) Marketing Campaigns PPC Facebook Yelp Radio Angie's List HomeAdvisor Direct Mail Google Avoca Membership Types Default HVAC Plumbing All Service Areas West, East, South (+ 1 unnamed) Dispatch Fees AC Service Fee After Hours — $300 (AC Repair/Service after 5pm) AC Repair — $120 (AC repair/service calls) Dispatch Zones West (Beverly Hills, Santa Monica), North (Glendale, Burbank, Pasadena), East (Alhambra, Monterey Park), South (Huntington Park, Inglewood), Central (Los Angeles), Pearland TX, Dublin GA Arrival Windows (configured for West zone) 9am–10am 10am–5pm 5pm–10pm (after hours) Emergency Rules Emergency enabled — triggers: "No heat", "No cool", customer is a member Services Not Provided (configure in KB) Roofing — transfer to (555) 123-4567 "ABC Roofing" Landscaping — politely decline, no transfer Transfer Destinations (configure in admin) Set up at least one: "Main Office" — a phone number the AI can warm-transfer to when customer asks for a manager or the AI can't help Responder Features to Enable Booking Rescheduling Cancellations On-Call Promotions Sell Membership Technician Arrival Duration "The technician will contact you when they are 30 minutes away" Environment ServiceTitan Integration sandbox (tenant 1250467022) — safe to create test customers, jobs, and bookings. Zones are in the LA metro area. Suggested Knowledge Base content to configure
Use this as a guide when filling out the Knowledge Base in Phase 3.
Company Name Eng Testing Home Services Assistant Name Sarah Timezone America/Los_Angeles City Los Angeles, CA CRM Service Titan Fees Name Service Fee ($120 standard, $300 after hours) Greeting "Thank you for calling Eng Testing Home Services! This is Sarah, how can I help you today?" Closing Line "Thank you for choosing Eng Testing Home Services! The technician will contact you when they are 30 minutes away. Have a great day!" Job Types (map to BU) Job Type Service Type Business Unit Example Caller Says No heat HVAC HVAC Service "My heater isn't working" No cool HVAC HVAC Service "My AC stopped blowing cold air" No power Electrical Electrical "I have no power in my kitchen" Install ceiling fan Electrical Electrical "I need a ceiling fan installed" Emergency Guidance Emergency if: customer has no heat, no cool, or is a current member. Non-emergency: ceiling fan install, routine maintenance. Business Hours Mon–Fri 8am–5pm PST. After hours: 5pm–10pm (higher fee applies). Services Not Provided Roofing → transfer to (555) 123-4567 "ABC Roofing" Landscaping → politely decline, no transfer number Membership Info Plans available: HVAC ($15/mo), Plumbing ($12/mo), All-inclusive ($25/mo). Members get priority scheduling and waived service fees. Sample FAQ Q: How much does a service call cost? A: Our standard service fee is $120. After-hours calls (after 5pm) are $300. Q: Do you offer memberships? A: Yes! We have HVAC ($15/mo), Plumbing ($12/mo), and All-inclusive ($25/mo) plans. Q: What areas do you serve? A: We serve the greater Los Angeles area including Beverly Hills, Santa Monica, Glendale, Burbank, Pasadena, and surrounding cities. Q: Can you do roofing work? A: We don't handle roofing, but I can transfer you to our partner ABC Roofing. Q: What if I need emergency service after hours? A: We have on-call technicians available. After-hours service has a $300 fee. Affiliated Brands ABC Roofing (roofing referral partner) Phase 3 — Configure the AI assistant using the requirements above
Use your team created in Phase 1 ("[YourName] Test HVAC"). Your goal is to make the assistant handle HVAC, Plumbing, and Electrical calls based on what's available in the ST sandbox.
☐ Go to Admin → Voice Assistants → create a new assistant for your team ☐ Go to Admin → Prompt Builder → create a Knowledge Base: — Company: "Eng Testing Home Services", America/Los_Angeles — Assistant name: "Sarah" — CRM: Service Titan — Job types from ST: "No heat" (HVAC Service), "No cool" (HVAC Service), "No power" (Electrical), "Install ceiling fan" (Electrical) — Map each job type to the correct Business Unit (e.g. "No heat" → HVAC Service, "No power" → Electrical) — Business hours: Mon–Fri 8am–5pm PST — Emergency: enabled ("No heat, no cool") — Greeting: "Thank you for calling Eng Testing Home Services! This is Sarah, how can I help you today?" ☐ Select the modular template → compile the prompt → review the output — verify your job types and BUs appear ☐ Deploy to VAPI — this pushes your prompt to a real VAPI assistant ☐ Configure a marketing campaign by going to Settings → Integrations → ServiceTitan Integration. Inside the integration, use the Edit Campaign option to set up or modify campaign tracking for this team. ☐ Assign a phone number (provision a test number or use the existing +18702300302) Phase 4 — Call your company as a customer
Switch hats — you're now a homeowner with a broken AC calling Eng Testing Home Services for help.
☐ Dial the phone number from your personal phone — you should hear "Thank you for calling Eng Testing Home Services! This is Sarah..." ☐ Play the customer: — "Hi, my name is John Smith" — "I'm at 123 Main St, Beverly Hills CA 90210" — "My AC stopped blowing cold air" (should trigger "No cool" job type → HVAC Service BU) — When offered time slots, pick one — Confirm the booking ☐ After the call ends, wait ~30 seconds for post-call processing ☐ Make a second call — try a different scenario: — "I have no power in my kitchen" (should trigger "No power" → Electrical BU) — Ask about the dispatch fee — Ask if they have a membership plan (HVAC, Plumbing, or All are available in ST) — Ask to speak to a manager (transfer request) ☐ Alternatively: use Admin → VAPI Testing to run automated test calls Phase 5 — Trace the call end-to-end
This is the most important part — understanding what happened at each step.
☐ Open Call Debugger — find your call by phone number or call ID ☐ Read the transcript — verify it matches your conversation ☐ Check tool calls — you should see: — findCustomer (searched for you by phone) — checkAvailabilities or getAvailabilities (fetched time slots) — bookAppointment (created the job in ST) ☐ Check the calls table in Supabase — find your call record, note the fields ☐ Check insights_call_analyses — see the AI-generated analysis of your call ☐ Open ServiceTitan integration dashboard — verify the job was created with correct customer, location, job type, and appointment window ☐ Check the VAPI dashboard — find the same call, see the raw VAPI logs Phase 6 — Break things and debug ☐ Make a call asking for a service you didn't configure — see how the AI handles it ☐ Try to book outside business hours — verify the AI rejects it ☐ Ask about pricing / fees — see if the fee tool is called ☐ Request a transfer — see how the transfer destination tool works ☐ Hang up mid-call — check how the end-of-call-report handles an incomplete conversation ☐ Edit the Knowledge Base, redeploy, and call again — verify the change takes effect Bonus — See it from the customer's perspective ☐ Open your test team in the Dashboard (dashboard.avoca.ai) — this is what your customer sees ☐ Find your test calls in AI Calls — see how they appear to the business owner ☐ Open Knowledge Base in the dashboard — edit the greeting, save, and redeploy ☐ Call again — verify the new greeting takes effect ☐ Check the ServiceTitan integration sandbox — verify the job was created under the correct BU (e.g. "HVAC Service" for "No cool", "Electrical" for "No power"). Check that the customer "John Smith" was created with the right address You're done! You've now touched every layer: Knowledge Base → prompt compilation → VAPI deployment → live call → tool execution → post-call processing → call analysis. You understand Avoca end-to-end.