Gatekeeper drafts mode

How each module gets a third setting, drafts: Clara prepares the replies and actions, and a person at the customer approves them before anything goes out.

2026-09-29 · design only, no code · board rows gk-drafts-mode (11) and gk-settings-page (12) · decision b90675488 · extends ADR-0139

Drafts is how renewals already work. Since #13009, when a company has renewal review on, a renewal reply is held instead of sent. It waits in the same queue, Slack post and review page as held emails. Drafts mode turns that into a setting per module and per building, replacing two separate company switches.

In drafts, Clara does the work and someone on the customer's team presses send. Renewals already works this way, so most of this is extending code we already have.

1 · The data model: off, drafts and live without a new failure mode

The boolean stays the only on/off answer. A mode goes next to it.

Stored todayReads asMigration
true, or absent everywherelivenone
false (company or building)offnone, because mode is ignored when the module is off
reviewRenewalMessages = truerenewals in draftsone-time script with read-back; after that, the old switch reads from the new setting
reviewOutboundEmails = trueprospects in drafts, for Clara's prospect emails only (Q6)same script. This switch also holds team-facing escalation emails (decision requests, nags, collections owner alerts), which belong to no module, so it stays in place for those

The fold may only add holds, never remove one. Each old switch keeps working until the new setting has been read back, so no customer gets more live sends because of the migration.

OFF refusals keep working without edits. Every gate refuses only on <module>_module_off, and drafts counts as a kind of ON. New code goes only where a prepared item can be held.

When the setting changes: going from drafts to off means pending drafts for that module are refused if someone approves them, and they are marked expired. Going from drafts to live leaves pending drafts waiting for approval rather than sending them automatically, because ADR-0139 §7 says turning a module on never replays old work. Going from live to drafts only affects work that starts after the change.

If the setting cannot be read, a drafts building sends as if it were live, as it would today, because we cannot tell it was in drafts. Q4 asks whether that is acceptable.

2 · What drafts does at each layer

Every layer asks the same read. As in ADR-0139, no layer assumes an earlier one already held the item.

LayerIn draftsOn approveOn reject
Clara's replies (text, email)Held. The #13009 hook (withClaraReplyRowDeferred and holdIf) holds back the reply's row. classifyReplyTopic, which already returns maintenance, picks the module. The reply goes to the held-message queue (EscalationHold, new source clara_reply_sms or _email).Checks the setting again, and if the module is now off, refuses. Otherwise it sends the stored words through the send door with the real module and an approval id. The conversation row is written when the message sends, and the same draft cannot send twice (as in approveHeldText).Nothing is sent. A note goes on the conversation and the team takes over the thread.
Tool actions and AppFolio writesThe tool gate prepares the action and does not run it. It is stored on that turn's draft, using the lifecycle of the renewal and new-lease change reviews: pending, then executed or execution_failed.A turn's reply and its actions are approved together. The actions run first, and the reply sends only after they succeed. If one fails, the reply stays pending and the approver sees the failure, so the resident is never told something was done when it wasn't. Before anything runs, the setting and the live AppFolio state are checked again. Writes go through the per-building work-order writer, the same place OFF refusals happen.Discarded, with nothing written to AppFolio.
Automation (auto-start, chases, reminders)Workflows still start, since starting one sends nothing. Any step that reaches a person is held, the way renewal outreach texts are today (held-text.ts). If a newer draft arrives on the same thread, it replaces the older pending one.Sends that step.Skips that step, and the workflow continues as it would after any refused step.
PromisesA promise in a held reply is not recorded yet.Recorded, with its follow-ups and pages.Voided, so nobody is paged about a promise the resident never heard.
Send door (backstop)A send in a drafts module with no approval id is refused and recorded, as OFF sends are today (recordModuleRefusal). The door has no thread context, so it cannot make a reviewable draft. A refusal here means an upstream lane missed its hold, so it raises an alarm, not a quiet log line.A drafts send goes out only when it has an approval id and the setting does not read off.
Always liveemergency, team_alert, login_code and staff_reply are never held. The hold check reads the front door's reason first, so a life-safety turn (already mapped to emergency) never reaches the topic check. Also, the notice telling the team a draft is waiting is itself a team_alert.
3 · The settings page: three states per module

This builds on the settings audit's Q2 and Q5. Q2 says the company sets the default, a building can override it, and a badge shows where the value comes from. Q5 says turning a setting down can only make Clara do less.

4 · Slicing into PRs of 300 lines or fewer, each working on its own

Each part branches off origin/main. Parts 11.1 to 11.3 can be open at the same time.

