Tavus Integration Spec
Status: Phase 1 SHIPPED 2026-06-05/06 ยท approved 2026-05-13 ยท Builder source: prompts/persona-builder.md
2026-06-05/06 status: Phase 1 production-shape shipped โ #2052 (
tavus_personas+video_callstables), #2054 (security guardrails preventing cross-client data leakage), #2055 (on-demand video call creation API). The 5-table additive plan in ยง2 below is now 7 tables โtavus_personasandvideo_callswere added by #2052 to support per-deployment Tavus persona records and persisted video-call sessions. Seeapi/migrations/for the migration filenames.
Naming note (updated 2026-06-02): API module is
api/src/tavus/(TavusManagerService,TavusController). Frontend Ops surface is/ops/tavus/*(function-calls, replicas, templates) โ legacy/ops/personas/*paths return 308 redirects viaTavusFunctionDefinitionsLegacyControllerfor ~30 days, then are removed (rename completed via #1031, closes #630). Terms likepersonaandreplicaremain admissible in Tavus-vendor context (Tavus persona objects, Tavus replica IDs).
This spec defines the design surface for the Tavus video-conversation integration in the h.work monorepo. It is the authoritative reference for the Builder agent during Phases 0โ6 of TODO_PERSONA.md.
The Tavus integration was originally prototyped as a standalone POC at the archived hwork-persona-manager/ repo; that POC is the historical source for the schema migrations (api/migrations/1746000000010-PersonaManagerInit.ts etc.). Migration filenames are immutable and retain the historical name.
1. Scopeโ
The Tavus integration adds two video-conversation workflows to h.work:
| Workflow | Trigger | Tavus role | Agent-service role |
|---|---|---|---|
| Onboarding | First deployment of a Specialist to a client org | Hosts a ~60min structured intake video call; its own LLM drives the conversation | Not in-loop. Tavus webhooks call back to the h.work API to record function-call outputs. |
| Reporting | Scheduled or ad-hoc check-in | Hosts the video call; in-conversation responses about work history are fetched live | Not in-loop (see ยง6.2). Function-call outputs persist via Tavus webhooks to the h.work API. |
2. Five new tables (additive only)โ
All new tables are created in a single new migration. No existing table is
altered. Each table carries organization_id for tenancy and gets an RLS
policy in the migration matching the patterns in migrations
9 (RLS), 12 (MemoryRLS), 13 (RlsMissingTables), 17 (OrgOnboardingRLS),
27 (OnboardingTablesRLS), 33 (RlsComplete). PM-0067 corrected the
originally-cited list โ migration 20 (OnboardingOtps) has no RLS content.
| Table | Purpose | Key FKs |
|---|---|---|
client_briefs (created as environment_profiles; renamed โ see note below) | Structured operating-environment facts captured during onboarding for an OrgSpecialistAssignment. One row per (org, specialist) deployment. | org_id โ organizations, assignment_id โ org_specialist_assignments, specialist_id โ specialists |
meetings | A scheduled or ad-hoc video touchpoint. kind in (onboarding, reporting). References Tavus persona/conversation IDs. | org_id, assignment_id, created_by โ users |
meeting_decisions | Decisions logged via reporting function calls. Also mirrored into actions (see ยง4). | meeting_id, org_id |
meeting_priorities | Priority changes logged via reporting function calls. | meeting_id, org_id |
tavus_function_definitions | Persona function-call library. Versioned JSON Schema. SuperAdmin-managed. | none (global) |
Full column-level schema is defined in the PM-0003 migration (Phase 0).
Naming โ rename complete (2026-06-04): The
environment_profilesโclient_briefsrename is fully shipped across three PRs, all merged 2026-06-04 under #632: the DB table +client_brief_revisionsaudit table via PR #1602 (migration1785500000001-RenameEnvironmentProfilesToClientBriefs.ts, closes #1172); the API module / entity / controller / route + permission guards via PR #1613 (closes #1173); and the frontend copy + API client via PR #1199 (closes #1174). The live names are now: tableclient_briefs, entityClientBrief, moduleapi/src/client-briefs/, API path/client-briefs/*(old/environment-profiles/*paths kept as 308 redirects byclient-briefs-redirect.controller.ts, slated for removal ~30 days after the frontend path swap). The historical migration1746000000010-PersonaManagerInit.tsthat originally created the table retains the old name (immutable history). This spec body below predates the rename โ read references to the live h.work table / entity / module / API path asclient_briefs/ClientBrief/api/src/client-briefs///client-briefs/*. This does not rewrite immutable historical names: the migration filenames above and the archived POC source model in ยง3 (POC entityenvironment_profile, the standalonehwork-persona-manager/repo) keep their original names by definition.
3. Entity mapping โ POC โ hwork-appโ
The standalone POC has its own entity model. The integration maps as follows:
POC entity (archived hwork-persona-manager/) | hwork-app equivalent |
|---|---|
client_org | Organization (existing) |
user (with tier) | User + platformRole + OrgMembership.org_role (existing) |
role_template + role_template_version | Specialist (existing); versioning deferred to ADR-0003 |
deployment | OrgSpecialistAssignment (existing) |
environment_profile (POC) | client_briefs (new) |
meeting | meetings (new) |
work_log_reference | Not ported. The brief assembler reads live from Conversation/Message/Action via service-layer queries (ADR-0007 of the POC is moot here). |
function_call_definition | tavus_function_definitions (new) |
audit_log (POC) | Not ported. Use existing AuditService.log(...) โ audit_logs table. |
4. The Action extension-point patternโ
The existing Action entity (created in migration 11) is designed as an
extension point. It carries actorType, actionType, payload, result,
risk_level, status. The Tavus integration uses it for reporting decisions
only:
await actionRepo.save({
orgId,
conversationId: null, // reporting decisions are not message-bound
messageId: null,
actorType: 'reporting_meeting',
actionType: 'decision_logged',
payload: { meetingId, decisionId, summary, manager_user_id },
result: { meetingDecisionId },
riskLevel: 'low',
status: 'completed',
});
This is the only sanctioned write to an existing table from persona manager code. All other mutations go to the new tables.
Reporting escalations are different โ they are not Action writes; they are
calls to ExpertQueueService.create(...), which itself creates an
ExpertQueueItem and emits the existing notification events. The persona
manager does not duplicate the queue surface.
5. Tavus webhook authentication (shared-secret token, NOT HMAC)โ
Rewritten post-#2277. An earlier revision of this section specified an HMAC-SHA256 mirror of the channels-webhook pattern. That design was implemented and then reverted: Tavus does not sign CVI webhooks (there is no signature header on the wire), so HMAC verification made every callback 401 and the integration went silent. Do not reintroduce HMAC verification here โ see
api/src/tavus/CLAUDE.mdand the docstring inwebhook-verifier.service.ts.
The Tavus webhook controller lives in api/src/tavus/tavus.controller.ts.
Authentication works as follows:
- At conversation-creation time,
buildTavusCallbackUrlappends the shared secret as a query param to the registered callback URL:โฆ/tavus/webhooks/function-call?token=$TAVUS_WEBHOOK_SECRET. - Each webhook endpoint reads the token back (
?token=query param, with anx-tavus-tokenheader fallback for manual replays) andWebhookVerifierService.verifyTokencompares it againstTAVUS_WEBHOOK_SECRETusing a length check +crypto.timingSafeEqual. - Fail-open when the secret is unset is deliberate: the URL builder omits the token AND the verifier returns silently, so an unconfigured env still receives callbacks. The verifier logs a loud warning on staging/production when the secret is missing.
Idempotency: each webhook payload carries a Tavus idempotency key. The
router dedupes in Redis (using the existing getRedisClient() from
api/src/common/redis.util.ts) with a tavus:webhook:idem:<key> namespace
and a 24-hour TTL.
6. Agent service touchpointsโ
Updated post-#1390 (#1482). The
persona-generator.service.tsflow described below was deleted. Tavus persona prompts are now built directly in TypeScript byapi/scripts/provision-tavus-personas.ts::buildSystemPromptat catalog-provisioning time โ they no longer share a template with the Python agent service. Per-call data (BRIEF JSON, identity headers) ships asconversational_contexton the conversation, not as template substitution on the persona prompt. See #1387.Updated 2026-06-12.
agent/prompts/onboarding_meeting.txtandagent/prompts/reporting_meeting.txthave been deleted. They were leftovers from the pre-#1387 design: the agent service'sprompts_loader.pyonly loads a prompt file when a/chatcall carries the matchingrolestring, and no code path has ever sentrole="onboarding_meeting"orrole="reporting_meeting". The files were dead โ editing them changed nothing about any call.
6.1 What actually drives a Tavus callโ
Two layers, both TypeScript-side:
- Persona
system_promptโ built once per Specialist from the catalog row byapi/scripts/provision-tavus-personas.ts::buildSystemPrompt. - Per-call
conversational_contextโ assembled at meeting start byapi/src/meetings/conversation-starter.service.ts. For onboarding this includesONBOARDING_INTAKE_SCRIPT(the 9-bucket intake script, inline in that file); for reporting it includes the BRIEF JSON. Identity headers (org name, calling user) ride along on both.
Function-call tools (layers.llm.tools) are attached to the persona by
provision-tavus-personas.ts: the seeded tavus_function_definitions
library (onboarding + reporting) plus
api/src/tavus/video-call-tool-definitions.ts (on-demand, #2056). The
script also re-syncs tools onto existing personas on every run.
6.2 Agent service involvementโ
None at call time. The FastAPI agent service is not in the loop for any
Tavus call type; Tavus webhooks call back into the NestJS API
(/tavus/webhooks/*) for function-call persistence, transcripts, and
recordings.
7. Frontend surfaceโ
New routes only:
/client/specialists/[id]/onboarding/โ start, in-progress, post-call review/client/specialists/[id]/meetings/โ list, detail, in-session, post-session/ops/tavus/function-calls(SuperAdmin) โ function-call CRUD (renamed from/ops/personas/function-callsvia #1031; legacy path returns 308 for ~30 days)/ops/tavus/replicas(SuperAdmin, Phase 5) โ Tavus replica directory/ops/tavus/templates(SuperAdmin, Phase 5) โ persona prompt templates
Additive functions are appended to frontend/src/lib/api.ts:
startOnboarding, getClientBrief, getClientBriefByAssignment,
updateClientBrief, approveClientBrief, plus meeting CRUD/start (renamed
from the original *EnvironmentProfile helpers via PR #1199; deprecated
aliases retained for one release). No signature changes to existing functions.
WebSocket events extend the existing /notifications namespace:
meeting_started, meeting_function_call, meeting_completed. Emitted via
the existing NotificationsGateway โ handlers obtain the gateway via DI
and call server.to('org:<orgId>').emit(...).
8. SOUL.md non-mutationโ
The Tavus integration generates orgs/<slug>/SOUL.proposed.md only. The
operator review/promote workflow for SOUL.md itself remains the existing
manual process. The Client Brief approval does not write to
SOUL.md; it only updates a row in client_briefs and writes the
proposed file.
9. Out of scope (deferred)โ
- Role-template versioning (POC
role_template_version) โ deferred to ADR-0003 when a versioning need actually arises. - POC's Celery + Redis async workers โ replaced by BullMQ (already installed) only for the persona archival job (PM-0066).
- POC's separate Postgres database โ collapsed into the main hwork-app Postgres. Tables live under the same connection.
10. Referencesโ
prompts/persona-builder.mdโ Builder operating promptdocs/decisions/tavus/ADR-0001-integration-approach.mdโ chosen integration approach (see alsodocs/decisions/tavus/ADR-0002-integration-test-deferral.md)hwork-persona-manager/docs/architecture.mdโ archived POC architecture (reference only; not the integration's authority)api/migrations/9, 12 (MemoryRLS), 13, 17, 27, 33 โ RLS patterns to mirrorapi/src/channels/channels.controller.ts:23-62โ HMAC pattern to mirrorapi/src/notifications/notifications.gateway.tsโ Socket.io pattern to extendagent/prompts_loader.pyโ prompt assembly pipeline