Skip to main content

FuneralWiseAI — At-Need Director Alert-Call (MVP) Expected Output Spec

STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.

Stage-1 agent draft, pre-populated from the founder's signed-off design (2026-07-02), the live schema (verified below), the funeral safety-logging design (products/funeralwiseai/safety-logging-design.md, the crisis_events no-FK tenant shape this reuses), and the funeral vertical spec. Do not build until the founder approves and the §11 open items are confirmed in a Stage-2 interview.

AI Bridge Principle. This feature IS the bridge, at its most literal: an AI capture becomes a real human director on a real phone as fast as possible. The AI never handles the at-need family itself — it escalates to a human. See architecture/ai-bridge-principle.md.


0. Scope — what this feature is (and is NOT)

In scope (MVP):

  1. A DB-trigger → authed-endpoint → Telnyx Call Control outbound call path that phones the on-call funeral director when an at-need callback is captured, from either channel (voice agent or chatbot), with zero change to the voice agent.
  2. A warm ~25s TTS summary read on answer, a DTMF ack (1 = acknowledge, 2 = repeat, 3 = bridge to the family), and answering-machine detection that leaves a voicemail but does not count as an ack.
  3. An escalation state machine: no answer / no ack within a configurable window → next director in the roster → chain exhausted → fall back to the existing SMS/email status quo + surface a failed-ack state in the "Needs a director" queue.
  4. A NEW at_need_alert_calls log table (no FK to churches — the crisis_events tenant_id + vertical shape) recording every attempt, and dashboard surfacing so no failure is silent.
  5. Per-tenant opt-in (default OFF), a global env kill switch, and a dial-allowlist (roster / notification numbers only; never the family's number except via an explicit director-initiated DTMF-3 bridge).

Explicitly OUT of scope / HARD BOUNDARIES:

  • NO changes to voice-agent-livekit/, the LiveKit SIP trunk ST_Xa3Bp9aixRFP, or any LiveKit dispatch rule. Telnyx Call Control is a separate API surface from SIP trunking; this feature places a fresh outbound Call Control call and never touches the inbound voice pipeline. Therefore there is no LIFE-SAFETY voice deploy and no voice-agent-engineer deploy gate for the call path itself. (The trigger fires off the DB row the voice agent already writes — the voice agent's code is unmodified.)
  • No email is sent by this feature to families or prospects; the only outbound is an operational phone call (and, on full-chain failure, the existing SMS/email status-quo notification — not a new send path).
  • Not a replacement for the existing notification system — it is a faster, higher-urgency layer in front of it that falls back to it.
  • No quiet-hours suppression by default (at-need is 24/7); per-tenant configurable.

Build posture (do-not-reinvent): the trigger source (voice_callback_requests, urgency='at_need') is already written by both the voice agent and the chatbot today — this feature adds a trigger + endpoint + Telnyx client + log table + dashboard surfacing, and changes nothing in the capture path.


1. Verified ground truth (2026-07-02)

Confirmed by querying the live DB (wrwkszmobuhvcfjipasi) before writing:

  • voice_callback_requests columns: id, church_id, caller_name, caller_phone, reason, preferred_time, urgency, status, created_at, agreed_day, agreed_time_window, source, is_hidden, admin_notes, updated_at, caller_email, vertical. The tenant id lives in church_id (overloaded to hold any tenant id — same pattern the safety-logging design documents), and vertical exists. source distinguishes voice vs chat (so the trigger covers both channels from one row type).
  • urgency is text; the value at_need is already in live use (present values: at_need, low, medium, normal, pastoral_emergency, pre_planning, urgent). Note pastoral_emergency also exists — the church cross-vertical hook (§9).
  • funeral_homes.metadata is JSONB and metadata.staff is a JSON array — the on-call roster source.
  • premium_funeral_homes has contact_phone (the tenant notification number) and metadata (opt-in config target).
  • crisis_events already ships the no-FK tenant shape (tenant_id, vertical, plus per-event status/observability columns) — the model for at_need_alert_calls.
  • at_need_alert_calls does NOT yet exist (new table).
  • pg_net is AVAILABLE but NOT installed (only http 1.6 and pg_cron 1.6.4 are installed). Enabling pg_net is a build prerequisite (§2, open item #1).
  • Moelker demo: token demo-moelker-7f3afuneral_home_id e0f4a11e-f00e-4a11-b000-000000000001, contact_phone 226-883-0526, plan fwa_demo, metadata.staff present (array). This is the only launch- enabled tenant (§8).

2. Trigger → endpoint (covers voice + chatbot, zero voice-agent change)

Should see:

  • A Supabase trigger on AFTER INSERT ON voice_callback_requests that fires only when NEW.urgency = 'at_need' and NEW.vertical = 'funeral', and makes an async HTTP POST (via pg_net) to /api/funeral/at-need/alert-call with the new row's id + a shared internal secret header. (Because it keys off the row, it covers both capture channels — source='voice' and source='chat' — with no change to the voice agent or chatbot capture code.)
  • The endpoint validates the shared secret first (constant-time compare; reject with 401 otherwise) and then re-loads the callback row + the tenant's config + roster server-side from the DB using the row id. It never trusts phone numbers or tenant ids from the POST body — dial targets are derived server-side from the tenant's roster (§7 security; mirrors SECURITY P0 #1102's "don't trust client- supplied identity").
  • The endpoint checks, in order: global kill switch (AT_NEED_ALERT_CALLS_ENABLED) → per-tenant opt-inquiet-hours (if configured) → dial. If any gate is off, it no-ops gracefully (logs a skipped attempt with the reason; falls through to the existing SMS/email status quo) and never calls.

Should NOT see:

  • Any modification to voice-agent-livekit/, the SIP trunk ST_Xa3Bp9aixRFP, or a LiveKit dispatch rule (§0 hard boundary; verify via git diff --name-only).
  • The endpoint dialing a number taken from the request body, or acting on an unauthenticated request.
  • The trigger firing for non-at_need rows, non-funeral verticals, or on UPDATE.

Build prerequisite: enable the pg_net extension (founder-gated extension install). If pg_net is declined, the fallback is the already-installed http extension (synchronous — must be wrapped so a slow/failed POST cannot block the INSERT) or a pg_cron drain of a queue row; pg_net (async, non-blocking) is the recommended and founder-specified path.


3. The call + TTS summary script

Should see:

  • A Telnyx Call Control outbound call to the current on-call director's number (from the roster/notification allowlist, §7). Telnyx is already the funeral telephony provider; Call Control is a distinct API surface from the inbound SIP trunk (no trunk/dispatch involvement).
  • On answer, Telnyx speak (TTS) reads a warm, brief (~25s) summary built by a dedicated script-builder from the callback row: an at-need acknowledgment, the caller name, time of the call (tenant-local), and a short reason summary — no jargon, calm and clear, e.g. "This is Moelker Family Funeral Home's answering service with an at-need call. A family member, {caller_name}, called at {local_time} — {reason_summary}. Press 1 to acknowledge, 2 to hear this again, or 3 to be connected to the family now."
  • DTMF handling: 1 = acknowledge (records who acked, ends the escalation), 2 = repeat the summary, 3 = bridge/transfer the director to the family's number (the only path that ever dials the family — director-initiated, live).
  • Answering-machine detection (AMD): if a machine/voicemail is detected, leave the TTS summary as a voicemail and treat the attempt as NOT acknowledged (it still escalates per §4).

Should NOT see:

  • The script reading raw/verbose DB fields, PII beyond what the director needs, or anything that reads as an upsell / sales tone (this is grief-adjacent — funeralwiseai-vertical.md).
  • A voicemail counted as an ack; the family's number dialed by anything other than a director pressing 3 on an answered call.

4. Escalation state machine

Should see (verify behavior, not DOM/code presence — sample at ≥2 timepoints):

  • State per alert: initiated → dialing → {answered→acked | answered→no_ack | no_answer | machine→voicemail} → escalating → {acked | exhausted→fallback}.
  • Ack window: if the current director does not answer, or answers but does not ack within a configurable window (default ~45–60s), advance to the next director in the roster (funeral_homes.metadata.staff, ordered by the tenant's on-call config).
  • Chain exhausted: when every roster entry has been tried without an ack, fall back to the existing SMS/email status quo notification and set a failed- ack state that surfaces in the funeral admin "Needs a director" queue (§5).
  • First ack wins: the moment any director acks (DTMF-1), escalation stops and no further directors are dialed.
  • Every transition is logged to at_need_alert_calls (§6) with a timestamp.

Should NOT see:

  • An escalation that never terminates (must exhaust the chain and fall back).
  • Two directors bridged to the family at once; a fallback that fires while an ack is still pending inside the window; a silent dead-end with no failed-ack surfaced.

Configurable (per tenant, sensible defaults): ack window seconds, roster order, max attempts per director, whether to loop the roster once or stop after one pass.


5. Dashboard surfacing (honest, never silent)

Ties into the command-center At-Need screen (funeral-dashboard-command-center.md §5.1):

Should see:

  • On the at-need item, an alert-call status that is honest and specific: called (which number/director, when), acknowledged by {director name} (when), escalated (how many directors tried), or failed — no ack (chain exhausted, fell back to SMS/email).
  • Status conveyed by text + icon, never color alone (AODA — command-center §9); a failed-ack at-need item is visibly flagged in the "Needs a director" queue.
  • A director can see the attempt timeline (each dial, its outcome, the ack) from the at-need drawer.

Should NOT see:

  • A fabricated "notified" state; a failure that does not appear anywhere (failures are never silent — honest-metrics contract, command-center §6).

6. at_need_alert_calls log table (NEW — no FK, crisis_events shape)

Should see:

  • A new table modeled on crisis_events (products/funeralwiseai/safety-logging-design.md): a plain tenant_id uuid (NO foreign key) + vertical text, so a funeral (or later church/vet/realtor) tenant id is never schema-blocked. Do NOT add any FK to churches/organizations (that is exactly the defect the safety-logging design retires).
  • Columns (proposed — confirm in Stage-2): id, tenant_id (no FK), vertical, callback_request_id (the source voice_callback_requests.id), source (voice|chat), director_name, director_phone, attempt_number, telnyx_call_control_id, outcome (acked|no_answer|no_ack|voicemail| bridged|skipped|failed), dtmf_pressed, acked_at, escalated_to_next_at, voicemail_left, error, created_at, updated_at.
  • Written by the endpoint / escalation state machine (service role), one row per attempt/transition; RLS consistent with the other tenant-scoped tables (service-role writes; per-tenant reads scoped by tenant_id).

Should NOT see:

  • An FK on tenant_id; PII beyond operational need stored in the clear (redact/omit the family's details beyond name + reason summary if the founder prefers — Stage-2 open item #5).

7. Config & safety controls

Should see:

  • Per-tenant opt-in, default OFF for every tenant (including Kolssak demo-kolssak-51cb — untouched, never dialed). Stored on premium_funeral_homes.metadata (e.g. metadata.at_need_alert_calls.enabled).
  • Global env kill switch AT_NEED_ALERT_CALLS_ENABLED — off ⇒ the endpoint no-ops for all tenants (belt over the per-tenant switch).
  • Launch state: the Moelker demo demo-moelker-7f3a (tenant e0f4a11e-…-000000000001) is the ONLY tenant enabled at launch, dialing the founder's cell 226-883-0526 (the demo's real on-call director).
  • Dial allowlist (hard): the system may ONLY dial numbers from the tenant's configured roster (funeral_homes.metadata.staff) / notification number (premium_funeral_homes.contact_phone). It NEVER dials the family's number — the sole exception is an explicit DTMF-3 bridge initiated by an already-answered director. The allowlist is enforced server-side from the DB, never from the request body.
  • Quiet hours: none by default (at-need is 24/7); per-tenant configurable if a tenant wants them.
  • CASL: this is a transactional operational alert to the account's OWN staff (the funeral home's own on-call directors) — no CASL implications (not marketing, not to the public). The spec states this explicitly so no agent adds a consent gate that would delay an at-need alert.

Should NOT see:

  • Any tenant other than Moelker enabled at launch; any path that dials the family outside the director-initiated DTMF-3 bridge; a dial target sourced from the POST body; Kolssak dialed or referenced.

8. Church / vet / realtor regression gate

  • RG-1 — For a church or vet tenant, the feature is entirely absent: the trigger does not fire (vertical guard + opt-in default OFF), no call is placed, and no at_need_alert_calls rows appear. Verify a church demo callback insert produces zero alert-call activity.
  • RG-2 — The capture path (voice_callback_requests INSERT) behaves exactly as before for every vertical; the trigger is additive and gated.
  • RG-3 — No change to voice-agent-livekit/, moderation.py, the SIP trunk, or dispatch rules (diff-verified) — church/vet/realtor voice lines are provably untouched.

9. Cross-vertical design note (funeral is the first consumer)

The core — trigger → outbound call → ack → escalate → log — must be designed vertical-generic so other verticals can adopt it later without a rewrite:

  • Church — a pastoral_emergency callback (the value already exists in voice_callback_requests.urgency) alerting an on-call pastor.
  • Vet — an after-hours animal emergency alerting the on-call vet.
  • Realtor — a hot-lead "call me now" alerting the on-call agent (speed-to-lead).

Requirements for genericity: key the trigger/endpoint on (vertical, urgency) pairs (config-driven, not funeral-hardcoded); source the roster + opt-in from the per-vertical tenant tables via a small adapter; keep at_need_alert_calls tenant-generic (tenant_id + vertical) so it serves all consumers. Funeral is the first and only wired consumer in this MVP — build the seam, don't build the other verticals yet.


10. Verification (per the Verification Ladder)

Behavioral verification; evidence attached; never "build passes".

Unit / integration:

  • Escalation state machine — table-driven tests over every path (answer→ack, answer→no-ack→next, no-answer→next, machine→voicemail→next, chain-exhausted→ fallback, first-ack-wins-stops-chain, kill-switch/opt-in/quiet-hours no-op).
  • Script builder — produces a warm ~25s summary from a callback row; includes caller name / local time / reason; no PII overreach; deterministic; handles missing fields gracefully.
  • Dial allowlist — a target not in the tenant roster/notification set is rejected; a POST-body-supplied number is ignored.
  • Auth — the endpoint 401s without the shared secret.

End-to-end (the real test):

  • A REAL outbound call to the founder's cell 226-883-0526 via the Moelker demo (demo-moelker-7f3a) — insert an at-need voice_callback_requests row for the Moelker tenant and confirm: the phone rings, the ~25s summary reads, DTMF-1 acks (verify at_need_alert_calls records the ack + the at-need item shows "acknowledged"), no-ack escalates to the next roster entry, and chain exhaustion falls back to SMS/email + a failed-ack flag. (Founder is the demo's on-call director, so this dials only the founder — no third party involved.)

Regression:

  • Church + vet callback insert ⇒ feature absent (§8).
  • git diff --name-only shows nothing under voice-agent-livekit/ or touching the SIP trunk / dispatch rules.

Observability:

  • Every attempt visible in the dashboard; no silent failure (§5).
// state machine
test.fixme('answer+DTMF1 => acked, escalation stops, at_need_alert_calls records ack + director', () => {});
test.fixme('no answer within window => dials next roster director', () => {});
test.fixme('answering machine => leaves voicemail, does NOT count as ack, escalates', () => {});
test.fixme('chain exhausted => falls back to SMS/email status quo + failed-ack in Needs-a-director queue', () => {});
test.fixme('first ack wins: a later director is never dialed once one acks', () => {});
// gates + security
test.fixme('global kill switch off => endpoint no-ops for all tenants', () => {});
test.fixme('tenant opt-in default OFF => no call (incl. Kolssak)', () => {});
test.fixme('endpoint 401s without the shared secret; ignores body-supplied phone/tenant', () => {});
test.fixme('dial allowlist: only roster/notification numbers; family number only via director DTMF-3 bridge', () => {});
// boundary + regression
test.fixme('git diff touches nothing under voice-agent-livekit/ or the SIP trunk/dispatch rules', () => {});
test.fixme('church/vet at-need-equivalent insert => feature entirely absent', () => {});
// e2e (Moelker demo, real call to 226-883-0526)
test.fixme('at_need insert for Moelker demo => phone rings, ~25s summary, DTMF-1 ack recorded + surfaced', () => {});

11. Open items (Stage-2 founder validation)

#QuestionBlocksPriority
1Enable pg_net? It is available but not installed. Confirm enabling it (recommended, async) vs. the http-extension or pg_cron-drain fallback.§2P0
2On-call roster shape — confirm the exact funeral_homes.metadata.staff structure (fields: name, phone, on-call order/flag) and how "current on-call" is chosen (rotation? all-in-order?).§4, §7P0
3Ack window + roster behavior defaults — confirm ~45–60s, one pass vs. loop, max attempts per director.§4P1
4Telnyx Call Control config — which Telnyx number/connection places the outbound call; AMD settings; TTS voice/locale for speak.§3P1
5PII in at_need_alert_calls — store caller name + reason summary, or redact to a minimum? Retention period?§6P1
6at_need_alert_calls columns + RLS — confirm the proposed columns and the per-tenant read policy.§6P1
7Fallback definition — confirm the exact existing SMS/email "status quo" path the chain falls back to (so we call it, not reinvent it).§4P1
8Bridge (DTMF-3) semantics — connect director↔family live; what happens if the family's line is busy/no-answer?§3P2
9Cross-vertical seam now vs. later — build the (vertical, urgency) config seam in this MVP (funeral-only wired) or defer?§9P2

12. Guardrails for agents building against this spec

  • Rule #17 not satisfied — do not build until founder approval.
  • HARD BOUNDARY — do NOT touch voice-agent-livekit/, the SIP trunk ST_Xa3Bp9aixRFP, or any dispatch rule. Telnyx Call Control ≠ SIP trunking; there is no LIFE-SAFETY voice deploy in this feature. Diff-verify the boundary.
  • Trust the secret, not the body — validate the shared internal secret; derive every dial target server-side from the tenant's roster; never dial a body-supplied number (SECURITY P0 #1102 posture).
  • Dial allowlist is absolute — roster/notification numbers only; the family's number is dialed ONLY via a director-initiated DTMF-3 bridge.
  • Default OFF everywhere — per-tenant opt-in default OFF + global env kill switch; Moelker demo is the only launch-enabled tenant; Kolssak never dialed or referenced.
  • No FK on at_need_alert_calls.tenant_id — reuse the crisis_events no-FK tenant_id + vertical shape (safety-logging-design.md).
  • Verify DB columns before use (Rule #18) — the §1 facts were verified; re-verify metadata.staff structure before coding against it.
  • No new email/SMS send path — the fallback uses the EXISTING notification path; the only new outbound is the operational phone call. No cron may originate a campaign send (portfolio P0).
  • Honest observability — every attempt in the dashboard; no silent failure.
  • Verify on the real host / real call — the e2e is a real call to the founder's cell via the Moelker demo; evidence attached.
  • If code diverges, update this spec first (founder approval), then the code.

End of spec. STATUS: DRAFT — NOT APPROVED.