PartWhat ships
11.1Maintenance texts in drafts, end to end, dark. Adds mode to both stored shapes and adds resolveModuleState. Widens the #13009 hook from "the renewal review switch is on" to "this reply's module is in drafts here". Maintenance replies are held as clara_reply_sms and approved through the existing hold route, which reads the module from the hold row. The only way to set drafts is the existing building-module script. Includes the ADR-0139 amendment.
11.2Maintenance actions. In drafts, write tools prepare instead of running, and the prepared action is attached to the turn's hold. On approve it runs through the per-building work-order writer after a fresh check. The held reply must not say the action already happened.
11.3The send-door backstop, plus maintenance automation (vendor chases, reminders, rating prompts). A newer draft replaces an older one, and promises are recorded only on approve.
11.4Maintenance email replies, using the held-email path that already exists, and calls, depending on the answer to Q7.
11.5Fold reviewRenewalMessages and reviewOutboundEmails into drafts. prod data, needs a go
11.6+One PR per module: prospects, renewals, collections. Collections dunning letters already always need approval (ADR-0125).
12.1Back office: BuildingModulesCard and the company module picker get a Drafts option, and the route accepts it.
12.2Customer settings page: the Modules card and the buildings table from section 3.
12.3A Drafts tab on /review, approval by customer roles, and editing before approving (depending on Q8).
5 · Answers (settled 2026-09-29)

Q3 and Q5 were answered by Gera in chat. The rest were settled from existing rules by the decider; its receipts are listed in brackets.

  1. Does the resident hear anything while a reply waits? No. Nothing goes out until it is approved. (ff0ad569d)
  2. Who can approve drafts? Anyone who can edit that module at that building. (f02dafcde)
  3. Can a customer turn a module to Live? No, Gera. Customers choose Off or Drafts. PropFlow turns on Live once the customer is ready.
  4. If the setting briefly can't be read? Same as today: only a stored setting acts. ADR-0139 section 3 already settles this. (f73d62d62)
  5. How long does a draft wait? 24 hours, Gera. A newer draft on the same conversation replaces the older one.
  6. Western Slope's review switch? It becomes Drafts for Leasing prospect emails, and the switch stays on for escalation emails. (f3716a290)
  7. Drafts on phone calls? Changed from the recommendation: a call behaves as Off in drafts mode. (fe08e2091)
  8. Can the reviewer edit Clara's words? Yes, in 12.3. (fe2b5cff7)
  9. Hold untopiced everyday texts? No. Only messages on a module's topic are held. (f3291d2c6)
  10. Name: the middle mode is called Supervised (Gera, 2026-10-01); stored value stays drafts. The setting reads Off · Supervised · Live, and every module gets it (Maintenance, Leasing, Renewals, Collections), each module turned on for it once its hold works end to end. Where this page says Drafts, read Supervised.
  11. Leasing actions in Supervised? Gera, 2026-10-01: booking, moving or cancelling a tour, writing the guest card to AppFolio and saving the prospect happen right away, as in Live. Only the message to the prospect waits for approval. For Leasing this replaces the "Tool actions and AppFolio writes" row in section 2, which still applies to Maintenance (11.2). The decider could not settle it (f587c74d6, fa9e0baa8, f94e2dcc2).
  12. Which Leasing messages wait in Supervised? Driver ruling, 2026-10-01: the test is did a person on the team start this send? A message a staff member triggered directly (the confirmation of a tour staff booked, the "new lease approved" message sent from staff's own approval) is already approved by a person, so it is exempt and goes out as in Live. A message Clara started (a tour outcome text after Clara's call, a tour confirmation email from a booking Clara made, every follow-up, reminder, nudge and application link) waits. A send either could start waits when Clara started it.
  13. An approved Leasing tour text on an escalated thread? Gera, 2026-10-02 (asked in chat by the driver, posted on propflowai #15302, superseding the earlier comment there): approved tour texts still send after the team takes over a thread. That covers the tour reminders, the lost-slot notice and the tour receipt after Clara's call. Live already sends these as earned receipts, and here a team member approved them. Clara's own drafted replies still close unsent once the team has the thread. A household copy goes out only when the principal's own text did. Recorded in ADR-0139 §9.
  14. Known follow-ups, not blockers (Leasing, 2026-10-01): (1) a price drop named in a held follow-up text is not added to the "mention once" list when the text is approved, so a later touch may mention it again; (2) if the prospect replies after a reviewer approved a held follow-up text, the text is not sent (correctly) but the hold still reads "approved".
  15. Renewals actions in Supervised? Driver ruling, 2026-10-01 (fc02e77e2), applying Gera's Leasing rule: the AppFolio renewal write (preparing the offer and sending it out for signing) happens right away, as in Live. AppFolio's own "ready to sign" email is AppFolio's message, not Clara's, so it goes too. Only Clara's messages to the resident wait: the offer text, the letter, reminders, the lease-ending warning, the term-change confirmation, the no-phone offer email and her replies about a renewal.
  16. Do held renewal offer and reminder texts expire after 24 hours? No (driver ruling, 2026-10-01). They belong to the renewal's own timeline: they wait until someone approves or rejects them, or the renewal moves on (the resident answers, signs or gives notice, or a PM decides), and then they are withdrawn. The 24-hour expiry and "a newer one replaces the older one" apply to Clara's replies about a renewal, as in Maintenance. The lease-ending warning has its own rule: a newer warning replaces an older one for the same lease. An approved warning waits for quiet hours to end before it is sent.
Sources read (origin/main, 2026-09-29)
PropFlow Docs