Western Slope inbox blast — 100 emails in 61 seconds

Clara sent Western Slope Property Management's own leasing inbox one "New application to review" notice for every pending application they have, all in a single minute this afternoon — and roughly 300 follow-ups are queued behind it.

2026-09-16 · production, org org_c42d9150 · STOPPED — switch pulled on all 70 properties, fix in PR #8935

The one thing

100emails into their real leasing inbox, 100 distinct applicants
61sfirst to last — 20:00:11 to 20:01:12 UTC
~300follow-ups still queued over the next three days

What happened

At 2:00 PM Mountain today the PM action-reminder walker took its hourly pass and opened a reminder cadence for every pending application in Western Slope's portfolio at once. Every cadence opens with an email titled "New application to review — <name>".

All 100 were distinct applicants — no repeats, so this is their entire application backlog arriving as one hundred separate emails, not a retry loop hammering one person.

It landed in their inbox, leasing@westernslopepm.com, not ours. About two hours later somebody on their side forwarded one of the notices out to an external broker, so a human over there has already seen it.

Send-side confirmation: every one of the 100 ingestion rows carries classificationReason = own_outbound with a parsed to of their address — our own mail, addressed to them, read back out of the mailbox we now have connected.

The clock

When (Mountain)What
15 Sep, 11:05Their real leasing mailbox is connected — live subscription, Clara reads and sends as that address.
16 Sep, 13:21PR #8885 removes the guard that had refused exactly this setup (a company's escalation contact being its own connected mailbox). Timing, not proven causation.
16 Sep, 14:00The blast Hourly walker fires. 100 opening notices in 61 seconds, all to the customer.
16 Sep, ~16:00Someone at their leasing inbox forwards one of the notices to an outside party.
17 Sep, 14:00Wave two ~100 "Still waiting on your review" emails — then again on the 18th and the 19th.

Why it went to them and not to us

Every PM notification picks its recipient by walking three rungs and taking the first one with an address in it. For almost every Western Slope property the first two rungs are blank, so everything falls through to the company-wide contact — which is their own leasing inbox.

RungFieldState on this accountResult
1renewalContactEmailNot set on any of their properties.Empty
2Property.propertyEmailSet on 1 of their 70 properties — the demo front door, pointing at us. The other 69 are blank (all 70 checked individually).Empty ×69
3Organization.
escalationOwnerEmail
Their live leasing inbox.All 100 land here

Resolver: resolvePmContactEmail in src/lib/domain/leasing/pm-contact-email.ts. Copy: pm-action-reminders/pm-action-reminder-copy.ts:148. Walker: pm-action-reminder-backstop.ts.

Switches we can pull

A cadence that is already running re-reads the per-property reminder switch on every tick and stops itself when it is off (stop_disabled in pm-action-reminder-logic.ts:211). So flipping that switch halts the 100 already in flight — nothing has to be killed by hand in Temporal.

Option A — turn off application reminders on their properties recommended

Option B — clear the company-wide escalation contact

npx tsx scripts/set-org-escalation-owner-email.ts --org <org-id> --clear --apply

Option C — do nothing

Checked and cleared

Three things that looked like the cause, or looked like more spam, and are not.

SuspectVerdictEvidence
The new company-mailbox poller (#8888, on by construction today)Not itAll 100 rows carry s3Key = webhook:… — the push lane. The poller never read these.
The harness-sink send guard (#8860)Not itIts rule is a local-part match on harness-owner[+tag]@propflowai.co; it does not touch the address on their demo property.
Renter-facing leasing emailCleanThree replies total, one each to three separate renters over two days (15th–16th). Texts: four, all to our own test number. No renter was spammed.
The earlier batch of application noticesClean128 sent 2–12 Sep went to our own sandbox inbox, never to the customer. Zero overlap with today's 100.

The actual defect

The routing explains which address it went to. It does not explain why it was allowed to send at all — and that is the part that matters, because the gate that should have stopped it already existed by then.

#8893 — "the company live switch holds every customer-facing email" — merged today at 22:15 UTC. The blast was at 20:00 UTC. Two hours and fourteen minutes earlier. Their company has never been switched on (Organization.claraLive absent), so that gate would have held all 100.

It did not, and it still would not tomorrow. The reminder send passes a hardcoded internalRecipient: true — and that is the master gate's first exemption, the caller's own declaration that "this is not a customer". The claim was true while every PM inbox was ours. It stopped being true the moment the chain started resolving to the customer's own address.

Fix in review: PR #8935 — derive the flag from the resolved address instead of asserting it. A PM inbox that really is ours stays exempt, exactly as before. Regression test proven to fail on the bug (exit 1, 2 failed) and pass on the fix (exit 0, 42/42).

Also worth fixing

Established entirely read-only against propflow-prod: no writes, no code changes, no deploys. Full working notes and query shapes are in the session scratchpad.

PropFlow Docs