40. Facility Pipeline — signal-driven business development
Date: 2026-06-17
Status
Proposed
Context
ADR-0033 gave us a CRM pipeline (crm_leads + crm_activities, the /admin/crm Kanban) and a
won→client conversion. ADR-0023 added prospect fields to facilities (CMS Five-Star, beds,
affiliation) and we imported 576 NY prospect facilities. What we still lack is a way to work that
prospect universe systematically: a reason to call a given facility today, a priority order, and an
automated follow-up rhythm. Business development is currently manual and memory-driven.
The domain gives us a strong, time-bound buying signal: when a skilled-nursing facility posts a job for an MDS Coordinator/Assessor, PDPM/RAC nurse, chart reviewer, or QM clerk, it has a gap in exactly the function Nightingale sells. Those postings are public and, from compliant sources, cheap to collect. Layer on the public CMS quality data (low QM/Staffing star, recent F641 citations) and the NY Medicaid case-mix transition (which makes MDS accuracy drive Medicaid rates from ~fall 2026), and we can score and tier every facility, then drive a research-backed outreach cadence.
We considered three shapes: (a) a standalone "Facility Pipeline" app area parallel to /admin/crm;
(b) bolting signals onto the existing CRM with no new surface; (c) extending the existing CRM/BD
surface with new tabs and a scoring/ingestion/follow-up layer. A multi-agent design review (recorded
in docs/facility-pipeline/02-review-synthesis.md) rejected (a) as a redundant third BD nav item and
rejected (b) as too thin to run a real cadence.
Decision
-
Extend the existing BD surface, don't compete with it. The feature lives under the current "Business development" nav group and reuses
crm_leads/crm_activitiesand the/admin/crmKanban (ADR-0033). New tabs: Signal Inbox, Today's Follow-ups, Sequences; the existing board and facility detail are reused. No new "Facility Pipeline" nav item competing with "Pipeline". -
Three additive layers (migration
0053_facility_pipeline.sql):- Signal ingestion —
job_raw(append-only landing) +job_signals(normalized, deduped, entity-resolved to a facility; unresolved → a manual-match queue). Afacility_aliasestable (mirroringbilling_payersfrom 0049) keeps the manual queue small. Ingestion runs through a cron-secret-gated route + ascripts/ingest-job-signals.mjsCLI, writing un-actioned rows for human review (per ADR-0024). Sources are compliant-only (CareerOneStop free API, schema.orgJobPostingon robots-allowed career pages, optional licensed vendor APIs); never Indeed or LinkedIn scraping. We store the existence of a posting, never candidate PII. - Scoring —
facilities.pipeline_score(0-100) +pipeline_tier(1-3), recomputed in puresrc/lib/pipeline/score.tson ingest and on quarterly CMS refresh, written under an optimistic-concurrency guard (score_updated_at). The model is role-weighted on the posting and deliberately does not double-count the CMS overall star (a composite of QM + staffing — we score the domain stars directly); weights sum to 1.0; tier cutoffs are calibrated against the cohort distribution, not hard-coded. - Follow-up automation —
follow_up_sequences/steps/enrollments/tasksdrive a cadence (seeded "NY SNF 21-Day", 9 touches). A daily cron materializes due tasks idempotently (unique (enrollment_id, step_no)); email steps are drafted via the AI orchestrator (ADR-0026) intooutbound_emailsand require human send (no auto-send), consistent with the welcome-email seam (ADR-0035). LinkedIn steps are manual-only and never auto-actioned.
- Signal ingestion —
-
Active clients are not prospected. Ingestion checks
facilities.status: a signal on anactiveclient routes to an expansion/upsell track assigned to the account owner, not a new-logo prospect lead. A separate Prospect-Discovery view surfaces high-score facilities that have no open posting (CMS-only signals), so the non-signal path is covered. -
Security per ADR-0004. All writes go through
'use server'actions that verify the caller viagetCurrentAdmin, Zod-validate input, and scope every query bycompany_id. Machine-churn tables (job_raw,job_signals) are notapp.audit_row()-audited (dailylast_seen_atrefresh would floodaudit_log; provenance lives injob_raw); the relationship tables (follow_up_*,facility_aliases) are audited likecrm_leads. Pure logic insrc/lib/pipeline/*is unit-tested.
Consequences
- The 576 NY prospects become a worked, prioritized queue with a daily worklist and an audit trail of every touch — the central goal.
- New optional env vars (
CAREERONESTOP_API_KEY, etc.) must be.optional()insrc/server/env.tssonext builddoes not fail without them; they are server-only (guardrails forbidsNEXT_PUBLIC_*SECRET). src/db/types.tsmust be regenerated after the migration; thefacility_contactsrole Zod schema must widen to the new vocabulary.- Scoring is only as good as the CMS data loaded. Loading the CMS Provider Information dataset (free,
quarterly) to stamp
cms_provider_numberis strongly recommended — it both feeds the score and makes entity resolution near-exact (CCN keys), shrinking the manual-match queue. - Cadence throughput is bounded by team size; the spec caps active Tier-1 enrollments per rep and uses a condensed Tier-2 nurture so a small team is not swamped.
See docs/facility-pipeline/ for the full design spec, review synthesis, implementation plan, UX
spec, and annotated mockups.