Every message Clara sends passes one checkpoint first. Each module has a three-position switch, and the switch decides what happens to the message.
7 October 2026 Β· Every message Clara sends now goes through the switch
Clara doesn't send for this module. Setting up works the same way while a customer finishes onboarding that module.
Clara writes it. A person approves it before it goes out.
Clara writes it and sends it on her own.
Supervised only holds messages. Updates to AppFolio, such as work orders, vendor assignments, notes, guest cards and tour bookings, always happen right away. Only what Clara says to a person waits.
Tap a position on any card to see what it does. Each card opens on its starting setting.
A call can't wait for approval, so a Supervised building picks one of two options.
Texts and emails wait for approval. Clara still answers and makes calls.
Texts and emails wait for approval, and calls go to the team instead of Clara.
Anything that puts someone in danger.
Work orders marked high or emergency priority.
Escalations and requests that ask the team to act.
A message someone on the team sent or approved.
When the team texts Clara, she answers right away.
A general reply like "what are your office hours?" If Clara files it under a module, that module's switch applies.
Approving a message sends exactly what Live would have sent. The only difference is the wait for a person.
Same attachments (like the signed lease PDF), the same people copied, the same email thread.
Same AppFolio notes, office alerts and delivery tracking as an automatic send.
Door codes, staff notices and vendor messages go out any hour once approved; resident and prospect messages respect quiet hours.
If the visit moved, the job closed, the address changed, or the resident wrote again, it doesn't go out, and the thread says why.
If nobody approves it within a day, it's closed and never sent.
A newer reply to the same person replaces the older one still waiting.
A message approved at night goes out in the morning. Urgent jobs don't wait.
If the module was switched Off after approval, or the job changed, it doesn't go out.
Each held message is posted to the building's Slack channel (#alerts if it has none) with the message and a link. A reminder follows in the thread while it waits.
The link opens the held message, where a person approves or rejects it.
One short card: who it's for, the message, why it's held, and "Reply approve or reject: why".
The thread ends with one line: "β Sent 9:05 AM to β¦ Β· View conversation", or "β Not sent β reason". A bounce after sending turns β into β.
Run on PropFlow's own test buildings only. Each message was held, approved, then delivered to a test inbox or phone.
Held at a Leasing-Supervised building, approved, delivered.
Clara called it general, but it was routed to Maintenance (Supervised), so it was held. Approved and delivered.
The leases-ending digest was held, approved and delivered.
Dropped emails were marked "delivered", held replies showed early in the thread, and the 24-hour cleanup never ran. All three are fixed and live.
Marked "dropped", and the Slack thread shows one β Not sent line.
Compact βΈοΈ card when held, then β Sent with the time, who, and a conversation link.
An approved handyman page and team notice went out right away, at 12:40 AM.
An approved reply that had gone stale was replaced by a fresh draft, and the team was told.
A resident texted about an emergency at a Supervised building. Clara's reply reached their phone within seconds, with nothing held, including on the follow-up text. Emergency email replies use the same fix but haven't been tested live yet.
The handyman is assigned in AppFolio as soon as the job is created, while his text still waits for approval. Re-tested live.
One row per module: one status, one button. A module moves Off β Onboarding β Ready to turn on β Supervised β Live. A PropFlow engineer presses Turn on, and the module starts in Supervised.
Sets it per building. A building that follows the company shows "Supervised (from company)".
Only PropFlow staff can change Off / Supervised / Live. Customers see a simple on/off per building.
What happened. On 29 Oct at 19:11 UTC the Off-to-Preview rollout put Renewals and Collections in Preview at Western Slope and Situs. On 30 Oct at 14:30 UTC the daily renewal scan read Preview as Supervised, opened the monthly "review the list" hold, and the hold emailed the approval request to Western Slope's renewal reviewers, Jay and Kat. Delivered. Fede's rule the same morning: Supervised and Preview send nothing to anyone without an approval, and Preview never opens an approval request to customer staff.
'none', no doorPreview is stored as its own word but most readers only know Off / Supervised / Live. Anything that narrows to those three treats Preview as Supervised. Anything a hold does after it is created (reviewer email, Slack card, approve button) ran with no mode check.
| Source checked | Renewals / Collections traffic found | Verdict |
|---|---|---|
| SendGrid (117 emails from Clara) | 1 email, "10 renewals to approve", to jay@ and kat@ (+ copy to our hello@) | Leaked to customer staff |
| Slack #apartment-western-slope (49 bot posts; 4 members, all PropFlow) | 2 "Held for review" cards: the renewal list, and a Collections text draft to a resident | Internal only |
| Twilio texts and calls from Clara's Western Slope number | 9 texts, all leasing or maintenance; 0 calls | Nothing |
| Western Slope Outlook mailbox sends | 5 sends, all leasing | Nothing |
| AppFolio write queue (28 writes) and texts through AppFolio (17) | All leasing | Nothing |
| Holds created (25) | 2 stamped Preview: the renewal list (reviewer notices sent) and the Collections text (pending) | Both now rejected |
| Renewal records, Collections records | 1 notice-to-vacate read in; 0 outbound; 0 collections rows | Nothing |
Controls held: the known email and the known hold came back in their queries. Temporal itself was not listed (no connection from the Mac); the renewal and send records stand in for it. No resident, prospect or AppFolio record was touched by either module.
Renewals set back to Off (read back Off at all 10 listed buildings). October renewal list rejected. Pending Preview Collections text rejected.
Mode left in Preview (Fede: just Western). October renewal list rejected so November starts clean. No reviewers stored, so no email ever left.
#23805 (merged 17:12 UTC): the reviewer email asks the mode. Gap: it checks only the list's anchor building. Scan-under-Preview fix: Smith's session, open.
Three auditors on origin/main 02a2004, read-only. Positive controls held (the batch scan and the old reviewer email were both flagged). About 60 send sites in 12 groups; three causes explain nearly all of them.
| # | Cause | Where (examples) | Reaches | Smallest fix |
|---|---|---|---|---|
| 1 | What a hold does after it opens. The Slack card to the customer's channel, the bump reminder, and some approve routes run with no mode check. Approving a Preview email hold or a held AppFolio write still sends or writes. | postHeldEmailForReview, postHeldTextForReview, bumpStaleHolds, bake-gate 'none' arms (:826, :1486), approve-held-pms-write, turnover-gate folding Preview into supervised | Staff, resident on approve, AppFolio on approve | Skip the customer-channel card when heldInPreview; approve routes declare the real module and re-read the mode; writes check writesGo. Gera's #23823 covers the email approve. |
| 2 | Staff notices declared 'none' or 'team_alert', which tells the email and text doors to skip the mode. ~35 sites. | reply-release close/ack/retraction notes, gated-inbound team notice, promises pm-delivery, autonomous team note, forward-to-PM, tour confirmations/cancellations/host fan-out, renewal yes/executed/holdover office notices, renewal_escalate, PM action reminders, vendor outcome, move-out mismatch, staff welcome text, unknown-caller note | Customer staff (email and text) | Replace with sendOrHoldTeamNotice({ coworkerNoteOf: <module> }); staff texts declare module + recipientType:'staff' + draftApprovalId:null. Caller-message paths ask the one rule with CALLER_MESSAGE_SUBJECT. |
| 3 | Gaps in the gates. decisionRequestModuleOf knows 3 modules; Resident services missing from the Supervised backstop; team-notify engine applies the mode only to email notices with a building and a module; new-lease and turnover gates have no Preview value; generic PMS writer has no Preview branch (guest-card and work-order writers do). | decision-request-hold.ts, supervised-reply-rule.ts, team-notify/engine.ts:527, new-lease-gate.ts, writer-registry.ts:219, move-out draft hook, listing hooks, charge re-stage | Staff texts, resident replies (Resident services), AppFolio writes (turnover, listings, charges) | Fail closed on an unknown module; add Preview to each gate; recording writer at Preview in the generic registry. |
| 4 | Slack asks to the customer's channel with no mode check. | postOpsEvent action cards, new-lease review alert, fair-housing hold notify, reply-release :2551 | Staff (if the channel has customer members) | One gate in postOpsEvent; emergency card exempt. |
| 5 | Per-path override beats Preview by Gera's decision 3 (path-mode.ts:169-216): a path set to Live sends at a Preview building. | twilio/client.ts:1240 pathRuled skips previewRefusesSend; process-envelope.ts:1739 | Anyone on that path | Decision for Fede and Gera, not a bug. |
Already safe: every resident, prospect and vendor send that declares its real module and threads the approval (email door, text door, voice door), the AppFolio writers behind writesGo, the emergency rule, and human-authored sends. Full file:line tables and the per-area notes are in the session's audit files; Smith has the list on the bridge.
Today's build: yes, by design (the hold lands on the customer's review page and Slack card, "approving will not send it"). Fede's 30 Oct rule: no. Recommendation: no. At Preview the hold's front door records and opens nothing reachable. One change in holdEscalationEmail / holdEscalationSms; the Slack card and approve routes then have nothing to act on.
Keep Gera's decision 3 (a path set Live sends at a Preview building) or make Preview absolute. Recommendation: Preview absolute; a per-path Live at a Preview building is the kind of hidden switch that bit us today.
Root cause of the sprawl, in Fede's words: "too many gates sending out notifications, not a single choke point. We've tried this many times and it keeps sprawling." The audit above agrees: about 10 places read the mode and decide for themselves, and about 35 of 60 senders skip the doors by declaring module 'none' or 'team_alert'. Four sweeps in four weeks (gatekeeper 7 Oct, Supervised-holds-everything 26β29 Oct, Preview 28β30 Oct, path-mode audit 30 Oct) patched sites; none removed the ability to bypass.
decide(module, building) β record Β· hold Β· send| Rule | What it replaces |
|---|---|
A send cannot be made without a module and a record. There is no 'none'. | The 'none' / 'team_alert' escape hatch at ~35 sites. |
| The mode is decided once, in one function, from the stored mode (Off / Supervised / Live / Preview / path override). It returns record, hold or send. | Ten readers each narrowing to their own three values. |
| Holds, approvals, Slack cards and bump reminders are consumers of that decision. Approving re-asks it. | Hold-side sends with no mode check (today's incident). |
| Exemptions are a closed list in one file: emergency, caller left a message, a human wrote it. | Exemptions scattered as flags and module names. |
| A build check refuses importing the raw senders (SendGrid, Twilio, voice, Slack notify, PMS writers) anywhere but the door module. | Audits. |
One function; the ten readers become callers. Positive control: the batch scan and the turnover gate change behaviour at Preview; nothing else does.
Type the send calls so module is required; migrate the ~35 'none' sites to their real module; delete 'none'. Hold-side sends go through the door.
Lint rule: raw senders importable only from the door. Exemption file. Delete the per-reader copies.
Owner: Gera's lane (modules, paths, gatekeeper), with the incident lane's audit tables as the inventory. Decisions D1 and D2 above are inputs. Status: proposed, handed off 30 Oct via the engineering bridge.