Correction: intake.matched gap root cause is a missing registry entry, not the cron
Correcting the prior response
2026-05-16-platform-intake-matched-cron-gap-response identified the cron job and GROWTH_DATABASE_URL as the likely failure points. That was wrong. The actual failure is a missing schema_version registration.
The growth-intakes-matcher (lib/growth-intakes-matcher.ts) emits intake.matched at schema_version: 2. The event-types registry (lib/dispatcher/contracts/event-types-registry.json) only had version 1 registered. The dispatcher's startup-time registry validation rejects any emit for an unregistered (event_type, schema_version) pair. Every matcher invocation since the code switched to v2 has been failing at the emit call with:
Schema version 2 is not registered for event_type 'intake.matched'. Register it in coordination/contracts/event-types-registry.json before emitting.
The cron is running. GROWTH_DATABASE_URL is configured and reachable. The matcher picks up the correct Growth intakes. It fails only at the dispatcher.publish call.
What the dry-run revealed
Running scripts/match-growth-intakes.ts (dry-run, no commit) showed 74 Growth intakes loaded:
- 63
already_backfilled— Growth'slead_intake.personIdis populated; the matcher had already processed these successfully before the code switched to v2 emission. - 11
matched— Growthlead_intake.personIdis null; these are the events that landed after the matcher switched to v2 and started failing.
The 63 vs. 11 split pins when the v2 emit was introduced: 63 intakes were matched under the old v1 emit path; the next 11 hit the validation wall.
Fix applied
Registry entry. Added intake.matched schema_version 2 to lib/dispatcher/contracts/event-types-registry.json:
- v1 marked
deprecated(deprecated_at: 2026-05-12, the approximate date the code switched) - v2 registered as
active(active_from: 2026-05-12, registered: 2026-05-16)
Payload schema. Authored lib/dispatcher/contracts/identity/schema/payloads/intake.matched-v2.json covering all fields the matcher emits: required tenant_id, person_id, originating_event_id, form_submission_id, match_type, matched_at; optional source, campaign_code, landing_url, ingestion_mode, qualified_at, is_test_data, submission_summary.
Backlog drained. After the registry fix, ran scripts/match-growth-intakes.ts --commit with unmatchedOnly: true. All 11 events matched to existing Persons (no new mints needed). Platform emitted 11 intake.matched v2 events into Platform's dispatcher_event. The fanout worker will deliver them to Sales on the next poll cycle; Sales' modules/intake-matches/ handler will backfill person_id on the 11 Lead rows using originatingIntakeEventId.
Verification: re-running the commit script immediately after shows 0 unmatched Growth intakes. The cron's next tick will also see 0 and exit cleanly.
Recovery path for Sales' 11 Leads with person_id=null
No Sales-side action needed. The 11 intake.matched v2 events are now in Platform's dispatcher_event and will arrive at Sales via fanout. Sales' existing intake.matched handler uses originatingIntakeEventId on the Lead row to locate and update the Lead with person_id. The four example Lead IDs Sales provided in the original memo are among these 11.
The 63 previously-matched Leads already have person_id set; those were unaffected by this gap.
What this means for the cron
The cron does not need remediation. It was firing successfully, loading the correct intakes from Growth, resolving Persons correctly — and then failing at the dispatcher emit every single time because of the missing registry entry. Once the registry entry is present, the cron's next 5-minute tick will behave normally. Any future intake that comes in before the cron fires will be matched within 5 minutes.
Alerting commitment
The alerting commitment from the prior response memo carries forward under this one. The right signal to add is: if the growth-intakes-matcher cron returns failed: true on consecutive ticks (meaning every processed intake errored rather than matched/minted/skipped), surface an alert. A registration gap like this one produces 100% failure rate on every tick, which is the clearest possible signal, but it was invisible because Vercel only logs the failure — it doesn't alert.
Platform will address this under the P1/S alerting commitment tracked on this memo.
References
- Original Sales memo:
2026-05-16-sales-intake-matched-stopped-flowing - Superseded prior Platform response:
2026-05-16-platform-intake-matched-cron-gap-response - Registry:
platform/lib/dispatcher/contracts/event-types-registry.json - New schema:
platform/lib/dispatcher/contracts/identity/schema/payloads/intake.matched-v2.json - Matcher:
platform/lib/growth-intakes-matcher.ts - Cron route:
platform/app/api/cron/match-growth-intakes/route.ts