Escalation email cleanup & the Camellia review gate · two designs, one page

Retiring the Old Escalation Emails, and the Approval Gate for What Replaces Them

Right now, seven or eight different pieces of code can each independently decide to email a person about something that needs their attention — and most of them don't talk to each other, so the same issue can get emailed twice, or in the worst case, not emailed at all. This page has two parts: first, a plan to safely retire the old, disconnected emails now that a shared "did we already tell a human about this?" check exists; second, a design for a temporary Slack approval step at Camellia, so that while that shared check is brand new there, a person signs off on every escalation email before it goes out — not forever, just for the first stretch.

2026-08-30 · Proposed — pending Fede review (built overnight 2026-08-30) · Part 1 is the deprecation report for the old escalation emails; Part 2 is the Camellia bake-gate design. Neither part is built yet — this page lays out the plan and the decisions that need your call before either one ships.

The one thing to understand before anything else

There's a piece of code — call it "the shared door" — whose whole job is answering one question: has a human already been told about this? Some of the old, one-off emails already ask the door that question before sending. Others don't — they just fire, with no memory of what anyone else already sent. The plan below moves everyone onto asking the same door, and only removes the old fallback emails once we can see, with real numbers, that the door is catching everything they used to catch.

Part 1 — Retiring the old escalation emails

DEPRECATION PLAN

Six places in the code still send a "tell a human" email their own way instead of going through the shared door. This section is the case-by-case plan for each one: what's replacing it, what it's still the only safety net for, and in what order it's safe to remove.

The rule everything below follows

Nothing gets deleted before its replacement is proven to be catching the same cases, live. A property going quiet — a resident's message reaching nobody — is the one failure this whole effort exists to prevent. So every step below is small, reversible on its own, and checked with real traffic numbers before the next step happens.

In plain terms

Think of it like turning off a backup generator only after you've confirmed the main power has been reliably on for a while — not just installed.

The six old-path emails, one by one

What it does todayWhat replaces itRisk of turning it off now
1 · Forward-to-PM emailClara hits a wall (rejected ID check, missing capability, out-of-scope ask) and emails the property's contact directly.Already replaced — the new tracked-request path takes over automatically for any property that has a "decision owner" person configured. The old email only still fires where that hasn't been set up.HIGH today, for properties without an owner set — which is most of them. That's their only path to a human.
2 · "Get me a person" escalationClara's highest-traffic escalation tool — anything from "the resident wants a callback" to a gas-leak report.Life-safety half: nothing replaces it, ever — stays a direct, instant, ungated email on purpose. Routine half: this is the biggest real gap in the whole system — it isn't wired to the shared door at all, and its own duplicate check is much weaker (just "is this conversation already flagged," which can silently swallow a second, different ask).HIGH to touch the life-safety half. Routine half: leaving it unwired is itself the risk — this is the single highest-value fix on this whole list.
3 · Unknown-caller noteAn unverified caller leaves a message; if the property has no one set up to receive tracked requests, this old email is the fallback.Already replaced, same story as #1 — it already asks the shared door first, and only fires when the door has nothing better to hand the request to.HIGH for the same reason as #1 — an unverified caller's message would get zero human follow-up on a property with no owner configured.
4 · Promise-ledger pageFires automatically whenever a promise Clara made — a callback, a follow-up, a document — can't be kept: a dead phone transfer, a failed application link, a failed text or email.Nothing to build — this one is the shared door's own built-in fallback already. It always tries the tracked path first.VERY HIGH — this is the safety net under every broken promise across the whole system.
5 · Collections escalation to the ownerWhen an account is flagged for eviction, or a demand letter goes uncured, this emails the property owner directly.Nothing today — and this one is genuinely a different kind of thing from the rest of the list. It's not "a caller needs help," it's "the owner needs to know about legal exposure."HIGH, and unrelated to the rest of this list — pulling it out risks going silent on eviction flags and uncured demand letters, a compliance gap, not a leasing-experience one.
6 · Held-renewal pageWhen a renewal can't proceed on its own (resident opted out everywhere, no email on file, mid-transfer, etc.), this pages a property manager directly.Nothing today. Its own "don't repeat myself" logic is built around a lease, not a live conversation — so it literally can't see whether the shared door already knows about the same tenant from a phone call. That's a design gap, not a bug.HIGH — an unpaged held renewal reverts to invisible-until-someone-happens-to-check, exactly what this system exists to fix.
In plain terms

