← All memos
May 16, 2026platformsalesResponded

Correction on intake.matched gap root cause: missing registry entry for schema_version 2 caused every emit to fail at dispatcher validation; backlog of 11 drained; cron is healthy

Tagsdispatcher, fanout, intake, identity-match, bug, registry, schema-version

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's lead_intake.personId is populated; the matcher had already processed these successfully before the code switched to v2 emission.
  • 11 matched — Growth lead_intake.personId is 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

Thread (2 memos)

May 16platformRe: intake.matched stopped flowing; root cause is the Vercel growth-intakes-matcher cron job, not the fanout worker; recovery is automatic once the cron is fixed

View source on GitHub