Weekly agent performance reports
Moves the weekly agent performance report pipeline from Rails/GoodJob to
native API2/BullMQ ownership: the top-level schedule, per-organization
fan-out, report lifecycle, content generation, citation persistence, and
delivery side effects all execute in api2 against Postgres. Unlike the
routine-workload migration, no Rails /internal/* domain endpoints execute
this work — report preparation, OpenAI content generation, citation writes,
and Resend email delivery are implemented natively in
src/lib/agent-performance-reports/.
Contract
- One BullMQ queue,
agent-performance-reports, carries six job kinds:generate-tick,generate-organization,content,preview-tick,preview-organization,send-organization. Two schedulers preserve the retired crons exactly: generation daily at 08:00 UTC (0 8 * * *) and preview Monday at 09:00 UTC (0 9 * * 1). - The generate tick resolves every org’s current and reportable reporting
windows natively — org timezone,
agent_performance_reporting_week_start_day(integer enum, Sunday default), and the 48-hour defaultagent_performance_reporting_resolution_hours— throughresolveWindowsForOrganizations, and persists the resolved windows in the tick job’sresolvedWindowsdata before the first fan-out so a BullMQ retry cannot re-resolve a drifting window. - Deterministic job IDs coalesce duplicate schedules and retries:
apr-generate-<org>-<start>-<end>-<type>for per-org generation,apr-content-<report>for per-report content,apr-preview-org-<org>-<scope>/apr-send-org-<org>-<scope>for delivery. Every completed step is written back viajob.updateData, so a retry resumes after the last durable marker instead of re-creating reports, re-enqueueing content, or re-sending email. - Per-organization failures are isolated in each tick: a broken org is logged and reported to Sentry without blocking the remaining fan-out, per the api2 background-job isolation rule.
- Report rows, report-conversation links, and citations are written
natively. Existing reports for the period are deduped by
findReportForPeriod;failedreports retry content on the same row,pending/in_progress/completedreports are skipped. Employees with no eligible conversations produce no report and get the weeklylatest_agent_performance_report_end_datecache clear. - Content generation calls the OpenAI Responses API directly with inline
report instructions and variables, model
(
gpt-4.1), strictagent_performance_resultJSON schema, metadata, and 240-second timeout as the Rails generator. Bot employees are skipped; failures mark the reportfailedand rethrow so BullMQ retries. - Preview and send emails are claim-guarded per report through
public.outbound_delivery_claims(apr-weekly-preview:<scope>:<report>/apr-weekly-email:<scope>:<report>). A proven non-delivery releases the claim so a retry re-sends; an ambiguous outcome staysuncertain, is surfaced to Sentry, and is never resent automatically. Sent reports getemails_sent_at, and the period-levelemails_already_sent_for_periodgate keeps a re-fired tick from scheduling a second auto-send. AgentPerformanceReport#generate_later, the admin generate/send/preview actions, and theagent_performance_reportsrake tasks enqueue throughApi2AgentPerformanceReportEnqueueronto the API2 queue — Rails produces no GoodJob rows for these paths. The legacy job classes stay deployed for already-enqueued rows and rollback.- The two retired cron entries remain in
config/initializers/good_job.rbwithenabled_by_default: falseand apause_api2_*kill switch for a non-racing code revert.
Rollback and rollout assertion
The Flipper kill switch pause_api2_agent_performance_reports
(owner: platform-eng; created 2026-09-09; review by 2026-12-09) stops the
API2 ticks and per-org workers immediately — queued BullMQ jobs complete as
skipped/paused rather than racing a reverted Rails path. Content jobs
intentionally do not check the flag: generation executes natively in api2
either way, so a paused flag cannot strand pending reports.
Rollback is a two-step, non-racing revert:
- Enable
pause_api2_agent_performance_reports. - Re-enable
generate_weekly_agent_performance_reportsandsend_weekly_agent_performance_emails_previewin the GoodJob dashboard (or revert theenabled_by_default: falselines).
Report rows created under API2 carry identical status/generate_later
semantics, so the Rails path resumes cleanly; a re-delivered period is
deduped by the emails_already_sent_for_period gate and per-report
emails_sent_at.
Re-driving work skipped while paused
- Generation — enqueue one
generate-organizationjob per reportable org viaPOST /internal/agent_performance_reports/enqueuewith the window dates, or revert toGenerateWeeklyAgentPerformanceReportsJob.perform_lateronce the cron is re-enabled. Rails-side re-drives still enqueue content on API2 throughgenerate_later, so a mixed run cannot produce a GoodJob content row. - Preview/auto-send —
POST /internal/agent_performance_reports/enqueue_previewre-runs the tick natively;enqueue_sendwithauto_scheduled: falseperforms a manual org send under a freshmanual:delivery scope. The claim keys on the window scope make a duplicate auto-send impossible. - Single report content —
report.generate_laterorPOST /internal/agent_performance_reports/enqueue_contentwithreport_id.
Production observation
Production evidence before cutover (EU+US, rolling 12h ending ~2026-09-09
10:13 UTC): GenerateWeeklyAgentPerformanceReportsForOrganizationJob had
53 executions and GenerateAgentPerformanceReportContentJob about 52;
including the top-level scheduler the theme was ~107 executions. After
deployment, require for a clean bake:
-
Zero new normal-path GoodJob rows for
GenerateWeeklyAgentPerformanceReportsJob,GenerateWeeklyAgentPerformanceReportsForOrganizationJob,GenerateAgentPerformanceReportContentJob,SendAgentPerformanceEmailsPreviewJob, andSendAgentPerformanceReportEmailsForOrganizationJob:SELECT job_class, count(*) FROM good_jobs WHERE job_class IN ( 'GenerateWeeklyAgentPerformanceReportsJob', 'GenerateWeeklyAgentPerformanceReportsForOrganizationJob', 'GenerateAgentPerformanceReportContentJob', 'SendAgentPerformanceEmailsPreviewJob', 'SendAgentPerformanceReportEmailsForOrganizationJob' ) AND created_at >= now() - interval '12 hours' GROUP BY job_class;Rows inside the 12-hour cleanup window created before the deploy are historical, not new traffic.
-
BullMQ acceptance and drain — schedulers
agent-performance-reports-generateandagent-performance-reports-previewproduce ticks, org jobs complete, and the queue depth returns to zero:SELECT state, count(*) FROM api2.bullmq_jobs -- BullMQ keyspace inspection via the queue dashboard WHERE queue = 'agent-performance-reports' GROUP BY state;(Equivalently, inspect
agent-performance-reports:*keys in the queue Redis or the Bull Board UI.) -
Report lifecycle parity — new
pending→completedtransitions onpublic.agent_performance_reportsfor the current windows, withagent_performance_report_conversationsandagent_performance_report_citationsrows populated:SELECT processing_status, count(*) FROM public.agent_performance_reports WHERE created_at >= now() - interval '24 hours' GROUP BY processing_status; -
No duplicate deliveries — every outbound claim row is either
deliveredor a singleuncertainsurfaced to Sentry; no report showsemails_sent_attwice for the same period:SELECT report_id, count(*) FROM public.outbound_delivery_claims WHERE delivery_key LIKE 'apr-weekly-%' GROUP BY report_id HAVING count(*) > 1;
Hold 24 clean hours across both regions — at least one completed daily generate tick and, on Mondays, the preview tick plus the 30-minute auto-send — before claiming ownership. Roll back at any sign of duplicate reports, duplicate emails, or unclaimed manual sends.