Skip to content

Per-team daily responder report

The original daily metrics email. One email per team per day, sent to the per-team recipient list, summarizing yesterday's voice-agent activity for that team.

This is the comp for the EAS enterprise daily report. The enterprise variant mirrors this surface's shape; if a structural change happens here, the enterprise variant probably needs the same change.

Audience and cadence

  • Recipients: per-team via coach_config.email_recipients.
  • Schedule: cron fires daily at 4 AM ET, but per-team delivery is shifted to that team's configured local hour (default 9 AM in the team's timezone via coach_config.report_delivery_time_local).
  • Active team set: RESPONDER_DAILY_REPORT_TEAM_IDS = [1006] as of main on 2026-05-21. Single team in this list today.
  • Disabled list: teams [204, 441] are hardcoded skip targets in send_batch_responder_email_reports.

What's in the email

Subject: Avoca Voice Dashboard Daily Report - {teamName}

Body: seven summary rows (no per-shop tables since this is single-team):

RowSource
All Calls (Human + AI)COUNT(*) FROM calls WHERE team_id = $1 AND display = true AND ...
Human Callstotal_calls - ai_calls
AI CallsCOUNT(*) FILTER (WHERE ai_or_human = 'AI')
LeadsCOUNT(*) FILTER (WHERE is_bookable = true), with (leadRate%)
Booked CallsCOUNT(*) FILTER (WHERE is_booked = true AND is_bookable = true), with (bookingRate%)
Recovered (team-marked)COUNT(*) FILTER (WHERE is_booked = false AND is_recovered = true)
TransferredCOUNT(*) FILTER (WHERE is_transferred = true)

leadRate = bookable_calls / total_calls. bookingRate = booked_calls / bookable_calls.

Date range: yesterday's midnight-to-midnight in the team's timezone.

Architecture

This is the canonical five-stage pipeline documented under Shared architecture. Per-stage file references for this surface:

StageFile / function
Cron + schedulerlib/inngest/functions/coach-report-schedule.ts:scheduleEmailReports (per-team loop near line 300)
Inngest eventresponder.send-daily-report
Handlerapp/api/inngest/route.ts:sendResponderEmailReport
Build + sendlib/email/email.ts:send_batch_responder_email_reports (line 193)
Metrics SQLlib/email/responder-report.ts:getResponderReportForTeam
Templateemails/coach/CoachReportEmail.tsx

Why CoachReportEmail

The template name is historical. The Coach product (older) and Voice Dashboard responder reports share the template, the recipient table, and the scheduler function. They differ only in metrics and Inngest event name. See the shared architecture page for the naming inheritance.

Failure modes

  • No coach_config row OR empty email_recipients → team is silently skipped in the scheduler loop. No event emitted. No log.
  • Team in disabledTeamIdssend_batch_responder_email_reports skips during the build loop. No log.
  • Zero calls in the windowgetResponderReportForTeam returns undefined → team is silently skipped (no email, no log).
  • Resend errorsend_batch_responder_email_reports throws → Inngest retries the send step per its default retry policy. The team's email may eventually fail permanently.
  • Cron paused / errored in Inngest → no events emitted. Visible in Inngest dashboard, not in app logs.

Operational notes

Adding a team

  1. Add the team id to RESPONDER_DAILY_REPORT_TEAM_IDS in coach-report-schedule.ts. Open a PR.
  2. Make sure the team has a coach_config row with email_recipients populated and (optionally) report_delivery_time_local set.
  3. After PR merge, the next 4 AM ET cron fire will include the team.

Forcing a test send to one team

Use the Path A pattern: write a one-off script under apps/web/scripts/ that imports the build-and-send helper directly. The pattern from apps/web/scripts/send-eas-enterprise-report-test.ts applies; reduce reportParams to a single { teamId, emails: ['<your-email>'] }.

Direct example sketch:

ts
#!/usr/bin/env node
import './_load-env';
import { send_batch_responder_email_reports } from '@/lib/email/email';

async function main() {
  await send_batch_responder_email_reports({
    reportParams: [{ teamId: 1006, emails: ['sandy.corsillo@avoca.ai'] }],
    cronFireTime: new Date(),
  });
}
main().then(() => process.exit(0));

Caveat: this hits production Resend and queries production Supabase. Use a recipient you own. Don't point at customer addresses.

Test calls excluded

The display = true filter in getResponderReportForTeam excludes test phone calls and Hamming simulation calls from the metrics. If a future refactor loosens that filter, the per-shop totals will start including test traffic. Pin this in tests when modifying the SQL.