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)
| Cause | Effect |
| No deterministic parser for AppFolio "New Lead" notification emails — phone capture relied on an LLM extractor | Phone silently lost at intake |
| An Aug 3 extractor prompt rule ("only the sender's own number") suppresses the renter's number in relayed emails | LLM 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 again | Email-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 termsIf 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 termsSome 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).