Settings cleanup strategy

About 160 things a person or a script can set in PropFlow today. Under half are reachable in the product, a third are on/off switches sessions left behind, and turning one customer live means flipping seventeen of them per building. This is the plan to cut, level and surface the whole set — in Gera's portfolio model, with today's behavior as every default.

Proposed — pending review (2026-09-10) · Fede's rulings of Sep 9 are binding · companion to the new-customer onboarding page and to Gera's portfolio architecture

Update, 2026-09-17: Property.emailShadowMode, Property.smsShadowMode and scripts/set-property-sms-shadow-mode.ts named below no longer exist — deleted by PR #9162, not merged into a new key as this proposal suggested. Staff notices now always reach the real recipient and tour texts are always sent, with no per-property shadow gate. Read the shadow-mode mentions on this page as history, not the current field set.

The one-sentence version. Every setting gets one home (company, building or person), one screen, and a default that equals what PropFlow does today — and the pile of per-building switches is deleted rather than moved, so switching Western Slope on is one decision instead of seventeen per building.

1. The problem, in plain English

159things that can be set
73with no screen at all
55are on/off switches or modes
22only PropFlow staff can reach
13controlled by a deploy, not a person
44switch fields on one building record

PropFlow has grown a settings layer nobody designed. A code sweep and a signed-in walk of the live product on Sep 9 found 159 separate things a person or a script can set. Only 56 of them are editable by a customer. Seventy-three have no screen anywhere — they are changed by editing the database or running a script. Twenty-two live on an internal staff page. Thirteen only change when we deploy.

Four separate problems are tangled together in that number:

  1. Switches nobody removed. Fifty-five of the 159 rows are on/off switches, modes or stages, and 44 of them sit on the single building record. Most were added to ship one change safely, were turned on, and were never deleted. A few were added for a bug fix that should never have had a switch at all.
  2. Company decisions typed per building. Renewal terms, turnover timing, fees, tour rules, who gets notified, where availability comes from — all of it is stored per building. Western Slope has 82 buildings. Camellia's own settings would have to be re-typed 82 times to onboard them.
  3. Decisions split across several fields with hidden fallbacks. "What happens when a caller asks for a person" is decided by three fields on two different screens, with a fallback chain, and a fourth and fifth field decide who gets told when the call fails. A building with all five empty lets a caller ask for a human, dials nothing, and leaves no record that anyone asked.
  4. Things in the wrong place. The follow-up chase settings sit on a building page but rewrite the whole company. Which properties are hidden is stored per person, so two admins at one company see different portfolios. The onboarding-guide state is stored once for the entire platform although it is per person.

Sources: the Sep 9 settings inventory (code sweep of the main checkout plus a signed-in walk of production as staff, with screenshots of every settings screen), and live read-only database reads of the three reference properties.

2. The target model, in Gera's terms

Nothing here invents a new mechanism. Gera's portfolio design already defines exactly the machinery this cleanup needs, and this plan is written to fit it rather than beside it.

Levels

A value hangs on one node of the chain building → group → company, or on a person. There is no platform level: a value that applies to everyone is the company's value, or it is a constant in the code — not a setting. Fede's ruling that every setting gets a level (company, building or person) and Gera's writableAt list are the same rule stated twice.

Registry

Every key is declared once in code with a class — POLICY (the company decides, a building may not override), PARAMETER (a company default a building may override), IDENTITY (a fact about one building, never inherited) — plus the one field that matters most here: absentMeans, "the one place a default lives". Our whole migration story is that field: absentMeans is set to what PropFlow does today.

Attach rows

A value is a row, <node>/ATTACH#setting#<key>#<startedAt>, on ORG#, GROUP#, PROP# or a person. One live row per key per node; a change is an insert plus an end, never an edit and never a move. So "changing a setting" and "raising a setting to the company" are the same operation, and both are auditable.

Nearest wins, and the company can lock

Reading a setting walks building → group → company and takes the first value it finds. A company row marked locked stops any building from holding its own value. That gives us the two things the product needs and does not have: type it once for 82 buildings, and let one building differ where it genuinely does.

Defaults = today, so migration is nothing

No backfill, no data migration, no cutover. Each key's absentMeans is set to the value production resolves today. A building with no rows behaves exactly as it does now. The proof is a Camellia replay that comes out byte-identical.

3. The cleanup rules

4. The switches: their own deep clean

Every switch gets one of three verdicts.

26DELETE — behavior becomes the default
12KEEP — one dark switch per new behavior
17CONVERT — really a setting with a value

DELETE — 26 switches

SwitchWhat it gatedWhy it goes
tourPipelineV2EnabledSpan-verified tour datesOn when unset; the legacy path is not a supported behavior
tourChannelMatchEnabledReply on the channel the person usedOn when unset; answering on the wrong channel is a bug
tourTemplateDeferralEnabledSuppress a duplicate tour-confirm textOn when unset; the duplicate is a bug
turnIntegritySingleWriterOne customer-facing writer per turnOn when unset; two writers is the double-send bug
turnIntegrityReviewGateAnomaly review before sendingOn when unset
turnIntegrityTypedToolResultsTyped tool errorsOn when unset, no confirmed production read site
turnIntegrityOutputGateOutput gateOn when unset, no confirmed production read site
tourPastDateTruthEnabledRefuse to confirm a tour in the pastOn at every live property; confirming yesterday is a bug
tourDeciderModeWhich tour-confirm code path runsTwo code paths for one job; keep the newer, delete the mode
tourScheduleChokepointEnabledAtomic tour writesThe non-atomic path is the deadlock; not an option
stableWorkflowIdentityDeterministic renewal workflow idsArmed by default since Sep 3; the rollback list is scaffolding
workflowOwnedDispatchWorkflow owns the sendOne dispatch owner, chosen; the allowlist is scaffolding
statedNameWinsEnabledA stated name overwrites a stored oneIdentity correctness; the engine decides, not a per-building flag
autoLinkCoApplicantsMerge co-applicants into one householdEngine decision, never a toggle (no manual-decision UI)
transferMissedOutreachEnabledPage a human when a transfer rings outA caller asking for a human must always leave a trace
boundedPhotoDeferralEnabledFile the work order after one photo askEndless photo asks with no work order is the bug
autoSyncEnabledApply rent-roll updatesNot applying the rent roll is stale data, not a choice
escalationEmailFormatWhich escalation email layoutPick one layout; a per-building format is not a product
escalationRelayRephraseEnabledRephrase a staff answerOne relay behavior; off everywhere, unfinished, nothing waiting on it
topicScopedHoldUnmuteEnabledUnmute when the matter closesStaying muted after the matter closes is a bug
escalationBakeApprovalRequiredHold escalation emails for Slack approvalTemporary bake scaffolding, and a human-click gate we do not build
useAgentTourProcessingAgent-driven tour processingSuperseded by the tour pipeline; off everywhere
realpageIlmDrainWindowActiveMid-migration off RealPage leadsOne-off migration marker, no live drain
operatingModeOn-site vs corporate labelSchema only; nothing reads it
env CLARA_TRACE_CAPTURE_PROMPTStore the prompt on tracesOn; we always want the prompt on a trace
env TEMPORAL_NONDETERMINISM_METRICS_ALERTNon-determinism metrics drainNever armed; dead code path

