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.
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:
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.
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.
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.
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.
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.
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.
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.
Every switch gets one of three verdicts.
| Switch | What it gated | Why it goes |
|---|---|---|
tourPipelineV2Enabled | Span-verified tour dates | On when unset; the legacy path is not a supported behavior |
tourChannelMatchEnabled | Reply on the channel the person used | On when unset; answering on the wrong channel is a bug |
tourTemplateDeferralEnabled | Suppress a duplicate tour-confirm text | On when unset; the duplicate is a bug |
turnIntegritySingleWriter | One customer-facing writer per turn | On when unset; two writers is the double-send bug |
turnIntegrityReviewGate | Anomaly review before sending | On when unset |
turnIntegrityTypedToolResults | Typed tool errors | On when unset, no confirmed production read site |
turnIntegrityOutputGate | Output gate | On when unset, no confirmed production read site |
tourPastDateTruthEnabled | Refuse to confirm a tour in the past | On at every live property; confirming yesterday is a bug |
tourDeciderMode | Which tour-confirm code path runs | Two code paths for one job; keep the newer, delete the mode |
tourScheduleChokepointEnabled | Atomic tour writes | The non-atomic path is the deadlock; not an option |
stableWorkflowIdentity | Deterministic renewal workflow ids | Armed by default since Sep 3; the rollback list is scaffolding |
workflowOwnedDispatch | Workflow owns the send | One dispatch owner, chosen; the allowlist is scaffolding |
statedNameWinsEnabled | A stated name overwrites a stored one | Identity correctness; the engine decides, not a per-building flag |
autoLinkCoApplicants | Merge co-applicants into one household | Engine decision, never a toggle (no manual-decision UI) |
transferMissedOutreachEnabled | Page a human when a transfer rings out | A caller asking for a human must always leave a trace |
boundedPhotoDeferralEnabled | File the work order after one photo ask | Endless photo asks with no work order is the bug |
autoSyncEnabled | Apply rent-roll updates | Not applying the rent roll is stale data, not a choice |
escalationEmailFormat | Which escalation email layout | Pick one layout; a per-building format is not a product |
escalationRelayRephraseEnabled | Rephrase a staff answer | One relay behavior; off everywhere, unfinished, nothing waiting on it |
topicScopedHoldUnmuteEnabled | Unmute when the matter closes | Staying muted after the matter closes is a bug |
escalationBakeApprovalRequired | Hold escalation emails for Slack approval | Temporary bake scaffolding, and a human-click gate we do not build |
useAgentTourProcessing | Agent-driven tour processing | Superseded by the tour pipeline; off everywhere |
realpageIlmDrainWindowActive | Mid-migration off RealPage leads | One-off migration marker, no live drain |
operatingMode | On-site vs corporate label | Schema only; nothing reads it |
env CLARA_TRACE_CAPTURE_PROMPT | Store the prompt on traces | On; we always want the prompt on a trace |
env TEMPORAL_NONDETERMINISM_METRICS_ALERT | Non-determinism metrics drain | Never armed; dead code path |
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.
| Switch | The new behavior | Where it may be baked |
|---|---|---|
voiceCallbackEnabled | Clara calls a person back after a failed voice escalation | Willows, then one Camellia bake with Fede's go |
promiseLedgerCallerOutboundEnabled | Text the caller back when a promise is kept | Willows |
tourRequestSmsEnabled | SMS receipt after an unconfirmed tour request | Willows |
vendorVoiceCallingEnabled | Clara phones vendors | Live at the Willows today |
preferenceCaptureEnabled | Ask for and remember beds, budget, move-in | Willows |
escalatedTourBookingEnabled | Book a tour on a thread a human is holding | Sandbox only today |
sameThreadMemoryAckEnabled | Acknowledge a staff answer in the same thread | Bench only today |
escalationCoworkerModeEnabled | Park the question instead of answering for the PM | Willows |
autonomousHoldoverEnabled | Convert a lapsed lease to month-to-month and rebill | Test property only; money-moving, Fede's call |
tourWindowMatchingEnabled | Only offer slots inside the window the prospect asked for | Three copies of this switch collapse to one |
pmsMessagingSync.enabled | Pull PMS guest-card threads in and reply as that seat | Situs Group candidate |
| notice-to-vacate firm routing today env EMAIL_NTV_FIRM_ROUTING_ENABLED | Treat a formal notice email as a firm move-out | Moves out of an env var onto the registry |
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 today | Becomes |
|---|---|
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, autoDraftLeaseOnApprovalEnabled | the 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 |
vendorTransferFirstEnabled | the 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.
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.
| Switch | Camellia | Yale 25 | Willows | Verdict |
|---|---|---|---|---|
tourPastDateTruthEnabled | true | true | true | DELETE |
tourDeciderMode | v2 | absent | v2 | DELETE |
autoSyncEnabled | true | absent | absent | DELETE |
transferMissedOutreachEnabled | true | absent | absent | DELETE — turns it on at Yale and the Willows |
boundedPhotoDeferralEnabled | absent | absent | true | DELETE — turns it on at Camellia and Yale |
statedNameWinsEnabled | absent | absent | true | DELETE — turns it on at Camellia and Yale |
autoLinkCoApplicants (leasing settings) | absent | absent | true | DELETE — turns it on at Camellia and Yale |
useAgentTourProcessing | false | absent | absent | DELETE |
escalationBakeApprovalRequired | false | absent | false | DELETE |
escalationEmailFormat | absent | absent | coworker | DELETE — pick one layout |
escalationRelayRephraseEnabled | absent | absent | absent | DELETE |
topicScopedHoldUnmuteEnabled | absent | absent | absent | DELETE |
tourScheduleChokepointEnabled | absent | absent | absent | DELETE — keep the atomic path only |
realpageIlmDrainWindowActive | absent | absent | absent | DELETE |
operatingMode | absent | absent | absent | DELETE |
workflowOwnedDispatch (global row) | on, limited to Camellia + Willows — Yale is not on the list | DELETE — one dispatch owner, Yale included | ||
stableWorkflowIdentity (global row) | on, same two properties | DELETE — armed by default since Sep 3 | ||
tourPipelineV2Enabled, tourChannelMatchEnabled, tourTemplateDeferralEnabled, the four turnIntegrity* gates | not stored anywhere — no property row and not on the global row, so every one of them is running on its code default of ON | DELETE — seven switches nobody has ever set | ||
voiceCallbackEnabled | absent | absent | true | KEEP dark |
vendorVoiceCallingEnabled | absent | absent | true | KEEP dark |
preferenceCaptureEnabled | absent | absent | true | KEEP dark |
sameThreadMemoryAckEnabled | absent | absent | true | KEEP dark |
escalatedTourBookingEnabled | true | absent | absent | KEEP — already on at Camellia |
promiseLedgerCallerOutboundEnabled | false | absent | absent | KEEP dark |
tourRequestSmsEnabled | absent | absent | absent | KEEP dark |
escalationCoworkerModeEnabled | absent | absent | false | KEEP dark |
autonomousHoldoverEnabled | true | absent | absent | KEEP — global holdover arm is also on |
tourWindowMatchingEnabled | absent | absent | true (leasing settings) | KEEP — three copies become one |
handoffMode | autonomous | absent | coworker | CONVERT to a company voice setting |
smsShadowMode / emailShadowMode | absent / false | false / false | absent / absent | CONVERT to watch-vs-send per channel |
capabilityStage | absent | absent | absent | CONVERT — never set; the code default of live is the real value |
autonomousRenewalEnabled | true | absent | false | CONVERT to area status |
autonomousTurnoverEnabled | true | absent | true | CONVERT |
autonomousListingPublishEnabled | true | absent | false | CONVERT |
autonomousMaintenanceEnabled | false | false | true | CONVERT |
autonomousVendorDispatchEnabled | absent | absent | false | CONVERT |
autonomousLeasingDigestEnabled / ...ReportingDigestEnabled | true / true | absent | absent | CONVERT |
newLeasePipelineEnabled / autoDraftLeaseOnApprovalEnabled / autonomousLeaseSendingEnabled | true / absent / absent | absent | true / true / absent | CONVERT |
vendorTransferFirstEnabled | true | absent | absent | CONVERT into "who answers" |
messagingDelivery / vendorJobReferenceMode / smsConsentMode | absent | absent | pms / work_order / absent | already 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.
Per switch, in this order, one switch per pull request:
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:
*Enabled, *Mode or *Stage field appears on the building or leasing-settings type with no registry row;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.
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.
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
Property.officePhone | Who answers when a caller asks for a person | MERGE into Who answers | company + building override | today's office number | Company · Who answers | One decision, not three fields and a hidden fallback |
Property.leasingDeskPhone | Who answers a leasing question | MERGE into Who answers | company + building override | absent — take a message | Company · Who answers | Same decision, a different role slot |
Property.escalationOwnerEmail | Who gets the message when nobody picks up | MERGE into Who answers | company + building override | today's owner, else the building inbox | Company · Who answers | An address is a valid answer to the same question |
PropertyLeasingSettings.renewalContactEmail / .renewalContactPhone | Who hears about a stalled renewal | MERGE into Who answers | company + building override | today's value, else the building inbox | Company · Who answers | A per-topic copy of the same destination question |
Property.vendorTransferFirstEnabled | Who answers a vendor | MERGE into Who answers | company + building override | Camellia: the office; elsewhere Clara | Company · Who answers | A destination choice, not a flag |
PropertyMaintenanceSettings.maintenanceTechPhone | Who answers a maintenance call | MERGE into Who answers | company + building override | today's tech number | Company · Who answers | Same slot pattern, one screen |
Property.emergencyPhone | The life-safety number | KEEP | company + building override | today's number | Company · Who answers | Life safety is its own decision; fallback role removed |
PropertyKnowledge.phone | The number Clara reads out to callers | KEEP | building | today's public number | Building · Identity | Public number is a fact about the building |
PropertyKnowledge.officeHours | Office hours | MOVE to company | company + building override | today's per-day hours | Company · Office hours | Hours gate transfers; a portfolio types them once |
holidayPolicy.observeFederalHolidays | Close on federal holidays | SURFACE | company + building override | observe all eleven | Company · Office hours | It closes the office; must not be database-only |
holidayPolicy.openOn / .extraClosures | Extra open and closed dates | SURFACE | company + building override | none | Company · Office hours | Calendar facts a manager needs to edit |
Property.handoffMode | Whether Clara speaks as herself | MOVE to company, SURFACE | company + building override | Camellia autonomous, Willows coworker, elsewhere coworker | Company · Voice | One company voice, chosen on purpose |
Property.voiceCallbackEnabled | Clara calls back after a failed escalation | KEEP as one dark switch | company | off (on at the Willows) | Brakes | New customer-facing behavior, not per building |
Property.transferMissedOutreachEnabled | Tell someone when a transfer rings out | DELETE | — | on (today: on at Camellia only) | — | An unanswered ask for a human must always surface |
Property.promiseLedgerCallerOutboundEnabled | Text the caller when a promise is kept | KEEP as one dark switch | company | off | Brakes | New behavior, one switch, ships dark |
Property.twilioNumbers | The phone numbers that ring here | KEEP | building or company attachment | today's numbers | Company · Connections | Numbers attach to a place; shown, not typed |
PHONE_TO_PROPERTY_MAP / PROPERTY_TO_PHONE_MAP | Code fallback for number routing | DELETE | — | hardcoded literals | — | Unknown numbers must refuse, never guess a building |
Property.tourRequestSmsEnabled | Text a receipt after a tour request | KEEP as one dark switch | company | off | Brakes | New customer-facing behavior |
Property.smsShadowMode | Watch texts without sending | MERGE into watch vs send | company + building override | send (Yale explicitly send) | Company · Go-live status | Shadow mode is a stage, not a separate flag |
Property.vendorVoiceCallingEnabled | Clara phones vendors | KEEP as one dark switch | company | off (on at the Willows) | Brakes | New behavior, still baking |
derived pm_phone_number, is_after_hours, office_hours_known, office_closed_directive, leasing_desk_available | What the phone agent is told each call | DELETE | — | computed per call | — | Resolver output, never a stored setting |
| ElevenLabs agent ids and prompts | Which voice agent answers | KEEP | brake | pinned in the repo | — | Model wiring, never a customer setting |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
tourMinLeadMinutes | How much notice a tour needs | MOVE to company, SURFACE | company + building override | 60 minutes | Company · Tours | Fede's ruling: a company rule with building exceptions |
tourDayPolicy.<day>.allowSameDay | Same-day tours | MERGE into the notice rule | company + building override | allowed every day | Company · Tours | Same-day is a notice length, not a second switch |
tourDayPolicy.<day>.minLeadMinutes | Notice on a particular weekday | MERGE into the notice rule | building override | inherits the company rule | Company · Tours | One rule with a weekday exception row |
tourDurationMinutes | How long a tour blocks | MOVE to company | company + building override | 30 minutes | Company · Tours | Typed once for a portfolio |
postTourFollowUpDelayMinutes | Wait before sending the apply link | MOVE to company | company + building override | 60 minutes | Company · Tours | Company practice, not a building trait |
postTourFollowUpChannel | How the apply link is sent | SURFACE, MOVE to company | company | the channel they used | Company · Tours | Half this pair is already on screen; finish it |
PropertyKnowledge.tourUrl | Self-booking link | SURFACE | building | none | Building · Identity | A link per building, currently invisible |
ConfirmationReviewRecipient.name / .phone | Who reviews tour confirmations | MERGE into Who answers | company + building override | none | Company · Who answers | Another destination field in its own table |
tourWindowMatchingEnabled (three copies) | Only offer slots in the window they asked for | KEEP as one dark switch | company | off (on at the Willows) | Brakes | One switch, not three copies at three levels |
tourDeciderMode, tourScheduleChokepointEnabled, tourPipelineV2Enabled, tourChannelMatchEnabled, tourTemplateDeferralEnabled, tourPastDateTruthEnabled, useAgentTourProcessing | Seven switches over the tour code path | DELETE | — | the on-state each already resolves | — | Correctness fixes; the old paths are bugs, not options |
escalatedTourBookingEnabled | Book a tour while a human holds the thread | KEEP as one dark switch | company | off (on at Camellia) | Brakes | New behavior, already baking at Camellia |
tour.gapMinutes | Minutes between back-to-back showings for one host | NEW Proposed 2026-09-13 | company + building override | 15 minutes (Situs Group's virtual assistants apply this by hand today) | Company · Tours | Settings, not prompt; defaults ship dark, no customer value changes (Fede, Sep 13) |
tour.minBookingNoticeMinutes | Minimum notice a caller needs to book a tour (a slot starting sooner than this is not offered) | NEW Proposed 2026-09-13 | company + building override | 60 minutes (default today's lead-time buffer in the availability math; Situs Group wants 120) | Company · Tours | Settings, not prompt; defaults ship dark, no customer value changes (Fede, Sep 13) |
tour.reminderLeadMinutes | How long before the tour the 'still coming?' reminder text goes to the prospect | NEW Proposed 2026-09-13 | company + building override | 60 minutes (today's constant) | Company · Tours | Settings, 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.
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
applicationLink | The apply-now link | MOVE to company | company + building override | today's link | Company · Applications | One portal for most portfolios |
leasingSource + OrganizationSettings.availabilitySource | Where "what's available" comes from | MERGE, MOVE to company | company + building override | each building's current source | Company · Availability | The code already says this is a company choice |
publicListingsSync.url / .mode, OnsitePricingConfig.* | Listings page and pricing feed | MERGE into the availability source | building override | today's config | Company · Availability | Three feeds, one question: which source |
Property.websiteUrl | The building's website | KEEP | building | today's URL | Building · Identity | A fact about the building |
PropertyKnowledge.leasePolicy | Lease terms, deposits, fees | SURFACE, MOVE to company | company + building override | today's policy, absent = not enrolled | Company · Applications | Clara quotes it; a manager must see it |
newLeaseTemplateName | Which lease document goes out | SURFACE | company + building override | none, fails closed | Company · Applications | Fails closed silently today |
smsConsentMode | How texting consent is captured | MOVE to company | company only | single opt-in | Company · Messaging | Consent is company policy; buildings may not differ |
jurisdictionStateCode | Which state's rules apply | KEEP | building | derived from the address | Building · Identity | Gera's model fixes this at the building |
preferenceCaptureEnabled | Ask and remember what they want | KEEP as one dark switch | company | off (on at the Willows) | Brakes | New behavior, still baking |
autoLinkCoApplicants | Link co-applicants into one household | DELETE | — | on (today: on at the Willows only) | — | An engine decision; never a switch or a button |
statedNameWinsEnabled | A stated name replaces a stored one | DELETE | — | on (today: Willows only) | — | Identity correctness, decided in the engine |
newLeasePipelineEnabled, autonomousLeaseSendingEnabled, autoDraftLeaseOnApprovalEnabled | How far an approved application goes by itself | MERGE into area status | company + building override | each building's current combination | Company · Go-live status | Three booleans, one graduation question |
realpageIlmDrainWindowActive | Mid-migration off RealPage leads | DELETE | — | off everywhere | — | One-off migration marker, no live drain |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
Property.emailIntegration | The mailbox Clara reads and replies from | MERGE into Connect Microsoft 365 / Google | company, attached per building | today's connected mailbox | Company · Connections | Fede's ruling: one connect button at the company |
Property.leasingCalendar | The calendar tours land on | MERGE into the same button | company, attached per building | today's connected calendar | Company · Connections | Same sign-in, same account, one button |
emailIntegration.allowedClassifications | Which mail Clara may act on | SURFACE | company | today's list | Company · Connections | Governs what Clara touches in a real inbox |
Property.propertyEmail + inboundEmailAddresses | The building's own email addresses | MERGE (one list), KEEP | building | today's addresses | Building · Identity | Identity of the building; its fallback role removed |
sendGridSenderIdentity | Fallback sender name and address | DELETE | — | Clara at clara@propflowai.co | — | Never varied; a constant, not a setting |
Property.emailShadowMode | Watch email without sending | MERGE into watch vs send | company + building override | send | Company · Go-live status | Same idea as the SMS shadow switch |
Property.messagingDelivery | Send through us or through the PMS | MOVE to company, SURFACE | company + building override | through us (PMS at the Willows) | Company · Messaging | A per-customer channel decision |
pmsMessagingSync.* | Pull PMS message threads in | KEEP as one dark switch | company | off | Brakes | New behavior; the seat details are connection data |
appfolioTrustedSenders | Which AppFolio mailers are trusted | DELETE | — | today's list becomes the constant | — | AppFolio's own addresses are the same for everyone |
operationalDataSenders | Senders to read but never answer | SURFACE, MOVE to company | company + building override | none | Company · Messaging | Silence is a rule someone must be able to see |
collectionsCorrespondents | The law firm's addresses and unit format | SURFACE, MOVE to company | company | absent, parser off | Company · Messaging | One firm per company, not per building |
AppFolio credentials (database, clientId, clientSecret) | The PMS connection | KEEP | company | today's connection | Company · Connections | Already right |
pmsAppfolioAccount.*, pmsSource, pmsExternalId, af.propertyId, rentRollSource | Which PMS record this building is | KEEP, SURFACE read-only | building | today's ids | Building · Identity | Identity, never inherited, never typed twice |
Property.autoSyncEnabled | Apply rent-roll updates | DELETE | — | on | — | Not applying the rent roll is stale data |
User.ignoredPropertyIds | Which PMS properties we ignore | MOVE to company | company | today's list from the company's admin | Company · Team & properties | Two admins must not see two portfolios |
AppSettings.emailsEnabled, .emailShadowMode, .smsEnabled, .testPhones | Platform email and text brakes | KEEP | brake | email on, shadow off, SMS on | Brakes | Incident brakes, not customer settings |
OrganizationSettings.emailsEnabled / .smsEnabled / .emailShadowMode | The same three at company level | MERGE into the platform brakes | brake | today's resolved on-state | Brakes | One 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.
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
PropertyKnowledge.sections[] | The policies Clara answers from | KEEP | building, company defaults inherited | today's sections | Building · Knowledge | Policy per building; company sections add, not replace |
KnowledgeSection.fairHousingScreenedAt, .retiredAt | Screening stamp and retirement | KEEP | brake | system-set | — | System stamps, never a person's setting |
amenities.community / .unit, neighborhood.*, inUnitLaundry, utilities.* | What the building has and who pays what | KEEP | building | today's values | Building · Knowledge | Facts about the building itself |
concessions[], concessionSource | Move-in specials | KEEP | building | today's specials | Building · Knowledge | Changes per building per month |
freeMonthAppliesTo | Which month is free | SURFACE | building | unset — Clara confirms | Building · Knowledge | Clara asks today because nobody can answer it |
pricingDetails.* (deposits, fees, late fees, parking, pets) | The fee schedule | MOVE to company | company + building override | each building's current schedule | Company · Applications | Fee policy is company policy with local exceptions |
pricingDetails.petPolicy.* | Pet rules | MOVE to company | company + building override | today's rules | Company · Applications | Set once, overridden where a building differs |
PropertyKnowledge.propertyName | Name inside the knowledge base | DELETE | — | the building's name | — | A second copy of the building's own name |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
renewalAutoStartDaysBeforeLeaseEnd | How early a renewal opens | MOVE to company | company + building override | 90 days | Company · Renewals | One renewal calendar for the portfolio |
RenewalPolicy.rentStrategy, .maxIncreasePercent, .maxIncreaseAmount, .termOptions, .mtmPremium, .delinquencyThresholdMonths | Renewal terms | MOVE to company | company + building override | each building's current policy | Company · Renewals | Yale runs with no terms because it was never typed |
renewalEligibility.maxLatePayments / .lateGraceDays | When a renewal stops being automatic | MOVE to company, SURFACE grace days | company + building override | today's value; grace 10 days | Company · Renewals | Half the pair is hidden today |
RenewalPolicy.voiceCallEnabled | Call the tenant about a renewal | SURFACE, MOVE to company | company + building override | on | Company · Renewals | On by default and invisible; that is backwards |
autonomousRenewalEnabled (property and eligibility copies) | Do renewal offers actually send | MERGE into area status | company + building override | Camellia live, Willows drafts, elsewhere drafts | Company · Go-live status | The graduation question, asked once per area |
autonomousHoldoverEnabled | Convert a lapsed lease to month-to-month | KEEP as one dark switch | company | off (on at Camellia, global arm on) | Brakes | Moves money; Fede's call, one switch |
renewalLapseDigestEnabled | Daily "no answer" list | SURFACE, MOVE to company | company + building override | off | Company · Notifications | A report belongs with the other reports |
renewalExecutionCcEmails | Who is copied when a lease is signed | MOVE to company | company + building override | today's list, else the renewal contact | Company · Notifications | Accounting is a company function |
RenewalArmState.autoStart, .autonomousSending, .collectionsHalt, .autoStartExcludedPersonIds | Master renewal and collections brakes | KEEP | brake | auto-start on, sending on, collections running | Brakes | Incident brakes; script-only by design |
RenewalArmState.workflowOwnedDispatch, .stableWorkflowIdentity | Which renewal plumbing runs | DELETE | — | on, including Yale | — | One code path, chosen; allowlists are scaffolding |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
aiAutoApproveThreshold | Dollar limit Clara approves without asking | MOVE to company | company + building override | $500 | Company · Turnovers & maintenance | A spending rule set by the company |
turnoverPolicy.autoDispatchCostCap | Dollar limit for turnover work | MERGE into the approval limit | company + building override | falls back to $500 today | Company · Turnovers & maintenance | Two ceilings that shadow each other |
handymanVendorIds | This building's handymen | KEEP | building | today's list | Building · Identity | People who actually go to that building |
Property.inHouseHandymanId | Legacy single handyman | DELETE | — | empty | — | Superseded by the handymen list |
preferredVendors (building and company) | Preferred vendors by trade | KEEP | company + building override | today's rosters | Company · Vendors | The one field that already inherits correctly |
vendorJobReferenceMode | Which number vendors quote | MOVE to company | company + building override | work order | Company · Vendors | A back-office convention, company-wide |
turnoverPolicy.unitAvailabilityDays, .inspectionDelayDays, .inspectionTimeOfDay, .inspectionDurationMinutes | Turnover timing | MOVE to company | company + building override | today's values; 10:00 and 60 minutes | Company · Turnovers & maintenance | Yale has none because it was never typed |
turnoverPolicy.defaultTrades, .defaultVendors, .finishingTrades | What a turnover starts with | MOVE to company | company + building override | today's lists; housekeeping last | Company · Turnovers & maintenance | A company make-ready recipe |
turnoverPolicy.chargeKnowledge, .conditionCoverageRooms | Move-out charge wording and room coverage | SURFACE, MOVE to company | company + building override | the shipped charge vocabulary | Company · Turnovers (advanced) | Charges reach residents; must be readable |
PropertyTurnoverSettings.customTaskTypes[] | Extra turnover task types | MOVE to company | company + building override | none | Company · Turnovers & maintenance | A vocabulary, defined once |
boundedPhotoDeferralEnabled | File the work order after one photo ask | DELETE | — | on (today: Willows only) | — | Endless photo asks with no work order is a bug |
autonomousMaintenanceEnabled, autonomousVendorDispatchEnabled, autonomousTurnoverEnabled, autonomousListingPublishEnabled | Do maintenance and turnover messages send | MERGE into area status | company + building override | each building's current combination | Company · Go-live status | Four booleans, one graduation question |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
pmActionReminders.enabled / .maxReminders / .intervalHours | Reminders about things waiting on you | MOVE to company | company + building override | on, 3 reminders, every 24 hours | Company · Notifications | A personal-feeling setting, typed once |
ownerReportRecipients | Who gets the weekly owner report | MOVE to company | company + building override | today's list, else the building inbox | Company · Notifications | Owners differ per building; defaults do not |
ownerReportWindowDays | How much the owner report covers | SURFACE, MOVE to company | company | 7 days | Company · Notifications | Hidden half of a visible report |
escalationOwnerCcEmail | Who is copied on a decision request | SURFACE, MOVE to company | company + building override | none | Company · Notifications | A copy list, not a destination decision |
pmNotificationMode | How much the manager hears | SURFACE, MOVE to company | company + building override | everything | Company · Notifications | Volume control a customer will want on day one |
leasingActivityChannel / .leasingActivityMention | Where leasing activity is posted | SURFACE, MOVE to company | company | no posting | Company · Notifications | One Slack destination per company |
autonomousLeasingDigestEnabled, autonomousLeasingReportingDigestEnabled | The weekly digests | MERGE into area status | company + building override | on at Camellia, off elsewhere | Company · Go-live status | Sending is the same question as elsewhere |
escalationCoworkerModeEnabled | Park the question instead of answering it | KEEP as one dark switch | company | off | Brakes | New behavior a customer would notice |
escalationEmailFormat, escalationRelayRephraseEnabled, topicScopedHoldUnmuteEnabled, escalationBakeApprovalRequired, sameThreadMemoryAckEnabled | Four escalation flags and one bake gate | DELETE four, KEEP the memory acknowledgement dark | — | the on-state each resolves | — | One layout, one relay, no human-click approval gate |
FollowupCadenceOverride.touches[] | How Clara chases each kind of person | KEEP, move the screen | company + building override | today's cadences | Company · Follow-ups | Editing a company rule from a building page misleads |
FollowupTouchBudgetOverride.budget | How often one person may be contacted | KEEP, move the screen | company | today's budgets | Company · Follow-ups | Same reason; it always applied company-wide |
AppSettings.vendorQuoteEmails / .vendorDispatchEmails | Vendor quote and dispatch mail | KEEP | brake | both on | Brakes | Platform brakes, already on a staff screen |
| Setting | Plain-English name | Verdict | Level | Default = today | Screen | Reason |
|---|---|---|---|---|---|---|
EnabledModules[...] + Property.capabilityStage.* | Area status: off, drafts, or live | MERGE — the survivor | company + building override | modules as shipped; stage live; sending as each arm reads today | Company · Go-live status | One question per area replaces seventeen booleans |
the four turnIntegrity* gates | Turn-integrity kill switches | DELETE | — | on (never written anywhere) | — | Correctness guards; the off-state is the bug |
env EMAIL_NTV_FIRM_ROUTING_ENABLED | Treat a notice email as a firm move-out | MOVE off env, KEEP dark | company | off | Brakes | Behavior must not depend on a deploy variable |
env CLARA_TRACE_CAPTURE_PROMPT, env TEMPORAL_NONDETERMINISM_METRICS_ALERT | Trace capture and a metrics drain | DELETE | — | capture on, drain never armed | — | One always wanted, one never used |
Property.isTest, Organization.isSandbox, .purpose, .status, .plan, .pms | What kind of account this is | KEEP | company / building | today's values | Company · Team & properties | Account identity, already correct |
Organization.name, .subscription.* | Company name and billing | KEEP | company | today's values | Company · Team & properties | Already right |
OrganizationSettings.logoUrl, .primaryBrandColor | Brand logo and colour | DELETE | — | unused | — | Declared, never read, nobody would miss them |
AppSettings.setupGuideState, .askClaraState | Whether the guide and helper are expanded | MOVE to person | person | expanded / small | Person · Account (implicit) | Per-person state stored once for the whole platform |
Person.displayName, name parts, email, phone, avatarKey | Your name, contact and photo | KEEP | person | today's values | Person · Account | Already right |
UserPreferences.displayZone | Whose clock times are shown in | KEEP | person | the building's clock | Person · Account | Already right |
User.dashboardLayout, .dashboardCardWindows, dashboard windows | Your dashboard arrangement | KEEP | person | shipped defaults | Person · Account (implicit) | Already right |
PersonPreferences.languagePreference, .smsOptOut, .emailOptOut, .timezone | Language, opt-outs, timezone | KEEP | person | English; detected; consent table decides | — | System-detected; the consent table is the real gate |
PersonRole.role / .scope, User.assignedPropertyIds | Who can do what, where | KEEP | person, scoped to company or building | today's grants | Company · Team & properties | Matches Gera's roles; grants, not a user class |
PermissionOverride | Staff flips of the permission matrix | KEEP | brake | matrix defaults | Brakes | Internal tool, not a customer setting |
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.
| Switch | What it does when on | Western Slope | Camellia | Yale 25 | Verdict |
|---|---|---|---|---|---|
capabilityStage.leasing | shadow/off suppresses every outbound — but only on the email path; no SMS call site reads it | shadow | absent | absent | CONVERT — the one visible company state; drafts = portfolio-arrived records only |
emailShadowMode | Clara never sends an email reply — to anyone, including someone who emailed her directly | true | false | false | REMOVE — folds into the state above |
tourRequestSmsEnabled | A caller who asks for a tour by phone gets a text receipt. Off = silence | false | absent | absent | REMOVE — a receipt is not a preference |
CONFIG / PMS_SYNC_PAUSE#<company> | Row present = the AppFolio sync and the availability heartbeat skip the whole company | paused since Sep 12, no reason recorded | absent | absent | REMOVE — "off" must still mean ingest; require a reason and an alarm |
publicListingsSync | The 15-minute listings job reconciles what is actually available | absent | set | set | REMOVE — set it; stale availability is a bug |
teamTourEmailAfterCallEnabled | A tour booked on a call also emails the team | true | absent | absent | REMOVE — turns it on everywhere |
tourInviteV2Enabled, newLeasePipelineEnabled | Newer invite copy; an approved application creates a next-steps item | absent / absent | absent / true | absent | REMOVE — finish the migration |
CONFIG / OPERATING_MODE_ARM | Global arm plus a property allowlist | not on the list | not on the list | the only entry | REMOVE the allowlist — a per-customer list in production |
propertyEmail / escalationOwnerEmail | Where team mail and escalations land | both PropFlow addresses | the client's own inbox | PropFlow | CONVERT — a visible company setting, and wrong at Western Slope today |
AppSettings.testMode | Nothing. Zero read sites anywhere | true on the global row — reads as "production is in test mode" | REMOVE | ||
Company copies of emailsEnabled / smsEnabled | Nothing — the resolver has no company fallback; the real brakes are the platform row, both on | both false | — | — | REMOVE the rows — they read as a gate and are not one |
Organization.claraLive | Nothing. Written and shown in the back office, no code reads it | absent | absent | absent | CONVERT — this is where the one dark hold belongs |
| Follow-up cadence arm | There is none. The per-building arm was deleted Jul 27; the 5-touch cadence ships on everywhere | a portfolio-arrived lead that engages enrols and gets texted today | KEEP 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 outright | production-wide | REMOVE — 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.
Only the ones that change what a customer experiences. Each is a yes or no, with a recommendation.
| # | Decision | Recommend |
|---|---|---|
| 1 | Fold 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 |
| 2 | Always 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 |
| 3 | Replace 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 |
| 4 | Link co-applicants into one household everywhere? Today it happens only at the Willows, which is why cosigner applications sat unlinked at Camellia. | Yes |
| 5 | File 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 |
| 6 | Let the name someone says on a call replace the stored name everywhere? Today only at the Willows. | Yes |
| 7 | Make the tour notice rule a company rule with per-building exceptions, folding same-day and per-weekday notice into that one rule? | Yes |
| 8 | One "Connect Microsoft 365 or Google" button at the company, instead of separate email and calendar buttons on every building? | Yes |
| 9 | Move 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 |
| 10 | Put Yale on the same renewal plumbing as Camellia and the Willows? It is the one property left off that list today. | Yes |
| 11 | Make "which properties we ignore" a company choice instead of a personal one, so two admins at one company see the same portfolio? | Yes |
| 12 | Move 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 |
| 13 | Stop routing an unrecognised incoming phone number to the caller's own building, and refuse instead? | Yes |
| 14 | Delete 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 |
| 15 | Keep 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.
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.
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.
absentMeans set to the value production resolves today.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.
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.
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.
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.
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.
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.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.