Lead Pipeline Audit — August 2026

Status: Shipped 2026-08-09 — the incident fixes + the three starred intake bugs are merged and verified on live production records. Published 2026-08-08, updated 2026-08-09.

What triggered this

Two real Zillow leads (Jul 26 and Aug 7) landed in PropFlow with no phone number, a blank Clara summary, and all five follow-ups going out by email — even though both renters gave their phone number, and it sat in AppFolio's guest card the whole time. 2 of the 15 Zillow/Apartments.com leads we've ever received were stranded this way.

In plain terms

When a renter asks about an apartment, their info reaches us twice: once as a notification email, once as a structured record from AppFolio. If the email arrives first, we created the lead from the email — badly — and then refused to let the good AppFolio data fix it. Those renters never got a text, only emails, and the PM's page showed almost nothing about them.

Root causes of the incident (fixes in flight)

CauseEffect
No deterministic parser for AppFolio "New Lead" notification emails — phone capture relied on an LLM extractorPhone silently lost at intake
An Aug 3 extractor prompt rule ("only the sender's own number") suppresses the renter's number in relayed emailsLLM fallback also returns nothing
Guest-card sync never writes a phone onto an existing person (old consent rule; Fede ruled guest-card sources collect consent at submit, so fill-if-empty is approved)The correct structured phone can never repair the record
Clara's summary only generates on a tour-shaped email for a brand-new prospect — never againEmail-only leads keep a blank summary forever

The audit: 16 further confirmed defects

A 23-agent sweep over intake, identity, sync, cadence, and UI; every finding independently re-verified against the code by an adversarial reviewer. 18 candidates → 16 confirmed, 2 refuted. The three starred intake bugs joined the current fix branch; the rest need owners.

HIGH ★ Phone-only leads lose everything at the Lambda boundary

lambda/inbound-processor/handler.ts:621

All parsed lead fields (name, phone, source) are discarded together whenever the lead has no email address — even though the parser deliberately admits phone-only leads and the queue payload carries the data.

In plain terms

If a renter gives a name and phone but no email, we throw away the name and phone too. The lead is created knowing nothing about them.

HIGH ★ Zillow-relay leads can get no reply at all, and share one conversation

agents/clara/lib/email/process-inbound-message.ts:636

The aggregator fast path ignores the Reply-To header the slower path honors. Clara's reply is then addressed to the no-reply relay, blocked as undeliverable, and marked COMPLETED — a silent no-reply to a real lead. All leads from one relay also collapse into a single conversation.

In plain terms

Some renters who wrote in through Zillow never heard back — and our system recorded it as handled. Different renters' messages were also filed into one shared thread.

MED ★ The generic phone fallback can grab the wrong number

agents/clara/lib/email/parse-lead-source.ts:413

Emails are filtered against relay domains but phone numbers are not: the first phone-shaped string in the body wins — often the property's or aggregator's own footer number, saved as the renter's.

HIGH A stranger's first text can attach to someone else's lead

src/lib/domain/identity/resolve.ts:462-484

When exactly one prospect has a pending tour, an unknown inbound SMS is assumed to be them and attached — merging two different people's identities and conversations.

HIGH Guest-card group stamp can demote a primary applicant

src/lib/domain/pms/writers/guest-card.ts:624-659

The group link is stamped without the verdict check the application writer uses, so a prospect's own application-group reference can be overwritten and a primary applicant marked non-primary.

HIGH Confirming a tour blanks the detail page

leasing/prospects/[id]/ProspectDetailClient.tsx (handleConfirmTour)

The page replaces its enriched prospect object with the bare PATCH response, which lacks the person join — the next render throws.

HIGH The operator's chosen touch channel is ignored

prospect-outreach-workflow.ts:319 → channel resolution

Workflows pass the configured per-touch channel preference, but the resolver never reads the field — every touch uses the global default instead of what the operator picked.

MED 12-hour cross-path cooldown silently swallows scheduled touches

src/lib/domain/leasing/autonomous-outreach-cooldown.ts:26

A fixed 12h cooldown shared across outreach paths suppresses a due cadence touch without recording an outcome — the strip says a touch is coming that already got eaten.

MED Dead-run repair re-sends touches the renter already got

outreach-reconcile.ts:353-374

Re-enrollment after a stalled run resets the cadence anchor without excluding prospects with already-sent touches — replaying touch 1 at a renter who received it weeks ago.

MED Guest-card poll can mint a permanent duplicate lead

src/lib/domain/pms/writers/guest-card.ts:452-455

The poll snapshots all prospects once per run; a lead Clara creates mid-run is invisible to the dedupe, so the same person gets a second prospect card.

MED Sync hash stamped before the name-heal runs

src/lib/domain/pms/writers/guest-card.ts:772-791

The content hash that suppresses re-processing is written even when the backfill step didn't run — permanently skipping a card whose heal never happened.

MED Unparseable phone silently drops a phone-only guest card

src/lib/integrations/appfolio/pms-adapter.ts:1277-1281

If the number can't be normalized to E164 it becomes undefined with no log — for a phone-only lead that's the whole contact record gone.

MED Tour pipeline skips the conversation-linked dedupe

src/lib/domain/leasing/tour/process-tour.ts:253, 426-460

conversationId is in scope but never passed to identity resolution, disabling one dedupe signal and leaving the new row's conversation foreign key null.

MED Prospects list sorts on a different date than it filters

src/lib/domain/leasing/load-prospects.ts

The window filter uses inquiryDate while sort/render use createdAt — a row can appear inside a window whose displayed date disagrees with it.

LOW Subject-word routing misattributes lead source

agents/clara/lib/email/parse-lead-source.ts:154

A bare "zillow" substring in any subject forces Zillow parsing and source attribution on ordinary prospect mail.

LOW Self-healing personId stamp is silently discarded

src/lib/domain/identity/resolve.ts:532-547

The repair write for a matched prospect missing its personId is dropped by updateProspectFields' field whitelist — the heal never lands.

What happens next

SHIPPED 2026-08-09  The fix landed after eight adversarial review rounds (which caught further real bugs, including three introduced by fix iterations — all pinned by tests). Proof: a live end-to-end harness on The Willows (63 scenarios, 0 failures, including exhaustive prospect-reply coverage) and a replay of all 253 production leads ever received — zero fields lost, 18 leads gained a field. Verified on live records: both stranded leads now carry their real phone and their follow-ups resolve to SMS. Also settled by measurement: a clean prospect-facing subject and a unified thread in the property mailbox are mutually exclusive on Exchange (it groups by subject), so the mailbox shows the notification and the conversation as two items — working leads from the PropFlow conversation view dissolves this entirely.

Update 2026-08-10 — all 13 remaining audit bugs are FIXED and merged (four PRs: identity, guest-card sync, outreach cadence, UI/parsing). A prod-evidence sweep also answered "did these ever fire in real life": only the stranger-SMS identity merge ever touched a real renter (once, 2026-07-13, self-corrected in ~2.5 minutes, no residue — and that heuristic is now deleted outright per Fede's ruling: unknown inbound contact always mints a new identity, merges must be deliberate); the prospects-list date mismatch was display-only (160 rows rendering the May 29 backfill clock — the fix corrects the display retroactively); everything else fired only on sandbox leads or never fired. No prod records needed repair. Still open: DMARC enforcement on the relay path is logging-only until a clean baseline (one marked line to flip).

PropFlow Docs