KEEP — 12 switches, one per new behavior, all dark

Each of these gates behavior a customer has not seen yet. They keep exactly one switch, at the company, never per building, and each carries an owner and a delete-by date.

SwitchThe new behaviorWhere it may be baked
voiceCallbackEnabledClara calls a person back after a failed voice escalationWillows, then one Camellia bake with Fede's go
promiseLedgerCallerOutboundEnabledText the caller back when a promise is keptWillows
tourRequestSmsEnabledSMS receipt after an unconfirmed tour requestWillows
vendorVoiceCallingEnabledClara phones vendorsLive at the Willows today
preferenceCaptureEnabledAsk for and remember beds, budget, move-inWillows
escalatedTourBookingEnabledBook a tour on a thread a human is holdingSandbox only today
sameThreadMemoryAckEnabledAcknowledge a staff answer in the same threadBench only today
escalationCoworkerModeEnabledPark the question instead of answering for the PMWillows
autonomousHoldoverEnabledConvert a lapsed lease to month-to-month and rebillTest property only; money-moving, Fede's call
tourWindowMatchingEnabledOnly offer slots inside the window the prospect asked forThree copies of this switch collapse to one
pmsMessagingSync.enabledPull PMS guest-card threads in and reply as that seatSitus Group candidate
notice-to-vacate firm routing
today env EMAIL_NTV_FIRM_ROUTING_ENABLED
Treat a formal notice email as a firm move-outMoves out of an env var onto the registry

CONVERT — 17 booleans become one question with three answers

Ten autonomy switches, the stage field and the module flags are all asking the same thing about one product area. They become capability_stage.<area> — Gera's key, already in his registry — with three values: off (the customer does not see it), drafts (Clara prepares, nothing leaves), live (Clara sends). The two shadow-mode switches become observe.sms and observe.email, his keys for the same idea on one channel.

Switch todayBecomes
capabilityStage.{leasing,renewals,maintenance}capability_stage.<area> — the survivor
EnabledModules['leasing.prospects' | 'leasing.renewals' | 'maintenance' | 'collections']the same key's off value
autonomousRenewalEnabled, autonomousTurnoverEnabled, autonomousListingPublishEnabled, autonomousLeaseSendingEnabled, autonomousVendorDispatchEnabled, autonomousMaintenanceEnabled, autonomousLeasingDigestEnabled, autonomousLeasingReportingDigestEnabled, newLeasePipelineEnabled, autoDraftLeaseOnApprovalEnabledthe same key's drafts vs live value
smsShadowMode, emailShadowMode (building and company copies)observe.sms / observe.email
handoffMode (coworker|autonomous)a company voice setting on a screen, not a hidden mode
vendorTransferFirstEnabledthe vendor slot of the one "who answers" setting

The global brakes stay as they are and are not settings: the platform email and SMS kill switches, the renewal auto-start and send arms, the collections halt, the excluded-people list, and the permission overrides. They are operational brakes we pull in an incident, they live outside the customer settings registry, and Gera's model has no platform tier for them by design.

What each switch is set to in production today

Read on Sep 9 from the production table with a consistent read, read-only. absent means the attribute is not stored on the record at all, so the code's own default applies. This is the evidence for every deletion below it: a switch that is off at a real property is a bake still running, not a delete.

