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
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.
| When (Mountain) | What |
|---|---|
| 15 Sep, 11:05 | Their real leasing mailbox is connected — live subscription, Clara reads and sends as that address. |
| 16 Sep, 13:21 | PR #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:00 | The blast Hourly walker fires. 100 opening notices in 61 seconds, all to the customer. |
| 16 Sep, ~16:00 | Someone at their leasing inbox forwards one of the notices to an outside party. |
| 17 Sep, 14:00 | Wave two ~100 "Still waiting on your review" emails — then again on the 18th and the 19th. |
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.
| Rung | Field | State on this account | Result |
|---|---|---|---|
| 1 | renewalContactEmail | Not set on any of their properties. | Empty |
| 2 | Property.propertyEmail | Set on 1 of their 70 properties — the demo front door, pointing at us. The other 69 are blank (all 70 checked individually). | Empty ×69 |
| 3 | Organization. | 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.
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.
pmActionReminders.enabled = false (it has a UI toggle on the property's leasing settings, and an API route).critical, so expect ~100/day of alarm noise until the cadences burn out.npx tsx scripts/set-org-escalation-owner-email.ts --org <org-id> --clear --apply
Three things that looked like the cause, or looked like more spam, and are not.
| Suspect | Verdict | Evidence |
|---|---|---|
| The new company-mailbox poller (#8888, on by construction today) | Not it | All 100 rows carry s3Key = webhook:… — the push lane. The poller never read these. |
| The harness-sink send guard (#8860) | Not it | Its 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 email | Clean | Three 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 notices | Clean | 128 sent 2–12 Sep went to our own sandbox inbox, never to the customer. Zero overlap with today's 100. |
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).
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.