Three of these six emails (#1, #3, #4) are already fixed in spirit — the new, better path already wins whenever a property is set up for it. What's actually blocking their removal isn't code, it's making sure every live property has that setup done. Two more (#2's routine half, plus #5 and #6) genuinely need work before anything gets turned off.

Decision 1 · Deprecation kill-order

The report's recommendation is a five-step, staged order: instrument first, close the biggest real gap next, let the data-only fixes finish quietly in the background, then delete — never one big sweep across all six at once. Doing it all together risks deleting a working fallback before its replacement has actually caught up.

In plain terms

Option A is "turn things off only once we can prove, with real numbers, that nobody needs them anymore." Option B is "turn them all off now and hope the new system already covers everything" — which is the riskier bet.

StepWhat happensDepends onTouches next week's Temporal rebuild?
0 · Instrument firstAdd counters to sites #1, #3, #4 to see how often the old fallback actually fires versus the new path — replaces guessing with real numbers.NothingNo
1 · Migrate the "get me a person" tool(routine half only)Wire it through the shared door, same email it already sends, just checks "did we already tell someone" first. Emergency path untouched.NothingNo
2 · Finish the owner rolloutGet every active property a configured decision owner — config work, not code. Starves the fallback legs to zero naturally.Step 0's counters, to know when "done" is trueNo
3 · Delete the old fallback branches(#1, #3, #4)Once counters show zero real fallback traffic for a sustained period, remove the branches — one small, live-verified step per site.Step 2 completeNo
4 · Decide the collections question(#5)Pick from the options in Decision 4 below.Your decisionNo
5 · Design renewal-page integration(#6)Real design work to let the shared door accept a lease-keyed question. Lower priority.Benefits from waiting until the door's core behavior is proven stableCoordinate timing

Recommended pacing: run steps 0 and 1 in parallel this week — pure code, no data dependency, and step 1 alone closes the biggest real gap. Steps 2–3 are a slower background rollout, not a sprint. Step 4 just needs your answer whenever you have a minute. Step 5 waits.

Decision 4 · The collections/eviction owner alert (site #5) — needs your call, not an engineering default

This one is genuinely different from the other five — it's not "a caller needs help," it's "the owner needs to know about legal exposure." It reuses the same "who should hear about this" field as the tracked-request system, but doesn't use any of that system's plumbing.

In plain terms

This is really asking: is a "your resident needs a callback" alert and a "you have an eviction risk" alert the same kind of thing, or two different things that happen to reuse the same contact field today? The recommendation says they're different things — leave them separate, just write it down clearly so nobody merges them by accident later.

Where this deliberately does NOT touch next week's work

A separate team is rebuilding the reminder/nag-cadence machinery (the daily re-send timers) on a different system next week. Nothing in this plan needs that finished, and nothing in this plan should land in the middle of it. One thing worth flagging to that team directly, not fixing here: the old reminder cadence for the forward-to-PM email is switched off by a hand-written check whenever a property has an owner configured — specifically so a property doesn't get double-chased by two reminder systems at once. If their rebuild changes how reminder types get registered, that switch needs to survive the rewrite. Flagging it, not fixing it here.

Part 2 — The Camellia bake gate

DESIGN — NOT YET BUILT

Once the shared door is doing more of the work at Camellia, it's brand new there. This is a design for a temporary safety net: while it bakes in, every team-facing escalation email at Camellia gets held and posted to Slack for a one-time approval, instead of going out immediately — controlled by a single on/off switch for that one property, default off.

Flagging this up front, before anything else

This design is, mechanically, exactly the shape of thing the standing rule "no PM review queues — alerts never become approval gates on AI output" exists to forbid. The case for treating it as a defensible one-time exception is laid out below — but it needs your explicit yes before it ships, separate from approving the design itself. See Decision 3.

What's actually being held, and what never is

~9 places in the codecan send a "a human needs to see this" email
The gateholds it, posts to Slack, waits for a click — Camellia only, flag off by default
Sent, exactly as it would have beensame function that always sent it — the gate only changes WHEN

One exception sits outside this entire flow: LIFE-SAFETY PAGES ARE NEVER GATED. A gas-leak or true-emergency page is structurally separate code — a different function entirely, not just a different flag check — so the gate physically cannot touch it by accident.

Related ruling tonight, outside this gate's own mechanics: tour access instructions (how a prospect actually gets into a unit for a self-guided tour) are also never blocked or held for approval — this supersedes the 2026-07-13 default of requiring an access code first. Noted here because it's the same category of call: some things a resident or prospect needs simply can't wait on a human, gate or no gate.

Where the gate hooks in

SiteHow it's covered
The shared door itselfCovers the biggest slice of traffic in one place: forward-to-team, unknown-caller-note, the "team already knows" notice, and both promise-ledger paging paths.6 of ~9 senders, one shared check
"Get me a person," routine halfCalls its own sender directly today, skipping the shared door — the highest-volume gap. Gets its own small wrap.Own wrap — emergency half untouched, always sends instantly
Collections/eviction owner alertDoesn't touch the shared door at all today. Gets its own small wrap.Own wrap
Held-renewal pageFully separate plumbing today. Gets its own small wrap.Own wrap
The daily "nudge the team again" reminder jobOnly re-sends about a request that already exists on file — and the system already refuses to create that record until the first email actually goes out. So holding the first email means there's nothing yet for the reminder job to chase.Nothing to build — falls out for free

Excluded from the gate — a scope call flagged for your confirmation

Four kinds of "thanks for answering, here's what happened next" replies — sent back to whoever already responded inside a thread they opened themselves — are excluded from the gate. These are conversational replies inside an existing staff thread, not new pages to someone who hasn't seen anything yet. Holding a "thanks for your reply" note for a second approval doesn't fit the spirit of the gate. See Decision 5.

Decision 5 · Exclude conversational thank-you replies from the gate?

These four reply types are follow-ups inside a thread a human already opened and already responded to — not a first notice to someone who hasn't seen anything.

How approving or rejecting actually works

Reuses a pattern already live in the product today — the same "click a link, land on a page that checks you're logged in, click Approve or Reject" flow already used for the "mark handled" button on a property-team follow-up. Nothing new gets built for how the email actually goes out; the gate only decides when.

Decision 2 · What happens if nobody clicks anything

A held email that nobody ever answers can't just sit there forever — that would quietly re-introduce the exact "goes silent" failure this whole system exists to prevent.

In plain terms

Option A means nothing goes out until you personally say so — safest for a first overnight test, but if you're asleep or heads-down, a real resident's message waits. Option B is the belt-and-suspenders version: it still nags you first, but after four hours it sends anyway rather than risk a resident never hearing back. The original design recommended B as the general behavior; A is called out here as the tighter setting specifically for tonight's first bake, where nothing should leave the building without your eyes on it first.

Decision 3 · The "no PM review queues" standing-rule exception

The standing rule says alerts never become approval gates a property team has to clear before AI output goes out. This design is an approval gate in front of AI-sent email — the literal thing the rule bans. The case for treating it as a narrow, defensible exception: it gates one specific person (you), for a fixed short bake window, on one property standing up a brand-new feature for the first time — not the property team, not an ongoing product behavior. It fails open, not closed — nothing waits forever, and Decision 2 controls exactly how that plays out. And it's fully removable in one step — flip the flag off, and nothing else in the product changes shape to accommodate it.

In plain terms

The standing rule exists so a property team never becomes a bottleneck the AI has to wait on to do its job. This isn't that — it's you personally watching a brand-new safety mechanism for a few hours the first time it runs at a real property, with a hard cutoff so nothing gets stuck. But it's still an approval gate, so it's asking your permission to make that one exception rather than assuming it.

Test plan — Willows first

Willows (the test property) already has the underlying escalation system turned on and is the property this was built and proven against — the right place to test the gate before it ever touches a real resident.

Only once all of that is green would this move to Camellia.

Rollout to Camellia

What makes this reversible. No existing send logic gets rewritten anywhere — every wrap is a small "if the flag is on, hold and post to Slack; otherwise, send exactly as today" check placed in front of a function that already exists. Turning the flag off, or undoing the change entirely, puts every one of these paths back to sending immediately, byte-for-byte the same as before the gate existed.

Technical appendix

Plain-English narrative above is the source of truth for the decision; this section is for whoever implements it.

Part 1 — sites and the source functions they live in
Part 2 — files the bake gate would touch
FileChange
src/lib/data/types.tsAdd escalationBakeApprovalRequired?: boolean to Property, next to escalationOwnerEmail. Unset = today's behavior, byte-identical.
src/lib/domain/properties/escalation-bake-gate.ts (new)One tiny reader, same shape as the existing pm-notification-mode.tsisBakeGateActive(property)
src/lib/domain/escalation/bake-gate.ts (new)The shared holdOrSend() helper: checks the flag, either calls the real sender or writes a held record + posts to Slack
src/lib/data/dynamo/escalation-hold.ts (new)New, isolated record type for a held email — mirrors the existing escalation-matter record's storage pattern, own partition, touches no existing table's shape
src/lib/domain/notifications/human-notification-lane.ts~3-line wrap around the existing send step inside notifyHumanOnce
src/lib/tools/handlers/handle-escalate-to-human.ts~3-line wrap around the non-emergency branch only
src/lib/domain/leasing/collections/owner-escalation.ts~3-line wrap
src/lib/domain/leasing/renewal-notify.ts~3-line wrap
src/app/api/admin/escalation-holds/[id]/route.ts (new)Approve/reject action — copies the auth + scoping pattern already used by the existing "mark handled" route
src/app/admin/escalation-holds/[id]/page.tsx (new)The one-click review page Slack links to
src/app/api/cron/escalation-hold-sweep/route.ts (new)The bump/timeout timer — copies the existing daily-nudge cron route's shape, checked every 15–30 minutes given the shorter fuse
config/automations.tomlOne new job entry for the sweep, same pattern as the existing nudge job

Slack posting reuses the single existing chat.postMessage call in src/lib/platform/observability/notify.ts — no new Slack app permission, no new channel needed. Approve/reject copies a pattern already live in production (the "mark handled" button flow) rather than building Slack-native buttons or reaction-watching, both of which were evaluated and ruled out for a one-time, short-lived feature.

Edge cases the bake-gate design accounts for

Provenance

Built overnight 2026-08-30 from two source documents: a deprecation report auditing all six old-generation escalation-email sites in the code (their replacements, what's still relying on each one, and a recommended kill order), and a design brief for a temporary Slack approve/reject gate in front of every team-facing escalation email at Camellia during its bake-in window. Both are proposals — nothing in either has been built. The deprecation report's scope explicitly excludes the separate Temporal rebuild of the escalation reminder/nag-cadence machinery another team is doing next week; nothing here depends on that work, and nothing here should land in the middle of it.

PropFlow Docs