SwitchCamelliaYale 25WillowsVerdict
tourPastDateTruthEnabledtruetruetrueDELETE
tourDeciderModev2absentv2DELETE
autoSyncEnabledtrueabsentabsentDELETE
transferMissedOutreachEnabledtrueabsentabsentDELETE — turns it on at Yale and the Willows
boundedPhotoDeferralEnabledabsentabsenttrueDELETE — turns it on at Camellia and Yale
statedNameWinsEnabledabsentabsenttrueDELETE — turns it on at Camellia and Yale
autoLinkCoApplicants (leasing settings)absentabsenttrueDELETE — turns it on at Camellia and Yale
useAgentTourProcessingfalseabsentabsentDELETE
escalationBakeApprovalRequiredfalseabsentfalseDELETE
escalationEmailFormatabsentabsentcoworkerDELETE — pick one layout
escalationRelayRephraseEnabledabsentabsentabsentDELETE
topicScopedHoldUnmuteEnabledabsentabsentabsentDELETE
tourScheduleChokepointEnabledabsentabsentabsentDELETE — keep the atomic path only
realpageIlmDrainWindowActiveabsentabsentabsentDELETE
operatingModeabsentabsentabsentDELETE
workflowOwnedDispatch (global row)on, limited to Camellia + Willows — Yale is not on the listDELETE — one dispatch owner, Yale included
stableWorkflowIdentity (global row)on, same two propertiesDELETE — armed by default since Sep 3
tourPipelineV2Enabled, tourChannelMatchEnabled, tourTemplateDeferralEnabled, the four turnIntegrity* gatesnot stored anywhere — no property row and not on the global row, so every one of them is running on its code default of ONDELETE — seven switches nobody has ever set
voiceCallbackEnabledabsentabsenttrueKEEP dark
vendorVoiceCallingEnabledabsentabsenttrueKEEP dark
preferenceCaptureEnabledabsentabsenttrueKEEP dark
sameThreadMemoryAckEnabledabsentabsenttrueKEEP dark
escalatedTourBookingEnabledtrueabsentabsentKEEP — already on at Camellia
promiseLedgerCallerOutboundEnabledfalseabsentabsentKEEP dark
tourRequestSmsEnabledabsentabsentabsentKEEP dark
escalationCoworkerModeEnabledabsentabsentfalseKEEP dark
autonomousHoldoverEnabledtrueabsentabsentKEEP — global holdover arm is also on
tourWindowMatchingEnabledabsentabsenttrue (leasing settings)KEEP — three copies become one
handoffModeautonomousabsentcoworkerCONVERT to a company voice setting
smsShadowMode / emailShadowModeabsent / falsefalse / falseabsent / absentCONVERT to watch-vs-send per channel
capabilityStageabsentabsentabsentCONVERT — never set; the code default of live is the real value
autonomousRenewalEnabledtrueabsentfalseCONVERT to area status
autonomousTurnoverEnabledtrueabsenttrueCONVERT
autonomousListingPublishEnabledtrueabsentfalseCONVERT
autonomousMaintenanceEnabledfalsefalsetrueCONVERT
autonomousVendorDispatchEnabledabsentabsentfalseCONVERT
autonomousLeasingDigestEnabled / ...ReportingDigestEnabledtrue / trueabsentabsentCONVERT
newLeasePipelineEnabled / autoDraftLeaseOnApprovalEnabled / autonomousLeaseSendingEnabledtrue / absent / absentabsenttrue / true / absentCONVERT
vendorTransferFirstEnabledtrueabsentabsentCONVERT into "who answers"
messagingDelivery / vendorJobReferenceMode / smsConsentModeabsentabsentpms / work_order / absentalready settings — see the area tables

Two things worth naming from this read. Yale is nearly empty: only four of these fields are stored on it at all, so Yale runs almost entirely on code defaults — which is why a default that equals today's behavior is the whole migration. And the seven "kill switches" on the staff page have never been written anywhere: they read as ON from their code default, which makes them a deletion with no production value to preserve. The global brakes read: platform email on, SMS on, vendor quote and dispatch emails on, renewal auto-start on, renewal sending on, holdover conversion on, collections not halted.

The safe deletion order

