Skip to main content

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_calls tables), #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_personas and video_calls were added by #2052 to support per-deployment Tavus persona records and persisted video-call sessions. See api/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 via TavusFunctionDefinitionsLegacyController for ~30 days, then are removed (rename completed via #1031, closes #630). Terms like persona and replica remain 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:

WorkflowTriggerTavus roleAgent-service role
OnboardingFirst deployment of a Specialist to a client orgHosts a ~60min structured intake video call; its own LLM drives the conversationNot in-loop. Tavus webhooks call back to the h.work API to record function-call outputs.
ReportingScheduled or ad-hoc check-inHosts the video call; in-conversation responses about work history are fetched liveNot 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.

TablePurposeKey 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
meetingsA scheduled or ad-hoc video touchpoint. kind in (onboarding, reporting). References Tavus persona/conversation IDs.org_id, assignment_id, created_by โ†’ users
meeting_decisionsDecisions logged via reporting function calls. Also mirrored into actions (see ยง4).meeting_id, org_id
meeting_prioritiesPriority changes logged via reporting function calls.meeting_id, org_id
tavus_function_definitionsPersona 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_briefs rename is fully shipped across three PRs, all merged 2026-06-04 under #632: the DB table + client_brief_revisions audit table via PR #1602 (migration 1785500000001-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: table client_briefs, entity ClientBrief, module api/src/client-briefs/, API path /client-briefs/* (old /environment-profiles/* paths kept as 308 redirects by client-briefs-redirect.controller.ts, slated for removal ~30 days after the frontend path swap). The historical migration 1746000000010-PersonaManagerInit.ts that 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 as client_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 entity environment_profile, the standalone hwork-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_orgOrganization (existing)
user (with tier)User + platformRole + OrgMembership.org_role (existing)
role_template + role_template_versionSpecialist (existing); versioning deferred to ADR-0003
deploymentOrgSpecialistAssignment (existing)
environment_profile (POC)client_briefs (new)
meetingmeetings (new)
work_log_referenceNot ported. The brief assembler reads live from Conversation/Message/Action via service-layer queries (ADR-0007 of the POC is moot here).
function_call_definitiontavus_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.md and the docstring in webhook-verifier.service.ts.

The Tavus webhook controller lives in api/src/tavus/tavus.controller.ts. Authentication works as follows:

  • At conversation-creation time, buildTavusCallbackUrl appends 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 an x-tavus-token header fallback for manual replays) and WebhookVerifierService.verifyToken compares it against TAVUS_WEBHOOK_SECRET using 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.ts flow described below was deleted. Tavus persona prompts are now built directly in TypeScript by api/scripts/provision-tavus-personas.ts::buildSystemPrompt at catalog-provisioning time โ€” they no longer share a template with the Python agent service. Per-call data (BRIEF JSON, identity headers) ships as conversational_context on the conversation, not as template substitution on the persona prompt. See #1387.

Updated 2026-06-12. agent/prompts/onboarding_meeting.txt and agent/prompts/reporting_meeting.txt have been deleted. They were leftovers from the pre-#1387 design: the agent service's prompts_loader.py only loads a prompt file when a /chat call carries the matching role string, and no code path has ever sent role="onboarding_meeting" or role="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:

  1. Persona system_prompt โ€” built once per Specialist from the catalog row by api/scripts/provision-tavus-personas.ts::buildSystemPrompt.
  2. Per-call conversational_context โ€” assembled at meeting start by api/src/meetings/conversation-starter.service.ts. For onboarding this includes ONBOARDING_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-calls via #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 prompt
  • docs/decisions/tavus/ADR-0001-integration-approach.md โ€” chosen integration approach (see also docs/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 mirror
  • api/src/channels/channels.controller.ts:23-62 โ€” HMAC pattern to mirror
  • api/src/notifications/notifications.gateway.ts โ€” Socket.io pattern to extend
  • agent/prompts_loader.py โ€” prompt assembly pipeline