Per switch, in this order, one switch per pull request:

  1. Read the live value at Camellia, Yale and the Willows with a consistent read, and confirm every real property already resolves the on-state. A switch that is off somewhere real is not a delete — it is a bake still running.
  2. Pin the on-state as the default in code: delete the read of the switch, make the behavior unconditional. No fallback, no "if unset" branch left behind.
  3. A regression test that fails on the old code and passes on the new — the behavior itself, not an assertion that a constant is true.
  4. Camellia replay byte-identical (Gera's control fitness function, green at every step), then before/after screenshots of the settings screens at the Willows.
  5. Strip the field from the type and leave a tombstone the drift guard checks, so the next session cannot reintroduce the same name.
  6. Delete the stored attribute from the property records last, in a verified sweep, and never before the code has stopped reading it.

The rule that stops the pile growing back

A switch does not exist unless it has a row on the arms registry, carrying: the key, the owner, the pull request that introduced it, the one customer-facing behavior it gates, the properties it is baking at, and a delete-by date.

CI fails the build when:

The registry row and the CI check are one small change, and they ship in the same window as the first deletions — otherwise the clean-up is undone by the next fleet.

Western Slope goes live on one switch

5. The verdict, setting by setting

Every row in the Sep 9 inventory gets one verdict. KEEP — stays as it is, already right. SURFACE — stays, but gets a screen it does not have. MERGE — folded into another setting. MOVE — same setting, different level. DELETE — gone. Level is where the value may be set: company, building, person, or brake (an operational switch we pull, not a customer setting). "Default = today" is the value production resolves right now, and it is what absentMeans gets set to.

The screens. Fourteen company sections — Who answers · Office hours · Voice · Tours · Availability · Applications & leases · Renewals · Turnovers & maintenance · Notifications & reports · Follow-ups · Messaging & consent · Connections · Go-live status · Team & properties. One Building page with two tabs — Identity (address, numbers, links) and Knowledge (policies, fees, amenities) — plus an Overrides tab that shows only where this building differs from the company. One Person page (Account). One internal Brakes page for the switches we pull in an incident.

Voice and phone

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
Property.officePhoneWho answers when a caller asks for a personMERGE into Who answerscompany + building overridetoday's office numberCompany · Who answersOne decision, not three fields and a hidden fallback
Property.leasingDeskPhoneWho answers a leasing questionMERGE into Who answerscompany + building overrideabsent — take a messageCompany · Who answersSame decision, a different role slot
Property.escalationOwnerEmailWho gets the message when nobody picks upMERGE into Who answerscompany + building overridetoday's owner, else the building inboxCompany · Who answersAn address is a valid answer to the same question
PropertyLeasingSettings.renewalContactEmail / .renewalContactPhoneWho hears about a stalled renewalMERGE into Who answerscompany + building overridetoday's value, else the building inboxCompany · Who answersA per-topic copy of the same destination question
Property.vendorTransferFirstEnabledWho answers a vendorMERGE into Who answerscompany + building overrideCamellia: the office; elsewhere ClaraCompany · Who answersA destination choice, not a flag
PropertyMaintenanceSettings.maintenanceTechPhoneWho answers a maintenance callMERGE into Who answerscompany + building overridetoday's tech numberCompany · Who answersSame slot pattern, one screen
Property.emergencyPhoneThe life-safety numberKEEPcompany + building overridetoday's numberCompany · Who answersLife safety is its own decision; fallback role removed
PropertyKnowledge.phoneThe number Clara reads out to callersKEEPbuildingtoday's public numberBuilding · IdentityPublic number is a fact about the building
PropertyKnowledge.officeHoursOffice hoursMOVE to companycompany + building overridetoday's per-day hoursCompany · Office hoursHours gate transfers; a portfolio types them once
holidayPolicy.observeFederalHolidaysClose on federal holidaysSURFACEcompany + building overrideobserve all elevenCompany · Office hoursIt closes the office; must not be database-only
holidayPolicy.openOn / .extraClosuresExtra open and closed datesSURFACEcompany + building overridenoneCompany · Office hoursCalendar facts a manager needs to edit
Property.handoffModeWhether Clara speaks as herselfMOVE to company, SURFACEcompany + building overrideCamellia autonomous, Willows coworker, elsewhere coworkerCompany · VoiceOne company voice, chosen on purpose
Property.voiceCallbackEnabledClara calls back after a failed escalationKEEP as one dark switchcompanyoff (on at the Willows)BrakesNew customer-facing behavior, not per building
Property.transferMissedOutreachEnabledTell someone when a transfer rings outDELETEon (today: on at Camellia only)An unanswered ask for a human must always surface
Property.promiseLedgerCallerOutboundEnabledText the caller when a promise is keptKEEP as one dark switchcompanyoffBrakesNew behavior, one switch, ships dark
Property.twilioNumbersThe phone numbers that ring hereKEEPbuilding or company attachmenttoday's numbersCompany · ConnectionsNumbers attach to a place; shown, not typed
PHONE_TO_PROPERTY_MAP / PROPERTY_TO_PHONE_MAPCode fallback for number routingDELETEhardcoded literalsUnknown numbers must refuse, never guess a building
Property.tourRequestSmsEnabledText a receipt after a tour requestKEEP as one dark switchcompanyoffBrakesNew customer-facing behavior
Property.smsShadowModeWatch texts without sendingMERGE into watch vs sendcompany + building overridesend (Yale explicitly send)Company · Go-live statusShadow mode is a stage, not a separate flag
Property.vendorVoiceCallingEnabledClara phones vendorsKEEP as one dark switchcompanyoff (on at the Willows)BrakesNew behavior, still baking
derived pm_phone_number, is_after_hours, office_hours_known, office_closed_directive, leasing_desk_availableWhat the phone agent is told each callDELETEcomputed per callResolver output, never a stored setting
ElevenLabs agent ids and promptsWhich voice agent answersKEEPbrakepinned in the repoModel wiring, never a customer setting

Tours

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
tourMinLeadMinutesHow much notice a tour needsMOVE to company, SURFACEcompany + building override60 minutesCompany · ToursFede's ruling: a company rule with building exceptions
tourDayPolicy.<day>.allowSameDaySame-day toursMERGE into the notice rulecompany + building overrideallowed every dayCompany · ToursSame-day is a notice length, not a second switch
tourDayPolicy.<day>.minLeadMinutesNotice on a particular weekdayMERGE into the notice rulebuilding overrideinherits the company ruleCompany · ToursOne rule with a weekday exception row
tourDurationMinutesHow long a tour blocksMOVE to companycompany + building override30 minutesCompany · ToursTyped once for a portfolio
postTourFollowUpDelayMinutesWait before sending the apply linkMOVE to companycompany + building override60 minutesCompany · ToursCompany practice, not a building trait
postTourFollowUpChannelHow the apply link is sentSURFACE, MOVE to companycompanythe channel they usedCompany · ToursHalf this pair is already on screen; finish it
PropertyKnowledge.tourUrlSelf-booking linkSURFACEbuildingnoneBuilding · IdentityA link per building, currently invisible
ConfirmationReviewRecipient.name / .phoneWho reviews tour confirmationsMERGE into Who answerscompany + building overridenoneCompany · Who answersAnother destination field in its own table
tourWindowMatchingEnabled (three copies)Only offer slots in the window they asked forKEEP as one dark switchcompanyoff (on at the Willows)BrakesOne switch, not three copies at three levels
tourDeciderMode, tourScheduleChokepointEnabled, tourPipelineV2Enabled, tourChannelMatchEnabled, tourTemplateDeferralEnabled, tourPastDateTruthEnabled, useAgentTourProcessingSeven switches over the tour code pathDELETEthe on-state each already resolvesCorrectness fixes; the old paths are bugs, not options
escalatedTourBookingEnabledBook a tour while a human holds the threadKEEP as one dark switchcompanyoff (on at Camellia)BrakesNew behavior, already baking at Camellia
tour.gapMinutesMinutes between back-to-back showings for one hostNEW Proposed 2026-09-13company + building override15 minutes (Situs Group's virtual assistants apply this by hand today)Company · ToursSettings, not prompt; defaults ship dark, no customer value changes (Fede, Sep 13)
tour.minBookingNoticeMinutesMinimum notice a caller needs to book a tour (a slot starting sooner than this is not offered)NEW Proposed 2026-09-13company + building override60 minutes (default today's lead-time buffer in the availability math; Situs Group wants 120)Company · ToursSettings, not prompt; defaults ship dark, no customer value changes (Fede, Sep 13)
tour.reminderLeadMinutesHow long before the tour the 'still coming?' reminder text goes to the prospectNEW Proposed 2026-09-13company + building override60 minutes (today's constant)Company · ToursSettings, not prompt; defaults ship dark, no customer value changes (Fede, Sep 13)

Built and reverted 2026-09-13: a first attempt wired a setting into the reminder timer; Fede ruled no settings changes before the refactor, so all three stay proposed until the registry can hold values.

Leasing intake, applications and leases

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
applicationLinkThe apply-now linkMOVE to companycompany + building overridetoday's linkCompany · ApplicationsOne portal for most portfolios
leasingSource + OrganizationSettings.availabilitySourceWhere "what's available" comes fromMERGE, MOVE to companycompany + building overrideeach building's current sourceCompany · AvailabilityThe code already says this is a company choice
publicListingsSync.url / .mode, OnsitePricingConfig.*Listings page and pricing feedMERGE into the availability sourcebuilding overridetoday's configCompany · AvailabilityThree feeds, one question: which source
Property.websiteUrlThe building's websiteKEEPbuildingtoday's URLBuilding · IdentityA fact about the building
PropertyKnowledge.leasePolicyLease terms, deposits, feesSURFACE, MOVE to companycompany + building overridetoday's policy, absent = not enrolledCompany · ApplicationsClara quotes it; a manager must see it
newLeaseTemplateNameWhich lease document goes outSURFACEcompany + building overridenone, fails closedCompany · ApplicationsFails closed silently today
smsConsentModeHow texting consent is capturedMOVE to companycompany onlysingle opt-inCompany · MessagingConsent is company policy; buildings may not differ
jurisdictionStateCodeWhich state's rules applyKEEPbuildingderived from the addressBuilding · IdentityGera's model fixes this at the building
preferenceCaptureEnabledAsk and remember what they wantKEEP as one dark switchcompanyoff (on at the Willows)BrakesNew behavior, still baking
autoLinkCoApplicantsLink co-applicants into one householdDELETEon (today: on at the Willows only)An engine decision; never a switch or a button
statedNameWinsEnabledA stated name replaces a stored oneDELETEon (today: Willows only)Identity correctness, decided in the engine
newLeasePipelineEnabled, autonomousLeaseSendingEnabled, autoDraftLeaseOnApprovalEnabledHow far an approved application goes by itselfMERGE into area statuscompany + building overrideeach building's current combinationCompany · Go-live statusThree booleans, one graduation question
realpageIlmDrainWindowActiveMid-migration off RealPage leadsDELETEoff everywhereOne-off migration marker, no live drain

Channels: email, calendar, texting, the PMS

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
Property.emailIntegrationThe mailbox Clara reads and replies fromMERGE into Connect Microsoft 365 / Googlecompany, attached per buildingtoday's connected mailboxCompany · ConnectionsFede's ruling: one connect button at the company
Property.leasingCalendarThe calendar tours land onMERGE into the same buttoncompany, attached per buildingtoday's connected calendarCompany · ConnectionsSame sign-in, same account, one button
emailIntegration.allowedClassificationsWhich mail Clara may act onSURFACEcompanytoday's listCompany · ConnectionsGoverns what Clara touches in a real inbox
Property.propertyEmail + inboundEmailAddressesThe building's own email addressesMERGE (one list), KEEPbuildingtoday's addressesBuilding · IdentityIdentity of the building; its fallback role removed
sendGridSenderIdentityFallback sender name and addressDELETEClara at clara@propflowai.coNever varied; a constant, not a setting
Property.emailShadowModeWatch email without sendingMERGE into watch vs sendcompany + building overridesendCompany · Go-live statusSame idea as the SMS shadow switch
Property.messagingDeliverySend through us or through the PMSMOVE to company, SURFACEcompany + building overridethrough us (PMS at the Willows)Company · MessagingA per-customer channel decision
pmsMessagingSync.*Pull PMS message threads inKEEP as one dark switchcompanyoffBrakesNew behavior; the seat details are connection data
appfolioTrustedSendersWhich AppFolio mailers are trustedDELETEtoday's list becomes the constantAppFolio's own addresses are the same for everyone
operationalDataSendersSenders to read but never answerSURFACE, MOVE to companycompany + building overridenoneCompany · MessagingSilence is a rule someone must be able to see
collectionsCorrespondentsThe law firm's addresses and unit formatSURFACE, MOVE to companycompanyabsent, parser offCompany · MessagingOne firm per company, not per building
AppFolio credentials (database, clientId, clientSecret)The PMS connectionKEEPcompanytoday's connectionCompany · ConnectionsAlready right
pmsAppfolioAccount.*, pmsSource, pmsExternalId, af.propertyId, rentRollSourceWhich PMS record this building isKEEP, SURFACE read-onlybuildingtoday's idsBuilding · IdentityIdentity, never inherited, never typed twice
Property.autoSyncEnabledApply rent-roll updatesDELETEonNot applying the rent roll is stale data
User.ignoredPropertyIdsWhich PMS properties we ignoreMOVE to companycompanytoday's list from the company's adminCompany · Team & propertiesTwo admins must not see two portfolios
AppSettings.emailsEnabled, .emailShadowMode, .smsEnabled, .testPhonesPlatform email and text brakesKEEPbrakeemail on, shadow off, SMS onBrakesIncident brakes, not customer settings
OrganizationSettings.emailsEnabled / .smsEnabled / .emailShadowModeThe same three at company levelMERGE into the platform brakesbraketoday's resolved on-stateBrakesOne key resolved nearest-wins, not three copies

Camellia carve-out (Fede, Sep 9, verbatim: do not break Camellia). Camellia's existing per-building mailbox and tour-calendar connections keep working exactly as today: no re-consent, no token change, no screen change for Camellia's people. The one-button "Connect Microsoft 365 / Google" is for new, company-level customers. If merging the two buttons cannot be done without touching Camellia's connections, Camellia stays on today's two-button screen and the change says so. Proven at the Willows with real reads before and after; Camellia is only read after deploy.

Knowledge, fees and policies

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
PropertyKnowledge.sections[]The policies Clara answers fromKEEPbuilding, company defaults inheritedtoday's sectionsBuilding · KnowledgePolicy per building; company sections add, not replace
KnowledgeSection.fairHousingScreenedAt, .retiredAtScreening stamp and retirementKEEPbrakesystem-setSystem stamps, never a person's setting
amenities.community / .unit, neighborhood.*, inUnitLaundry, utilities.*What the building has and who pays whatKEEPbuildingtoday's valuesBuilding · KnowledgeFacts about the building itself
concessions[], concessionSourceMove-in specialsKEEPbuildingtoday's specialsBuilding · KnowledgeChanges per building per month
freeMonthAppliesToWhich month is freeSURFACEbuildingunset — Clara confirmsBuilding · KnowledgeClara asks today because nobody can answer it
pricingDetails.* (deposits, fees, late fees, parking, pets)The fee scheduleMOVE to companycompany + building overrideeach building's current scheduleCompany · ApplicationsFee policy is company policy with local exceptions
pricingDetails.petPolicy.*Pet rulesMOVE to companycompany + building overridetoday's rulesCompany · ApplicationsSet once, overridden where a building differs
PropertyKnowledge.propertyNameName inside the knowledge baseDELETEthe building's nameA second copy of the building's own name

Renewals

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
renewalAutoStartDaysBeforeLeaseEndHow early a renewal opensMOVE to companycompany + building override90 daysCompany · RenewalsOne renewal calendar for the portfolio
RenewalPolicy.rentStrategy, .maxIncreasePercent, .maxIncreaseAmount, .termOptions, .mtmPremium, .delinquencyThresholdMonthsRenewal termsMOVE to companycompany + building overrideeach building's current policyCompany · RenewalsYale runs with no terms because it was never typed
renewalEligibility.maxLatePayments / .lateGraceDaysWhen a renewal stops being automaticMOVE to company, SURFACE grace dayscompany + building overridetoday's value; grace 10 daysCompany · RenewalsHalf the pair is hidden today
RenewalPolicy.voiceCallEnabledCall the tenant about a renewalSURFACE, MOVE to companycompany + building overrideonCompany · RenewalsOn by default and invisible; that is backwards
autonomousRenewalEnabled (property and eligibility copies)Do renewal offers actually sendMERGE into area statuscompany + building overrideCamellia live, Willows drafts, elsewhere draftsCompany · Go-live statusThe graduation question, asked once per area
autonomousHoldoverEnabledConvert a lapsed lease to month-to-monthKEEP as one dark switchcompanyoff (on at Camellia, global arm on)BrakesMoves money; Fede's call, one switch
renewalLapseDigestEnabledDaily "no answer" listSURFACE, MOVE to companycompany + building overrideoffCompany · NotificationsA report belongs with the other reports
renewalExecutionCcEmailsWho is copied when a lease is signedMOVE to companycompany + building overridetoday's list, else the renewal contactCompany · NotificationsAccounting is a company function
RenewalArmState.autoStart, .autonomousSending, .collectionsHalt, .autoStartExcludedPersonIdsMaster renewal and collections brakesKEEPbrakeauto-start on, sending on, collections runningBrakesIncident brakes; script-only by design
RenewalArmState.workflowOwnedDispatch, .stableWorkflowIdentityWhich renewal plumbing runsDELETEon, including YaleOne code path, chosen; allowlists are scaffolding

Maintenance and turnovers

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
aiAutoApproveThresholdDollar limit Clara approves without askingMOVE to companycompany + building override$500Company · Turnovers & maintenanceA spending rule set by the company
turnoverPolicy.autoDispatchCostCapDollar limit for turnover workMERGE into the approval limitcompany + building overridefalls back to $500 todayCompany · Turnovers & maintenanceTwo ceilings that shadow each other
handymanVendorIdsThis building's handymenKEEPbuildingtoday's listBuilding · IdentityPeople who actually go to that building
Property.inHouseHandymanIdLegacy single handymanDELETEemptySuperseded by the handymen list
preferredVendors (building and company)Preferred vendors by tradeKEEPcompany + building overridetoday's rostersCompany · VendorsThe one field that already inherits correctly
vendorJobReferenceModeWhich number vendors quoteMOVE to companycompany + building overridework orderCompany · VendorsA back-office convention, company-wide
turnoverPolicy.unitAvailabilityDays, .inspectionDelayDays, .inspectionTimeOfDay, .inspectionDurationMinutesTurnover timingMOVE to companycompany + building overridetoday's values; 10:00 and 60 minutesCompany · Turnovers & maintenanceYale has none because it was never typed
turnoverPolicy.defaultTrades, .defaultVendors, .finishingTradesWhat a turnover starts withMOVE to companycompany + building overridetoday's lists; housekeeping lastCompany · Turnovers & maintenanceA company make-ready recipe
turnoverPolicy.chargeKnowledge, .conditionCoverageRoomsMove-out charge wording and room coverageSURFACE, MOVE to companycompany + building overridethe shipped charge vocabularyCompany · Turnovers (advanced)Charges reach residents; must be readable
PropertyTurnoverSettings.customTaskTypes[]Extra turnover task typesMOVE to companycompany + building overridenoneCompany · Turnovers & maintenanceA vocabulary, defined once
boundedPhotoDeferralEnabledFile the work order after one photo askDELETEon (today: Willows only)Endless photo asks with no work order is a bug
autonomousMaintenanceEnabled, autonomousVendorDispatchEnabled, autonomousTurnoverEnabled, autonomousListingPublishEnabledDo maintenance and turnover messages sendMERGE into area statuscompany + building overrideeach building's current combinationCompany · Go-live statusFour booleans, one graduation question

Notifications, reports and follow-ups

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
pmActionReminders.enabled / .maxReminders / .intervalHoursReminders about things waiting on youMOVE to companycompany + building overrideon, 3 reminders, every 24 hoursCompany · NotificationsA personal-feeling setting, typed once
ownerReportRecipientsWho gets the weekly owner reportMOVE to companycompany + building overridetoday's list, else the building inboxCompany · NotificationsOwners differ per building; defaults do not
ownerReportWindowDaysHow much the owner report coversSURFACE, MOVE to companycompany7 daysCompany · NotificationsHidden half of a visible report
escalationOwnerCcEmailWho is copied on a decision requestSURFACE, MOVE to companycompany + building overridenoneCompany · NotificationsA copy list, not a destination decision
pmNotificationModeHow much the manager hearsSURFACE, MOVE to companycompany + building overrideeverythingCompany · NotificationsVolume control a customer will want on day one
leasingActivityChannel / .leasingActivityMentionWhere leasing activity is postedSURFACE, MOVE to companycompanyno postingCompany · NotificationsOne Slack destination per company
autonomousLeasingDigestEnabled, autonomousLeasingReportingDigestEnabledThe weekly digestsMERGE into area statuscompany + building overrideon at Camellia, off elsewhereCompany · Go-live statusSending is the same question as elsewhere
escalationCoworkerModeEnabledPark the question instead of answering itKEEP as one dark switchcompanyoffBrakesNew behavior a customer would notice
escalationEmailFormat, escalationRelayRephraseEnabled, topicScopedHoldUnmuteEnabled, escalationBakeApprovalRequired, sameThreadMemoryAckEnabledFour escalation flags and one bake gateDELETE four, KEEP the memory acknowledgement darkthe on-state each resolvesOne layout, one relay, no human-click approval gate
FollowupCadenceOverride.touches[]How Clara chases each kind of personKEEP, move the screencompany + building overridetoday's cadencesCompany · Follow-upsEditing a company rule from a building page misleads
FollowupTouchBudgetOverride.budgetHow often one person may be contactedKEEP, move the screencompanytoday's budgetsCompany · Follow-upsSame reason; it always applied company-wide
AppSettings.vendorQuoteEmails / .vendorDispatchEmailsVendor quote and dispatch mailKEEPbrakeboth onBrakesPlatform brakes, already on a staff screen

Areas, stages and account

SettingPlain-English nameVerdictLevelDefault = todayScreenReason
EnabledModules[...] + Property.capabilityStage.*Area status: off, drafts, or liveMERGE — the survivorcompany + building overridemodules as shipped; stage live; sending as each arm reads todayCompany · Go-live statusOne question per area replaces seventeen booleans
the four turnIntegrity* gatesTurn-integrity kill switchesDELETEon (never written anywhere)Correctness guards; the off-state is the bug
env EMAIL_NTV_FIRM_ROUTING_ENABLEDTreat a notice email as a firm move-outMOVE off env, KEEP darkcompanyoffBrakesBehavior must not depend on a deploy variable
env CLARA_TRACE_CAPTURE_PROMPT, env TEMPORAL_NONDETERMINISM_METRICS_ALERTTrace capture and a metrics drainDELETEcapture on, drain never armedOne always wanted, one never used
Property.isTest, Organization.isSandbox, .purpose, .status, .plan, .pmsWhat kind of account this isKEEPcompany / buildingtoday's valuesCompany · Team & propertiesAccount identity, already correct
Organization.name, .subscription.*Company name and billingKEEPcompanytoday's valuesCompany · Team & propertiesAlready right
OrganizationSettings.logoUrl, .primaryBrandColorBrand logo and colourDELETEunusedDeclared, never read, nobody would miss them
AppSettings.setupGuideState, .askClaraStateWhether the guide and helper are expandedMOVE to personpersonexpanded / smallPerson · Account (implicit)Per-person state stored once for the whole platform
Person.displayName, name parts, email, phone, avatarKeyYour name, contact and photoKEEPpersontoday's valuesPerson · AccountAlready right
UserPreferences.displayZoneWhose clock times are shown inKEEPpersonthe building's clockPerson · AccountAlready right
User.dashboardLayout, .dashboardCardWindows, dashboard windowsYour dashboard arrangementKEEPpersonshipped defaultsPerson · Account (implicit)Already right
PersonPreferences.languagePreference, .smsOptOut, .emailOptOut, .timezoneLanguage, opt-outs, timezoneKEEPpersonEnglish; detected; consent table decidesSystem-detected; the consent table is the real gate
PersonRole.role / .scope, User.assignedPropertyIdsWho can do what, whereKEEPperson, scoped to company or buildingtoday's grantsCompany · Team & propertiesMatches Gera's roles; grants, not a user class
PermissionOverrideStaff flips of the permission matrixKEEPbrakematrix defaultsBrakesInternal tool, not a customer setting

Added 2026-09-13 — switches this page did not yet cover

Second read, 2026-09-13, live from the production table, read-only. The Sep 9 pass had no Western Slope column and predates the two switches that now implement its dark hold. absent means the code default applies.

SwitchWhat it does when onWestern SlopeCamelliaYale 25Verdict
capabilityStage.leasingshadow/off suppresses every outbound — but only on the email path; no SMS call site reads itshadowabsentabsentCONVERT — the one visible company state; drafts = portfolio-arrived records only
emailShadowModeClara never sends an email reply — to anyone, including someone who emailed her directlytruefalsefalseREMOVE — folds into the state above
tourRequestSmsEnabledA caller who asks for a tour by phone gets a text receipt. Off = silencefalseabsentabsentREMOVE — a receipt is not a preference
CONFIG / PMS_SYNC_PAUSE#<company>Row present = the AppFolio sync and the availability heartbeat skip the whole companypaused since Sep 12, no reason recordedabsentabsentREMOVE — "off" must still mean ingest; require a reason and an alarm
publicListingsSyncThe 15-minute listings job reconciles what is actually availableabsentsetsetREMOVE — set it; stale availability is a bug
teamTourEmailAfterCallEnabledA tour booked on a call also emails the teamtrueabsentabsentREMOVE — turns it on everywhere
tourInviteV2Enabled, newLeasePipelineEnabledNewer invite copy; an approved application creates a next-steps itemabsent / absentabsent / trueabsentREMOVE — finish the migration
CONFIG / OPERATING_MODE_ARMGlobal arm plus a property allowlistnot on the listnot on the listthe only entryREMOVE the allowlist — a per-customer list in production
propertyEmail / escalationOwnerEmailWhere team mail and escalations landboth PropFlow addressesthe client's own inboxPropFlowCONVERT — a visible company setting, and wrong at Western Slope today
AppSettings.testModeNothing. Zero read sites anywheretrue on the global row — reads as "production is in test mode"REMOVE
Company copies of emailsEnabled / smsEnabledNothing — the resolver has no company fallback; the real brakes are the platform row, both onboth falseREMOVE the rows — they read as a gate and are not one
Organization.claraLiveNothing. Written and shown in the back office, no code reads itabsentabsentabsentCONVERT — this is where the one dark hold belongs
Follow-up cadence armThere is none. The per-building arm was deleted Jul 27; the 5-touch cadence ships on everywherea portfolio-arrived lead that engages enrols and gets texted todayKEEP AS THE DARK HOLD — wire the hold here first
Unmapped-line fallback (voice)A dialled number that maps to no property adopts the caller's own on production; staging refuses outrightproduction-wideREMOVE — refuse on production too

Counts from this pass: 14 REMOVE, 3 CONVERT, 1 KEEP as the single dark hold. The one genuine hold — portfolio-arrived records are recorded, never replied to — does not exist in the code today and has to be built before any of the fourteen can come out. Facts read live; two inferences are labelled in the working file ~/agents/006/hidden-flags-audit-2026-09-13.md.

6. Decisions for Fede

Only the ones that change what a customer experiences. Each is a yes or no, with a recommendation.

#DecisionRecommend
1Fold the office line, the leasing-desk line, the emergency fallback, the escalation owner and the building inbox into one setting — "when a caller asks for a person, ring this number or send the message to this address"?Yes
2Always tell someone when a call transfer rings out unanswered? Today that only happens at Camellia; everywhere else the call is only written to a log.Yes
3Replace the seventeen on/off switches per building with one status per area — off, drafts only, or live — set for the company and overridable for one building?Yes
4Link co-applicants into one household everywhere? Today it happens only at the Willows, which is why cosigner applications sat unlinked at Camellia.Yes
5File the work order after one photo request everywhere? Today only the Willows does; elsewhere Clara can ask for a photo forever and never file anything.Yes
6Let the name someone says on a call replace the stored name everywhere? Today only at the Willows.Yes
7Make the tour notice rule a company rule with per-building exceptions, folding same-day and per-weekday notice into that one rule?Yes
8One "Connect Microsoft 365 or Google" button at the company, instead of separate email and calendar buttons on every building?Yes
9Move renewal terms, turnover timing, fees and reminder settings up to the company, so an 82-building customer types them once? Yale runs today with no renewal terms and no turnover policy at all because nobody typed them.Yes
10Put Yale on the same renewal plumbing as Camellia and the Willows? It is the one property left off that list today.Yes
11Make "which properties we ignore" a company choice instead of a personal one, so two admins at one company see the same portfolio?Yes
12Move the follow-up chase settings off the building page onto the company page? They already rewrite every building in the company from there, which is easy to miss.Yes
13Stop routing an unrecognised incoming phone number to the caller's own building, and refuse instead?Yes
14Delete the six settings nothing reads — brand logo, brand colour, the on-site-versus-corporate label, the legacy single-handyman pointer, the duplicate building name, and the fallback sender identity?Yes
15Keep the five unfinished behaviors as one dark switch each at the company — call the person back after a failed call, remember what a prospect wants, phone vendors, convert a lapsed lease to month-to-month, treat a notice email as a firm move-out — rather than a switch per building?Yes

Numbers 2, 4, 5, 6 and 10 turn on at properties where the behavior is off today. They are correctness fixes, so under the Sep 3 rule they ship without a switch — but they still ship dark to prod first, get tested at the Willows, and only reach a customer property with your explicit go.

7. The plan, in phases

Aligned to Gera's ladder, and honest about what does not need it. His four-part step mechanic applies throughout: write the new rows while nobody reads them, prove parity offline against recorded inputs, cut one reader at a time behind the existing fail-closed switch, then strip the old field and leave a tombstone. Camellia is flipped last at every step and its replay stays byte-identical the whole way.

Phase 0 — inside the Western Slope window, on today's rails

Needs nothing from the ladder. This is the deep clean.

Proof: a regression test per deleted switch that fails on the old code; Camellia replay byte-identical; before-and-after screenshots of every settings screen at the Willows; the CI check failing on a deliberately added switch.

Phase 1 — the registry and the rows, dark (Gera steps 0–2, targeting Oct 13–23)

Proof: scope-parity-replay.ts mismatches at zero against recorded inputs; one live row per key per node; the roster stamp complete and staying complete on the nightly check.

Phase 2 — the company screens (Gera step 7, keys one at a time)

Proof: per key, the pre-pin keys resolve at the company; Camellia byte-identical after each cut; the settings screen shows the same effective value before and after.

Phase 3 — Western Slope goes live on one switch (Gera steps 3a → 3b → 4 → 5a, Nov 18 – Dec 17)

Proof: the refusal naming the missing keys, then a real inbound call and a real inbound text answered on their line, and a row on the Western Slope go-live tracker with the production evidence link.

Phase 4 — after the window (Gera steps 8–10)

Proof: the lock sweep's dry run and pre-image; the drift guard failing when a tombstoned name is reintroduced; the arms registry with no row past its delete-by date.

8. Conflicts with Gera's design

Two, both small, both needing one edit to his registry rather than a change to this plan. Everything else in this document lands on keys he has already declared — capability_stage.<function>, observe.sms, observe.email, office_hours, holiday_policy, tour.minLeadMinutes, tour.dayPolicy, tour.durationMinutes, consent_model, application_link, new_lease_template, jurisdiction, vendor_roster.<trade>, outreach.cadence, fees.*, pets.policy, listings.feed_url, pms_delivery_mode. Three more are proposed and not yet in his registry: tour.gapMinutes, tour.minBookingNoticeMinutes and tour.reminderLeadMinutes (Fede, Sep 13) — same shape, nearest-wins, pending his registry entry.

  1. Four keys where Fede ruled there is one. His registry declares office_phone, transfer.<role>, escalation.owner and escalation.cc as separate keys. Fede's ruling of Sep 9 is that "when a caller asks for a person" is one setting whose value is either a number to ring or an address to send the taken message to. Proposed resolution: implement it as transfer.<role> with a number-or-address value, retire escalation.owner and escalation.cc as separate keys, keep emergency_phone distinct for life safety, and keep office_phone only as the public number Clara reads out — a building fact, not a destination. One registry edit, Gera's call.
  2. Our master brakes sit above the company, and his model has no level there. The platform email and SMS kill switches, the renewal auto-start and send arms, the collections halt and the permission overrides all apply above any one customer. His registry deliberately has no platform tier — a building is stopped only by its own company's lock or a statutory floor. Proposed resolution: these are not settings and never become registry keys. They stay operational brakes on their own internal page, outside the settings model, and nothing in the customer settings path reads them. Naming it here so nobody adds a platform tier later to make them fit.

Checked and found aligned, not conflicting: which state's rules apply stays a building fact that is never inherited; texting consent stays a company policy a building may not override; the vendor roster keeps the company-then-building inheritance it already has; the follow-up cadences stay company rows.

PropFlow Docs