Settings placement: criteria, inventory and the five decisions
The settings pages are "pretty vanilla" (Gera) and the product is about to grow policies, rules, prompts, flags and deciders. The open question with Fede is where each of those lives: one of the four settings levels, the property detail page, or inside a module. Two read-only sweeps of the codebase at d0bed136d3 (after the four-level split, #7324 and #7326) produced this page: an inventory of every knob that exists today and where it is, and a set of placement criteria applied to about 95 current and proposed knobs. The short version is that the codebase already contains most of the answer, and that a good part of what exists is stored at the wrong level.
What each page would look like
Wireframes of the recommendation, each with a dropdown for the choice underneath it. Field names are the real knobs from the inventory below; the values are illustrative sample data, not any account’s real configuration.
First, the gap: three places already hold building configuration
Before choosing where anything should go, this is where it is now. All three of these are live today.
Settings › Property/settings, with a property picked
- Integrations
- Calls
- Leasing & tours
- Emails & reminders
- Maintenance Automation — incl. the auto-approve ceiling
The Property page/properties/<id> — what you get when you click a building
- Property details (phone, website)
- Pricing & fees
- Renewal policy — incl. “Autonomously send renewal offers”
- Turnover policy
- Knowledge & concessions
The module pagesLeasing, Maintenance, Renewals, Collections
- Follow-up cadences
- Preferred vendors by trade
- Chase cadences
- Qualification rules
Autonomy
Renewals
This building
Clara here
1 · Account — you
Profile
Notifications
Security
Profile
How I want to be told
Set by my company
2 · Organization — your company
Company
What Clara may do
Contact policy
Team
Integrations
Defaults every property inherits
Contact policy
Contact policy
3 · Property — one building
Autonomy · Leasing
Clara at this property
Calls
Leasing
Notifications
Integrations
4 · Admin — PropFlow staff
Autonomy arms
Platform
Access
The end state: every level, every card, every field
2026-09-08. Gera: “We want the end state of this product — everything exactly as it should look, even if the logic isn’t connected. Unbuilt features greyed out with an ⓘ saying coming soon.” Six domain passes (identity & channels · leasing & tours · maintenance, vendors & turnover · renewals & collections · people, team & notifications · autonomy, compliance & admin), each grounded in the codebase, merged by level. Every field is marked exists or coming soon. Hover a row for the one-line reason and the code it points at.
Where is it decided? The shared-resource binding model
Your calendar and phone-number questions are the same question. A shared resource is provisioned at one level, owned at another, bound at a third, and sometimes personal. Today the four resources landed at four different scopes by accident; this is the deliberate model.
| Resource | Provisioned by | Owned at | Bound at | Personal? | Shown where | Today |
|---|---|---|---|---|---|---|
| Phone number (voice + text, same line) | PropFlow staff (Twilio) | Company — the number pool | Building — one or more per building | no | Org › Phone numbers (pool) · Property › Lines & inboxes (picker) | Property.twilioNumbers is absent everywhere; an env map; each new number is a source PR |
| Company main line | PropFlow staff | Company | Company routing table | no | Org › Phone numbers › Main line | Does not exist; called-number → building is the only resolver |
| Email inbox Clara reads | Customer (OAuth) | Company tenant consent (coming soon) | Building — one mailbox each | no (shared team box) | Property › Lines & inboxes · Org › Email | Property.emailIntegration, authorised per building |
| Sender identity (from-name, address, signature) | Customer | Company default (brand + domain) | Building override | no | Org › Clara's identity · Property › Send email as | Per-property script-only; global clara@propflowai.co fallback |
| Delivery path (direct vs through the PMS) | — | Company (a property of the PMS account) | Building | no | Org › PMS · Property (badged) | Per-property only today |
| PMS account (credentials, database) | Customer enters; staff rotate | Company — may hold several databases | Building — which database + Clara's seat | no | Org › PMS accounts · Property (read-only) | Credentials keyed PER USER today (PMSCRED#<userId>) |
| Tour calendar | Customer (OAuth) | The person, or a shared mailbox | Building picks from the offered list | YES | Account › My calendar · Property › Tours › Book tours on | Only a per-building Outlook connection is read (Property.leasingCalendar); the person-level connection exists but is never read for tours |
| Voice agent | PropFlow staff | PropFlow | Building, via the called number | no | Admin › Voice agents · Property (read-only) | Agent ids are code constants |
| Timezone · website · listings feed | — | a fact about the building | — | no | Building page | Timezone has NO writer at all — quiet-hours gating defaults to Chicago |
| Language Clara answers in | — | Company capability | Building override | person preference on the spine | Org › Clara's identity · Property row | No company or building knob; a per-person preference only |
Two corrections from the identity pass, both verified: the tour calendar is read from a per-building Outlook connection today (the person-level one is written but never read), and PMS credentials are keyed per user, not per company — the same misfile as company name. One bug the pass reported was already gone by the time this published: onboarding used to show every new customer the literal +1 (844) 510-1007 — one customer’s line — as “their” number; #7304 removed it the same morning and a test now forbids any phone number on that step. The other bug from this pass, the global text/email switches writable by any role, is closed by #7365 (staff-only on write).
The Autonomy ladder, row by row
One shared enum, the same four words on every row. The rung is a projection over fields that already exist — no new column per row. Customers stop at Act with approval; Autonomous is unlocked per building by PropFlow after a soak.
| Module | Off | Suggest | Act with approval (customer ceiling) | Autonomous (staff only) |
|---|---|---|---|---|
| Leasing | capabilityStage.leasing='off' | 'shadow' + drafts (soon) | newLeasePipelineEnabled, lease send stops at draft | autonomousLeaseSendingEnabled + autonomousListingPublishEnabled |
| Tours | lane off (soon) | propose only, PM confirms (PROPOSED) | escalatedTourBookingEnabled=false | escalatedTourBookingEnabled + tourRequestSmsEnabled |
| Maintenance | capabilityStage.maintenance='off' | 'shadow' | dispatch held (autonomousVendorDispatchEnabled=false) | autonomousVendorDispatchEnabled + autonomousTurnoverEnabled, inside the spend limit |
| Renewals | capabilityStage.renewals='off' | 'shadow' + drafts | autonomousRenewalEnabled=false ("every renewal pauses for review") | autonomousRenewalEnabled + autonomousHoldoverEnabled |
| Collections | not enrolled | draft dun shown | FIXED here by ADR-0125 | not offered — ⓘ "always needs a person's approval" |
| Conversations | greyed — Clara always answers | greyed | handoffMode='coworker' (park and hold) | handoffMode='autonomous' (answer, say so once, move on) |
The ceiling model. Platform ceiling (PropFlow staff, per company × row) → company cap (org admin, also carries the default for new buildings) → building value (PM). Every row badges its source. Rungs above the cap render greyed with ⓘ “Capped at Act with approval by Camellia Admin”. Under the ladder sits the one spend line: Clara may approve up to $500 per job without asking, with turnovers as a sub-line. Shadow / live / test shows as a chip from Admin-owned state, never a control.
Ceilings versus defaults
The difference between a value a parent locks and one it merely pre-fills is the difference between a compliance posture and a suggestion.
| Setting | Platform ceiling? | Company may set | Building may set | Why |
|---|---|---|---|---|
| Autonomy rung, per module | Yes — max Act with approval until staff raise it per building | a cap ≤ ceiling, and a default | a value ≤ org cap | Art. IV.6: activation is Fede's, every time |
| Collections rung | Yes — fixed at Act with approval | lower only | lower only | ADR-0125 |
| Spend Clara may approve | Yes — a maximum dollar figure (founder number) | a default ≤ ceiling | a value ≤ org default | Money leaves the customer's account |
| Channels (SMS, email, voice) | Yes — plan gates them on | on / off | off only | Narrow-only |
| Languages | Yes — platform list | subset | subset | Clara can't speak what she isn't tested in |
| Modules | Yes — plan | none (read-only) | none | What the plan buys |
| Contact ceiling | Yes — a platform maximum nobody may exceed | a lower number | none | Compliance posture; today org-only with no platform floor |
| Quiet hours | Yes — 8 PM–9 AM is the widest anyone gets | none | narrower only | TCPA floor |
| Opt-out honouring · fair-housing rails | Fixed | none | none | Law / never a switch |
| Emergency definitions | Yes — platform baseline (gas, fire, flood, no heat) | add only | add only | Life safety cannot be unchecked |
| Record retention | Yes — floors per record type | longer only | none | Keep more, never less |
| Tone | Yes — the evaluated set | pick | pick | Prompts are code |
| Brand, company name, notification recipients | No ceiling | default | override | Pure defaults |
| Test / shadow / live, kill switches, arms, allowlists | Staff only — not on the chain | none | none | Observability and cutover state |
The pages
1 · Account — you
Only what constrains me personally. Company policies appear here only when I have to act on them.
30 fields across 6 cards · 5 exist today · 25 coming soon
Profile 5 fields · 2 coming soon
How I want to be told 12 fields · 11 coming soon
My calendar 4 fields · 4 coming soon
My showing calendar & availability 4 fields · 4 coming soon
My connections 2 fields · 2 coming soon
Security 3 fields · 2 coming soon
2 · Organization — your company
The defaults level: every inherited row is authored here and merely overridden per building. Nothing here is writable today.
177 fields across 21 cards · 47 exist today · 130 coming soon
Clara's identity 10 fields · 7 coming soon
Phone numbers 6 fields · 5 coming soon
Email 4 fields · 4 coming soon
Property management system 9 fields · 5 coming soon
Team 9 fields · 6 coming soon
Who can manage people 7 fields · 4 coming soon
manage-teammate.ts; they are not a second writer beside it.Notification defaults 10 fields · 9 coming soon
Escalation policy 7 fields · 5 coming soon
Security policy 3 fields · 2 coming soon
Follow-ups 10 fields · 3 coming soon
Leasing standards 16 fields · 14 coming soon
Maintenance policy 11 fields · 11 coming soon
Dispatch & spend 4 fields · 3 coming soon
Vendor standards 8 fields · 6 coming soon
Turnover standards 9 fields · 7 coming soon
Preventive maintenance 3 fields · 3 coming soon
Renewals — company standard 19 fields · 14 coming soon
Collections — company standard 11 fields · 9 coming soon
Compliance 10 fields · 6 coming soon
Plan & billing 5 fields · 3 coming soon
Clara's capabilities 7 fields · 3 coming soon
Company autonomy caps 6 fields · 5 coming soon
3 · Property — one building
Instructions to the software for this building. Every row badges where its value came from.
76 fields across 10 cards · 24 exist today · 52 coming soon
Autonomy 9 fields · 7 coming soon
Lines & inboxes 16 fields · 9 coming soon
Tours 1 fields · 1 coming soon
Leasing & tours 18 fields · 14 coming soon
Maintenance 3 fields · 2 coming soon
Turnover 3 fields · 1 coming soon
Renewals 4 fields · 2 coming soon
Collections 8 fields · 5 coming soon
Notifications 12 fields · 9 coming soon
Compliance 2 fields · 2 coming soon
4 · The building page — facts
What is true about this building with or without PropFlow: who, where, hours, money facts, access.
39 fields across 7 cards · 13 exist today · 26 coming soon
This building 4 fields · 2 coming soon
People 16 fields · 13 coming soon
Owner access 9 fields · 9 coming soon
org_admin (the customer’s own top-level admin) and not the PMS credential holder, which is what Property.ownerId actually is (types.ts:3371). Three different things, one word; this card is the third-party landlord.Leasing facts 5 fields · 2 coming soon
Lease policy (facts) 4 fields · 2 coming soon
Access & entry 5 fields · 5 coming soon
Go-to vendors 3 fields · 0 coming soon
Systems & appliances 2 fields · 2 coming soon
5 · Admin — PropFlow staff
Ceilings, arms under soak, provisioning, and the watch list.
49 fields across 12 cards · 31 exist today · 18 coming soon
Autonomy ceilings 2 fields · 2 coming soon
Watch list 12 fields · 2 coming soon
Number provisioning 3 fields · 3 coming soon
Voice agents 2 fields · 1 coming soon
PMS account registry 1 fields · 1 coming soon
Leasing rollout switches 4 fields · 0 coming soon
Maintenance arms 5 fields · 1 coming soon
Renewal & collections arms 8 fields · 1 coming soon
Coming-soon governance 4 fields · 4 coming soon
Kill switches 3 fields · 1 coming soon
Arms under soak 2 fields · 0 coming soon
Access 3 fields · 2 coming soon
6 · The vendor record
Not a settings page: facts about a vendor the company deals with.
10 fields across 1 cards · 3 exist today · 7 coming soon
Vendor record (the org directory) 10 fields · 7 coming soon
7 · Module pages — read-only mirrors
Each shows what it is running with and links to the writer. None of them edit a setting.
16 fields across 4 cards · 9 exist today · 7 coming soon
Leasing › Prospects — "How this building leases" strip 7 fields · 1 coming soon
Maintenance › Work orders — strip 6 fields · 4 coming soon
Leasing › Renewals — "Renewal Playbook" (mirror) 1 fields · 1 coming soon
Collections — Policy drawer (mirror) 2 fields · 1 coming soon
The event catalogue — who is told what
Every event that can notify someone, across all modules. The default recipient is a role; the company sets the default, a building may override. This is the table the Notifications routing rows are built from.
| Module | Event | Default recipient | Set at | Today |
|---|---|---|---|---|
| Escalation | Decision request opened | {Escalation owner} + CC | Org default · Property override | exists |
| Escalation | Daily reminder on an unanswered ask | same, re-resolved live | inherits | exists |
| Escalation | Still unanswered after N reminders → moved up | {Regional manager} → {Org admin} | Org | coming soon |
| Escalation | Life-safety page (gas, fire, flood) | {Escalation owner} → on-call → inbox, all hours | Org · never off | exists |
| Escalation | Forward to office / plain page / unknown-caller note | {Property manager} | Org → Property | exists |
| Escalation | Missed transfer call | {Escalation owner} → inbox | inherits | exists |
| Escalation | Promise needs a human | {Escalation owner} | inherits | exists |
| Escalation | Fair-housing hold on a draft | PropFlow watch list → {Org admin} | Admin → Org | exists |
| Leasing | New tour / application / lease (activity) | {Leasing agent} digest | Org → Property | exists |
| Leasing | Application awaiting review (+ reminders) | {Property manager}; cadence per person | routing Org → Property · cadence Account | exists |
| Leasing | New-lease package needs review | {Property manager} | inherits | exists |
| Leasing | Lease signed / renewal executed | {Accounting} | Org default · Property override | exists |
| Leasing | Renewal needs manual review | {Property manager} email + text | Org → Property | exists |
| Leasing | Leases ending with no answer (digest) | {Property manager} | Org toggle | exists |
| Leasing | Virtual tour: "your team calls at…" | {Leasing agent} | inherits | exists |
| Leasing | Stale leads weekly list | {Leasing agent} | Org toggle | exists |
| Leasing | Rent-roll sync digest | {Property manager} | Org toggle | exists |
| Leasing | Weekly / monthly owner report | {Owner contacts} → inbox | Org window · Property recipients | exists |
| Leasing | Tour booked / moved / cancelled | {Leasing agent} | inherits | exists |
| Maintenance | In-house work order dispatched (SMS) | {In-house crew} | Property | exists |
| Maintenance | Vendor call outcome | {Property manager} | inherits | exists |
| Maintenance | PO required before dial | {Property manager} (+ {Accounting}) | inherits | exists |
| Maintenance | Quote above the spend ceiling | {Property manager} (review queue) | Org ceiling → Property | exists |
| Maintenance | Work-order milestones digest | {Property manager} | Org → Property | coming soon |
| Turnover | Move-out drafted in AppFolio | {Accounting} → {Property manager} → inbox | inherits | exists |
| Turnover | Move-out date mismatch | {Property manager} | inherits | exists |
| Turnover | Inspection scheduled / condition report ready | {Property manager}, {On-site} | Org → Property | coming soon |
| Turnover | Turnover dispatch above cost cap | {Property manager} | inherits ceiling | coming soon |
| Collections | Past-due / habitability hand-off | {Property manager} | inherits | exists |
| Collections | Eviction-flagged or uncured demand | {Escalation owner} — not the landlord | Org → Property | exists |
| Collections | Counsel / court / programme mail arrived | {Property manager} | Property | exists |
| Collections | No escalation owner named at a real building | {Org admin} weekly | Org | coming soon |
| Account | Invite accepted / role changed / removed | {Org admin} | Org | coming soon |
| Account | New sign-in / MFA enrolled or reset | the person | — | coming soon |
| Account | Contact ceiling hit | PropFlow watch list → {Org admin} | Admin → Org | exists |
Coming-soon governance: how the greyed rows stop lying
Three ad-hoc “coming soon” conventions exist in the product today — a select label, a pill, an accordion body — and no registry. The design and the product drift the moment they are two documents. So: one typed settings registry in code, from which the settings pages, the search index and this wireframe are all generated.
- Declare. One row per field: id, level, card, label, control, inheritance chain, mode (ceiling / default / fixed), status (live / coming soon / staff only), which field stores it, which route writes it, since when, and the decision page that decided it. A row with no decision link fails lint.
- Show. A coming-soon row renders through one component — greyed control, real label, ⓘ with the one-line why. A page cannot render a field the registry does not list.
- Gate. Three drift tests: every rendered field has a row; a coming-soon row has no writer (the same fence that protects hand-off mode today); a staff-only row renders only under Admin.
- Graduate. One PR flips the status, adds the writer and the reader; the tests refuse one without the other. Nobody un-greys at runtime — no flag, no env, no toggle. The wireframe re-renders from main, so the design cannot say one thing while the product says another.
The decision register
Status 2026-09-08 — CLOSED. All 22 decided. Ten by button, twelve in Gera’s own words (the decisions page). Two reversed the recommendation on this page: seats (row 8) and rental criteria (row 9), the second toward more caution than was proposed. Each row below keeps its original recommendation underneath the answer, so a reader can see what was rejected as well as what was chosen.
Every genuinely open choice the six passes surfaced, deduplicated, each with a recommendation. Answer by picking.
| # | Decision | Recommendation | Raised in |
|---|---|---|---|
| 1 | Number topology | Decided 2026-09-08 (Gera): any shape — one number may serve the company, a group of buildings, or one building, mixed within one customer; the settings page shows the binding for wherever you are in the nav (his own words are on the decisions page). Was: One line per building, voice and text together, auto-assigned from the company pool at onboarding. A company main line is opt-in, never the default. | Identity |
| 2 | Calendar of record for tours | Decided 2026-09-08 (Gera): same as the number: a calendar may be company-only, shared by some buildings, or one per building, in any mix; a person’s offered calendar is one more source the building can bind. The unread person-level connection still stops being written until it is read. Was: Offered personal calendars by default (a person offers theirs; the building picks; first-free, round-robin optional); company shared calendars as the scale path. Stop writing the unread person-level connection until it is read. | Identity · Leasing |
| 3 | Sending domain | Decided 2026-09-08 (Gera): the from-address is a binding like the number (company, group or building). On the domain itself his standing rule applies — “we work around their system” — so the customer’s own domain when they have one, the PropFlow fallback when they do not. Was: The customer's own domain with a verified-status chip; clara@<customer>.propflowai.co as the zero-setup fallback. | Identity |
| 4 | Platform spend ceiling | Decided 2026-09-08 (Gera): in his words: greyed out as a placeholder with the rules shown; the value is governed by the property when set there, otherwise the company’s — “we would want both, where property takes precedence”. Read with row 6 (the cap wins), this is exactly the ceilings-vs-defaults split above: the company holds a default the building may override and a cap the building cannot exceed. The $5,000 / $1,500 figures were not decided; they stay as the placeholder’s sample values. Was: $5,000 per job is the most any customer may set. Autonomous dispatch above $1,500 needs the company cap raised by an org admin, not a PM. Under the ceiling, "Act with approval" auto-accepts quotes — otherwise the ceiling means nothing. | Autonomy · Maintenance |
| 5 | What "Off" means | Decided 2026-09-08 (Gera): accepted — “Hand it to a named person, never silence.” Was: Park to a named human, never silence. Today capabilityStage.off equals shadow suppression, which is silence — that changes before Off is offered. For collections, onboarding forces a choice and an Off building shows a persistent banner. | Autonomy · Collections |
| 6 | Company cap vs a building already above it | Decided 2026-09-08 (Gera): accepted — “The company cap wins; the building is clamped and labelled.” Was: The cap wins. The building clamps and its badge reads "Clamped by company cap on <date>". No grandfathering. | Autonomy |
| 7 | What the plan gates | Decided 2026-09-08 (Gera): in his words: “all of it should be configurable by the customer … the admins for those accounts could configure things like autonomy, compliance and contact limits.” My reading, not his click: nothing is locked by the plan; what varies is the role — a customer admin configures autonomy, compliance and contact limits, a regular user does not. That is consistent with the Admin ceilings above (the customer configures inside a platform limit). Was: Modules, channels and languages only. Autonomy, compliance and the contact ceiling are never plan-gated — safety is not an upsell. | Plan |
| 8 | Seats | Decided 2026-09-08 (Gera): overruled — “Let’s do per unit and then have 12 users per account cap.” Nothing counts users today, so the cap needs a row and a behaviour when it is reached. Folded into the architecture’s phase P7 (staff and login). Was: None. Unlimited users, pay per unit; unitCount already exists and there is no seat concept to defend. | Plan · Team |
| 9 | May Clara state published rental criteria? | Decided 2026-09-08 (Gera): overruled, toward caution — “No, I think there’s fair housing rules… the safe answer, we don’t want to hand off to the person. We want to maybe do ‘that’s something we would review when your application comes in’… we want to play it safe.” So Clara neither states the criteria nor transfers: she says the application is reviewed when it arrives. The criteria card comes off the design. Was: Yes — one sentence, verbatim from the company criteria sheet; never "you would / wouldn't qualify". The transfer stays for "would I qualify?" | Leasing |
| 10 | Renewal negotiation floor | Decided 2026-09-08 (Gera): accepted — “Yes, a company-level floor, default zero.” Was: Yes, company-level, default 0% (today's behaviour), never above the minimum increase. Everything outside the floor and the offered terms still escalates. | Renewals |
| 11 | Payment plans | Decided 2026-09-08 (Gera): accepted — “Never on her own; propose for PM approval once lawyers sign off.” Was: Never, as the default even after the field exists. Ship "propose only, PM approves" greyed until counsel signs off per state. | Collections |
| 12 | Autonomous for collections | Decided 2026-09-08 (Gera): accepted — “No; PropFlow unlocks it per state after legal review.” Was: Never for a customer. An Admin unlock per state after counsel sign-off; the customer sees the rung greyed with "Requires PropFlow legal review for <state>". | Collections |
| 13 | Who owns staff assignment | Decided 2026-09-08 (Gera): the writer follows the nav — a building selected edits that building’s people, no building selected edits the company’s — and both write the same graph, so there is one fact and two views of it. Was: Org › Team owns who works where. The building's People card only chooses which assigned person is primary or escalation owner here, and adds non-staff rows (crew, owner, counsel). Two writers for one fact would drift. | People |
| 14 | Is the maintenance tech a login or a crew member? | Decided 2026-09-08 (Gera): accepted — “Either: crew gets texts with no login; app users get a staff role.” Was: Crew = an in-house vendor membership (no login, gets SMS). A tech who needs the app = the staff role. The People card accepts either; the resolver reads the phone off the person. | People · Maintenance |
| 15 | The escalation ladder | Decided 2026-09-08 (Gera): accepted — “Company sets the cadence within bounds; PropFlow is the last rung only.” The control (cadence bounds, the ordered role list) is this page’s; the path change — PropFlow’s Slack from first rung to last, and only when no customer role resolves — is a code change in agents/clara and is taken by the architecture effort as a P7 row. Was: A company knob with bounds (ask again every 4–72h; stop after 1–5), then move up to the regional manager, then the org admin. PropFlow's Slack becomes the last rung, only when no customer role resolves. | Escalation |
| 16 | Expired vendor insurance | Decided 2026-09-08 (Gera): accepted — “Warn by default; companies may choose to block.” Was: "Warn" as the platform default; "block except emergencies" selectable at company level. Blocking by default strands jobs at onboarding when zero certificates are on file. | Maintenance |
| 17 | Vendors not in AppFolio | Decided 2026-09-08 (Gera): accepted — “Yes, PropFlow-only vendors that a later sync links.” This also closed the architecture side’s open question on vendor identity: a vendor card with no engagement rows is the directory. Was: Allow a PropFlow-only vendor row the sync later links. Otherwise a Yardi or Buildium customer has no vendor directory at all. | Maintenance |
| 18 | Quiet-hours narrowing per building | Decided 2026-09-08 (Gera): accepted — “Yes, narrower only.” ⚠️ The resolver must refuse a widening write rather than silently ignore it: a control that looks writable and quietly drops the value is worse than one that says no. Was: Yes, narrower only — the one compliance override a building gets. (One quiet hours, on the building's clock, stays settled.) | Compliance |
| 19 | Retention floors | Decided 2026-09-08 (Gera): accepted — “7 years conversations and audit, 4 years consent and opt-out.” Platform floors, not per-customer settings. Was: Conversations 7 years, consent and opt-out at least 4 years as coded, audit 7 years — and let the DPA cite the registry, not the reverse. | Compliance |
| 20 | Voice as its own capability | Decided 2026-09-08 (Gera): accepted — “Yes, calls and texts are separate switches.” Was: Yes, separate from texting. A customer who allows texts but not calls is a real profile. | Capabilities |
| 21 | Tone | Decided 2026-09-08 (Gera): accepted — “Three tested variants, greyed until each is tested.” Was: A three-variant evaluated select, shipped greyed until the variants are graded. Never a free-text persona field. | Identity |
| 22 | A level between company and building | Decided 2026-09-08 (Gera): reversed. A group exists in the portfolio-architecture record as a binding target and a precedence rung (building → group → company → registry default). It is not a fourth settings page: it is where a shared number or calendar hangs, and what a value inherits from. Bulk-apply stays as the gesture for policy. Was: No. Region is a permission scope (assigned properties); a PMS account is an integrations binding. Build a bulk-apply gesture on Property Settings instead. | Model |
What onboarding must collect — and where it lands
Onboarding is the first write, not a separate store. Nothing should be enterable only at onboarding, and nothing required before Clara runs should be settable only by a script.
- Collected today: company name (lands on the user row), company type and size, PMS credentials (per user), imported buildings and exclusions, one inbox and one calendar bound to the first imported building, and a “your number” that is actually one customer’s line.
- Required before Clara runs but set only by scripts today: a phone number per building, the PMS account binding and Clara’s seat in it, the delivery path, timezone, text-consent model, sender identity, the escalation owner, demand party, eviction counsel, assistance programmes, the operational-sender lists.
- End state: the channels step becomes a per-building table — number auto-assigned from the pool, inbox connect, calendar pick — one consent for forty buildings, then a picker each. Every collected value then lives in the cards above with a Set here badge.
Verification note. Each domain pass cited file and line for its claims about current code; the integration re-read a sample from every pass (the settings resolver’s “no global fallback” header, the offer-only lead time, the absent screening criteria, the absent vendor insurance fields, the second vendor chain in turnover policy, the delinquency threshold saved to two fields, the collections review-gate ruling, the Colorado-only jurisdiction registry, the stuck-matter Slack surface, the silent no-owner paths, the nag constants, the user-gated settings route, the shadow-equals-off comment, the per-building calendar read, the per-user PMS credentials, the onboarding number literal, the timezone with no writer) and found every one verbatim. Where an agent inferred rather than read, the row says so.
The big picture: how not to become AppFolio
Written 2026-09-08, at the point where the surface is about to be filled in. Onboarding new companies is the driver, and the storage shape is the part that stops being changeable once real customers have values in it.
What the incumbents get wrong, and why it is one decision away
AppFolio, Yardi, Buildium, RealPage and Entrata all arrived at the same place by the same road: every module grew its own setup screen, owned by its own team, and nobody merged them. The result is a settings tree three or four levels deep where the same idea — who gets notified, office hours, late fees — is configured in two or three places under different names. That is an industry pattern rather than a claim about any one screen, and it is why each of these vendors sells implementation consulting and why “where do I turn that off?” is a top support category.
Three specific failures. PropFlow is one decision away from each:
- No inheritance. Property settings are copies. Set up forty buildings and you have forty independent copies; change company policy and you edit forty times. Nobody can answer “which buildings are different from the default, and why?”
- Config lives where the feature was built, not where a customer would look for it. This is exactly the renewal-versus-maintenance autonomy split above.
- Facts and decisions are mixed together. A fee schedule sits beside “auto-post late fees”, so changing one quietly changes what the other means.
What to steal, and whether it fits us
| Pattern | Where it is proven | Fits PropFlow? |
|---|---|---|
| Scope in the URL — the team / project or org / repo split is a path segment | Vercel, GitHub, Linear | Yes, and it is the one we violate. Organization and property are exactly their team and project. |
| Inheritance with a visible “inherited from” state | GitHub org → repo policy, Vercel team defaults | Yes. Already the proposal. The badge is not decoration — it is how a manager answers “is this building special?” |
| A parent can lock a value, not merely default it | GitHub enterprise policy | Yes. This is how “Autonomous is staff-only” gets enforced rather than hoped for: the platform sets a ceiling, nobody below can exceed it. |
| Settings answer the global search box | Linear, Stripe, Shopify | Yes, with one precondition — see below. |
| One canonical writer, read-only mirrors elsewhere | General to all of them | Yes, and overdue. |
| Progressive disclosure — a few switches visible, detail underneath | Stripe, Intercom | Yes. Fifty-one knobs on one screen is a tree; six cards of three to five visible rows is a page. |
| Test / live mode switch | Stripe | No. A manager has no test mode for a real building. |
| Settings that go below the lowest real object | Notion per-page | No. Nothing below the property. No per-unit knobs, ever. |
The one rule
“If it decides what Clara does it goes in Settings; if it describes the building it goes on the building page” is right in spirit but loses every argument at the edges, because Clara reads almost everything. Pricing decides what she quotes. Pool hours decide what she says. Sharpen it into a test you can run in your head:
Would this still need to be true if you cancelled PropFlow tomorrow?
- Yes → it is a fact about the building. It lives on the building page.
- No → it is an instruction to the software. It lives in Settings.
- Module pages hold neither. They show what they are running with, read-only, and link to the writer.
A fact was in the filing cabinet before we arrived and survives us. A setting only exists because Clara does. Clara reading a fact does not make it a setting.
That test picks the surface. A second one picks the level, and it is the one that answers “whose notification is this?”:
If you change it, who feels the difference?
- Only me → Account. My own inbox, my own reminders.
- Everyone at this building → Property.
- Everyone at the company → Organization.
Run them in order: first which surface, then which level. Neither question answers the other.
Worked: the word “notifications” means two different things
This is the case that trips everyone, because both things are called notifications and both are real.
| Who gets told | How I want to be told | |
|---|---|---|
| Examples | Escalation owner and CC, maintenance tech phone, renewal contact, who receives the weekly owner report | One daily digest or every event as it happens; how many reminders about my pending approvals; how often |
| It is really about | Who staffs this building | Me |
| Change it and… | a different person gets paged | my inbox looks different, nobody else's does |
| Level | Property (some inherited from the company) | Account |
| Today | Correct — Property Settings › Emails & reminders › Who else gets notified | Mis-scoped. There is no Account-level version. “Reminders for you” sits on Property Settings and is stored per building (pmActionReminders on the property's leasing settings), so a manager with twelve buildings tunes their own reminders twelve times and no page anywhere says how they want to be reminded. |
The Property Settings page already knows these are two different things — its own subtitle says “Reminders for you” are emails about your to-dos and “Who else gets notified” are emails to other people. It splits them into two blocks on one card. The correction is small: the first block moves up to Account, the second stays.
A trap on the same test, worth naming. A company policy earns a place on your Account page only if it constrains you personally — two-factor, because you have to enrol; a cap on your own reminder cadence, because it bounds a setting of yours. It does not earn a place merely by being a rule you are subject to. Resident quiet hours are the clean counter-example: they are a real company policy, but changing them alters what residents receive, not what you do. Run the level test on them and the answer is “everyone at this company”, so they live at Organization with a per-building override for state law — and never on a personal page, where their presence would imply they are yours to bend. This corrects an earlier draft of the Account wireframe above, which listed them under “set by my company”.
Honest counter-argument: you might genuinely want more nagging on one difficult building. That is fine and needs no special case — Account holds the default, a property may override it, exactly like every other inherited value. What is wrong today is that there is no personal default to inherit from.
Applied to real knobs
| Knob | Test | Home |
|---|---|---|
| Rent, fees, concessions, pool hours, pet policy | Would exist without us | Building page. Clara quotes them; she does not own them. |
| “Autonomously send renewal offers” | Meaningless without us | Moves off the building page into the Settings Autonomy card, on the same ladder as maintenance. This is the accident the rule exists to fix. |
| Leasing follow-up cadence (text again after 2 days, then 5) | The hard one. A whiteboard could hold it, so it feels like a fact — but the knob is not what our policy is, it is when Clara acts | Settings. The Leasing page shows “Running with: 2d / 5d” and nothing editable. |
| Preferred vendors by trade | Two knobs fused into one. “Ace Plumbing is our plumber here” would be true without us; “dispatch to them without asking” would not | Split them. Roster to the building page and org directory; the dispatch decision to Settings. Today they are fused, which is why nobody can say whether editing the roster changes behaviour. |
| Quiet hours (no texts after 9pm) | Feels like company policy, but only ever governs Clara's outbound | Settings, organization level, inherited and overridable. Most managers will meet the inheritance badge here first, so make it a good one. |
Settings search: yes, at card level, and not until scope leaves the cookie
Removing the settings rows from ⌘K on 2026-09-07 was correct, and the code comment states the law to build to: a search result may only exist if its link fully determines what renders. A row that lands on a page which then consults a cookie to decide what to show is a lie, and a support ticket. Today only /settings/account is a real address; /settings resolves to Organization or Property from propflow-property-filter — and that is the global picker shared with Dashboard, Leasing and Maintenance, so this is not a settings-local problem.
- Put scope in the URL.
/settings/organization/<card>and/settings/property/<id>/<card>. Bare/settingskeeps today's follow-the-picker behaviour so the sidebar link is unchanged. Landing on a property settings URL sets the picker rather than reading it — the same way opening a building page already tells you which building you are in. The building page is deep-linkable; settings is the odd one out. - One settings registry, in code. Every knob declared once: id, label, level, card, keywords, default. The cards render from it, the inheritance resolver reads it, and the search index is generated from it. The hand-written index (24 pages + 18 sections) is fine for pages and will rot the day it has to track ninety-seven knobs. The registry is also what makes the inheritance badge and the audit trail nearly free.
- Index cards, not fields. A result reads Calls › After-hours and voicemail, with a chip naming the building or Organization. When no building is picked and the hit is property-level, the row says Property setting — choose a property and the second step is the building list. Never guess a building.
- Field-level search later, and only with scroll-and-highlight. A field hit that dumps you at the top of a twenty-row card recreates the same broken promise one level down.
Cost: the route change and picker sync is a day or two; the registry is the real work, and you need it for inheritance regardless; generating search rows from it is an afternoon. Field-level search is a week not to spend yet.
Decide now, defer the rest
Before the surface fills in — these get expensive or impossible once companies are onboarded:
- Apply the rule. Move renewal autonomy into the Autonomy card, demote module-page config to read-only mirrors, split the vendor roster from the vendor dispatch decision. Do it before customers learn the current homes.
- The store inherits, it never copies. One table keyed by scope kind, scope id and key; absence means inherit; the resolver walks property → organization → platform. Do not write resolved values onto each property for speed. That is the incumbent mistake, and in practice it cannot be undone.
- Stable string ids for every knob. They end up in URLs, audit rows, search entries, exports and support calls. Renaming one after launch is a migration.
- Scope in the URL. Cheap now, painful once links are sitting in customer emails.
- Ceilings at the platform level, not just defaults. One autonomy enum used by every module, with a platform row that caps it.
- Audit from day one — who changed what, from what, to what, when. Trivial against one writer now, impossible to reconstruct later, and the first “why did Clara do that?” call will ask for it.
- Convert the hard-coded early-customer behaviour into rows (the categories the customer-identifier fence already tracks, plus the name and timezone sweep in the section below). Every one of those is a knob that does not exist yet, so the inventory is not really ninety-seven until they are counted.
- Decide whether a level will ever sit between company and building — region, portfolio, owner. Judging by how management companies are structured, it will. Do not build it; write the resolver as a walk up a list of scopes rather than two hard-coded lookups.
Safe to defer, because none of it touches the store or the ids: field-level search, bulk apply-to-these-twelve-buildings, property templates for onboarding, change-history UI, per-role card visibility, export/import, a settings API.
Never build: per-unit settings, and a second writer for anything.
The decisive pass: every knob adjudicated
2026-09-08. Gera: “I don't want to tell you to do individual things … go through each one, break it down, validate the current designs.” This is that pass. It applies the two tests and does not relitigate them. Claims marked verified were read in the codebase; claims marked inference are reasoning from the business.
The building page: what belongs there, and the three tells
Asked directly of a live building page (/properties/<id>) and its three configuration cards: turnover policy, vendor job reference, renewal policy. None of the three belongs where it is — and each is wrong for a different reason. That is what makes a single “move it to Settings” answer useless and a policy necessary.
| On the page today | Where it is stored (verified) | Verdict |
|---|---|---|
| Turnover policy inspection delay, default trades, finishing trades | Inside PropertyKnowledge — the store of what Clara knows and says | A rule filed in the facts store. None of it is true about the building; all of it instructs Clara. → Property Settings › Maintenance, inherited from the company. |
| Renewal policy rent strategy, max increase, terms, MTM premium — plus “Eligibility & autonomy” | Also inside PropertyKnowledge, with an autonomy toggle embedded in the editor | Two faults at once. The rules are misfiled like turnover; and an autonomy switch is hiding inside a policy card. → Rules to Property Settings › Renewals; “autonomously send renewal offers” to the Autonomy ladder, beside maintenance. |
| Vendor job reference work order vs purchase order | Top-level on Property. Its own comment says: “this is PMS configuration” | Right kind of thing, wrong subject. It is a fact — but a fact about the PMS account, not the building, and one account serves the whole company. → Organization › Integrations, overridden only where a building is bound to a different account. |
The policy, in one line and three tells
The building page answers “what is true about this building?” Settings answers “what should the software do?”
If a card cannot be read as a sentence about the building — its address, its hours, its units, its fees, its people — it is not a building-page card.
In practice nobody adjudicates from the principle; they notice a smell. These are the three that catch essentially every misfiling in the inventory:
- A rule filed in the knowledge store.
PropertyKnowledgeis what Clara says; a rule is what Clara does. Anything in that store which changes behaviour rather than an answer is in the wrong house. Catches: turnover policy, renewal rent rules, late fee and grace days. - A fact about something other than the building. The right kind of thing attached to the wrong subject. A fact about the PMS account belongs to the company; a fact about a person belongs on the person. Catches: vendor job reference, Clara's PMS display name, delivery path, handyman quiet hours.
- An autonomy switch inside a policy editor. If one card holds both “max increase 8%” and “do it without asking”, the second one belongs on the ladder. A policy says what good looks like; a rung says how far Clara may go alone. Catches: renewal autonomy, and it is how the maintenance/renewal split happened in the first place.
A fourth tell, from the level test rather than the surface test: the label says “you” or “me” but the value is keyed to a building — which is how a personal reminder preference ended up stored once per property.
What survives on the building page after all three tells are applied: address, unit list and status, occupancy and the metric cards, pricing and fees, concessions, the knowledge sections Clara quotes, office hours and holidays, and the People card — who manages, who fixes, who owns. Every one of those reads as a sentence about the building. Nothing left on it instructs Clara.
1 — People are not settings
The question “where does the maintenance tech go?” has no satisfying answer because the thing being placed is not one kind of thing. Verified: a person responsible for something at a building is stored four different ways today.
| Who | Stored as | Shape |
|---|---|---|
| Handymen / in-house crew | Property.handymanVendorIds | References to real vendor records |
| Maintenance tech | Property.maintenanceTechPhone | A bare phone string |
| Escalation owner and CC | escalationOwnerEmail / CcEmail | Bare email strings |
| Staff and on-site managers | User.assignedPropertyIds + a spine PersonRole | Real accounts with permissions |
The decision: a person attached to a building is a role, never a setting. Settings may hold a routing decision — when this happens, page whoever holds that role — but the person lives once on the identity spine and every notification resolves through it at send time. Three shapes cover every “who” in the inventory:
- Role — a human who owes an answer and may need permissions. Person + PersonRole (staff) or Person + VendorMembership (crew). PM, on-site manager, maintenance tech, handyman, escalation owner.
- Contact record — an outside party you deal with but do not manage, stored like vendors are (a company plus named contacts). Owner/landlord, eviction law firm.
- Genuine string — an inbox nobody owns. The property inbox, an accounting@ alias, an assistance-programme address.
The test for the third row: if this address stopped being read, would a specific person be at fault? If yes, it is a person wearing an email address. Most of today's strings fail it.
What the strings already cost, in code we have
- Clara cannot recognise her own tech. Verified:
classify-origin-thread.ts:26says “Deliberately NOT matched: the property-levelmaintenanceTechPhone”. A phone string is not an identity, so an inbound text from the tech cannot be tied to them. The cost is already paid, in a comment explaining why it must be. - “Owner” means three different things. Verified:
demand-context.ts:13— “Property.ownerIdis a Better Auth userId for PMS credentials, not a [landlord]”. The word covers the PMS-credential holder, the escalation owner (staff), and the actual landlord. Row 69 “owner escalation recipient” is the second of those, and is the same field as row 13. - The code already asked for the fix. Verified: the doc comment on
escalationOwnerEmail(types.ts:2999) says owner resolution “should CHAIN INTOresolvePmContactEmailas its default rather than growing a second address ladder of its own.” That resolver exists (pm-contact-email.ts:44). One resolver, role → that person's contact → the property inbox, is not a new idea here; it is the idea the field's own author wrote down and did not get to build. - The spine is already ready. Verified:
VendorMembershipcarriespersonId,role,propertyIds,isInHouse(ADR-0033 collapsedVendorCompany.isInHouseinto this row, making it the single source of truth for in-house crew),quietHours,availability,startedAtandendedAt. Everything the handyman needs already exists as a row. The property simply does not point at it.
In plain terms, what strings cost you: the tech changes their number and you edit every building and miss one, which then pages a dead number silently. One person covering six buildings appears six times with nothing linking them. A phone string cannot be told “you may approve up to $500”, which is precisely what the Act with approval rung needs. The audit trail says an email address instead of a name and a role. And when someone leaves, nothing ends — the string keeps getting paged.
The model
The building page gets a People card — one row per role, each pointing at a real record. It is the canonical writer for who.
| Row | Shape | Backed by |
|---|---|---|
| Property manager, on-site manager | Role | Person + PersonRole. Read-only mirror — the Org's Team page owns the assignment, so the building page never becomes a permissions editor. |
| In-house crew (tech and handymen) | Role | Person + VendorMembership with isInHouse. Tech and handyman are one role: the person is who you page, the company is where the work order is assigned. Both derive from one membership, and both string fields retire. |
| Owner (landlord) | Contact record | An owner entity plus named contacts. Inference: owners are usually an LLC, so this needs a company-shaped record rather than a bare person. demandParty stays as the legal-name fact — it names a legal entity on a legal instrument, which is genuinely a fact. |
| Legal counsel (eviction firm) | Contact record | A vendor company with trade legal. It is a vendor; nothing about it needs a new mechanism. |
| Assistance programmes | String | An inbox nobody owns. Correctly a string. |
Settings holds routing rows, not addresses. Each row is event → recipient, where the recipient is a pointer: a role (whoever holds it at send time), a specific person, or an inbox. Escalations default to the PM role; lease-signed goes to accounting; the weekly owner report goes to the owner role. Every row badges its inheritance like any other setting. Migration is uneventful: backfill each string into a person and a role, let the resolver fall back to the legacy string until nothing reads it, then delete the field. No customer-visible change on day one.
2 — The five families you named
Follow-ups and chases: one control, five lanes
They are one knob wearing five hats. Every lane is the same shape — attempts × interval × channel × stop-when. What differs is who is on the other end and whether the lane is legally regulated. So: one Follow-ups card at Organization, one row per lane (prospect inquiry, re-engage, tour confirm, post-tour, vendor chase, renewal chase, collections chase), each expandable, each overridable per building. Above them, one contact ceiling at Organization only, with no property override — that is the company's compliance posture, and a per-building override is how a complaint starts. Post-tour follow-up is a lane of this, not its own knob. PM action reminders are not a chase; they nag your own staff, and they are handled below.
Money: one ceiling, and two things that are not ceilings
One autonomous-spend ceiling: Clara may commit up to $X without asking. The turnover cost cap is a sub-line of it, not a second ceiling. “Big-ticket items” needs no knob at all — it is simply anything above X, which is the approval rung doing its job. Two imposters: the delinquency threshold is measured in months and gates whether a renewal offer goes out, so it is a renewals policy rule, not a ceiling; and late fee and grace days are in the lease whether or not PropFlow exists, so they are a building fact.
Clara's identity: three things, not one
Brand (what she calls herself, sender name, tone) is Organization — every channel, every building. Lines (phone numbers, PMS display-name binding, from-address) are provisioned facts, shown read-only and never typed. The greeting line should not be a field at all: it is brand plus building name, derived. An editor there invites free text into a spoken prompt that is evaluated.
Delivery and routing: this is Integrations, and most of it is org-level
The delivery path and Clara's PMS display name are properties of the PMS account, and one database equals one organization. Both move up to Organization, inherited down, overridden only where a building is bound to a different account. The property inbox and the listings URL are facts about the building. What remains on Property Settings is bindings, all read-only.
Hours: two concepts, do not merge them
When the building is open (office hours, holidays) is a fact — building page. When Clara may contact people (resident quiet hours; handyman quiet hours, which belong to the person and follow them across every building) is a protection rule — Settings. Scheduling capacity (duration, lead time, buffers, weekday policy) is an instruction to the booking engine. Collapsing them would be wrong because they answer to three different people: the office manager, the compliance officer, and the leasing lead.
3 — What actually changes
Moves
| Knob | From → to | Why |
|---|---|---|
| Company name | User.companyName → the organization | Org identity sitting on a personal field |
| Clara's PMS display name; messaging delivery path | Property → Organization (inherited) | Both belong to the PMS account, which is org-level |
| Escalation owner; renewal contact; owner report recipients | Email strings → People card roles + routing rows | People stored as strings |
| Maintenance tech phone; handymen | → One “in-house crew” role on the People card | Two fields describing one person |
| Eviction firm | Script-set strings → a vendor company with trade legal | It is a vendor |
| SMS enabled; email replies; vendor quote and dispatch mail | A global row any signed-in user can flip → Organization capabilities | Wrong level and wrong permission today |
| Hidden-from-sync properties | A per-user list → Organization | A personal denylist governing company-wide sync |
| Turnover policy; renewal rent strategy | The knowledge blob → Property Settings | Rules living in the facts store |
| Delinquency threshold | Autonomy card → Renewals | Not a money ceiling |
| Late fee and grace days; application link; listings URL; property inbox | Settings → building page | All four exist without PropFlow |
| Vendor job reference (WO vs PO) | Building page → Organization | A convention of the PMS account |
| All four chase cadences | Module drawers as writers → Settings writes, drawers mirror | Five drawers each writing their own copy is the whole scattered feeling |
Splits — one row that is secretly two
| Row | The fact half | The decision half |
|---|---|---|
| Transfer destination | The office and emergency phone numbers are building facts | “Where does Clara transfer to” is a routing row defaulting to that number. Today they are one field, so the number is stored twice. |
| Escalation owner + CC | — | Owner is a role; the CC is an inbox. Verified: in the escalation bake-gate tests the CC is a PropFlow address on the Willows bench fixture — PropFlow watching its own bake. That is an internal watch list, not a customer setting. |
| Signature / sender identity | The from-address is the property inbox | The display name is company brand |
| Weekly owner report | Recipients are the owner role | The window (7 vs 30 days) is a company habit |
| MFA and SSO | — | The company mandates; the person enrols. Two rows, two levels. |
| PM action reminders | The building has no half — delete the property row | “Remind me, this often” is Account. “Staff get reminded at all, up to this cadence” is Organization. |
Right where they are, against instinct
These exist to stop the same argument being had twice.
- Tone and persona stay in code. Every prompt change is a replay-gated deploy, and free text from a customer would bypass every evaluation. At most a company-level enum later.
- Languages are not a knob. Language is per person on the spine, so a building-level toggle would answer a Spanish speaker in English at an “English” building.
- Handyman quiet hours belong to the handyman, not the building. His sleep follows him across every building he covers.
- The contact ceiling has no property override. A busy lease-up will want more touches. That is exactly the pressure the ceiling exists to resist.
- Tour duration is a company default. Most operators run one length; the per-building override is for exceptions, not the rule.
- PMS credentials are org-level. One database per company; two databases means two organizations.
- Assigned properties live on the Team page, not the building. It is a permission scope, so the company owns it and the building page mirrors it read-only.
- Hand-off mode stays visible even though only PropFlow can flip it. A customer who can see the rung understands why Clara is or is not answering.
- Vendor preferences by trade change nothing. Verified: of roughly twenty-six rows marked as inheriting from the company, this is the only one actually built. It is the reference implementation: build every other inherited row to look like it, and migrate it onto the generic resolver last, once that resolver can replay its tests.
Should not exist
The greeting-line editor (derived). Post-tour follow-up delay (a lane of the cadence card). Owner escalation recipient (the same field as escalation owner). The tech phone and the CC as customer fields. The leases-ending digest (its own doc says temporary). Free-text collections tone (enum or nothing). Vendor transfer-first and shadow mode (activation states, not switches). The workflow dispatch allowlist (a cutover flag). Tour buffers (not built — do not build until asked).
4 — Does the four-level design survive? Yes, with two amendments
Account is thin, and that is fine. A level is justified by who feels the change, not by row count, and “only me” is a distinct audience. It is also thin partly because rows were mis-filed onto Property: the reminders row alone justifies the level, and the giveaway is a card headed “Reminders for you” sitting on a per-building page. A useful rule falls out of it: if the label says “you” or “me” and the value is keyed to a building, it is mis-scoped. This is also the first place the chain runs company → account rather than company → property, which is the honest shape for a preference as opposed to a policy.
Organization earns its place, but read today's page as intent rather than state. Its job is to be the defaults level — every inherited row is authored here and merely overridden per building. The honest statement: roughly nineteen designed rows, zero writable, one working inherited chain. Until the organization writer exists, every “from company” badge in the wireframes above is fiction. That makes the sequence binding rather than advisory: organization writer, then the generic resolver, then the cards.
No fifth level. The two candidates are region and PMS account. Region is a permission scope, already handled by assigned properties; PMS account is an integrations binding. What operators actually want for “these six buildings” is a bulk-apply gesture on Property Settings, not a new level. Inference: revisit only if one customer runs several PMS databases or dozens of buildings under genuinely separate regional policies.
The building page and Settings stay two surfaces. Different readers (an agent opens the building page daily; property settings change a few times a year), different provenance badges (facts carry scraped / manual / PMS-synced; settings carry set here / from company, and mixing the two vocabularies is how a scraped fact gets mistaken for a company policy), and different blast radius (a settings change re-routes Clara; a fact change corrects the world). They need one seam, not one page: the People card is the canonical writer for who, and Settings routing rows point at those roles. Link both ways in the header.
Amendment 1 — the Autonomy card holds two things only: the ladder and the spend ceiling. Everything else that drifted onto it is either a rung behaviour or a module policy row. A card with fourteen switches is not a ladder.
Amendment 2 — Admin gets a named watch list for the per-property CCs, holds channel and shadow modes PropFlow staff use to observe a bake. Naming it is what stops those leaking into customer fields, as the escalation CC already did.
Verification note. Claims about classify-origin-thread.ts, demand-context.ts, VendorMembership, resolvePmContactEmail and the single built inheritance chain were read in the codebase today. Two claims from the drafting pass were corrected before publishing: a quoted comment on escalationOwnerEmail that does not exist in the code, and a CC address attributed to a customer's production configuration when it appears in a bench test fixture.
In one screen
- Four levels stay; the rule for which is "whose fact is it". Different per person → User. Same for every building the company runs → Organization. Another building could legitimately differ → Property. No customer may ever see or change it → Admin.
- Settings page vs. property detail vs. module: a knob that changes what a customer hears across channels is a setting; a fact about the building is on the property detail page (Clara cites it, it needs provenance); a policy a PM only changes with the queue in view gets an edit-in-context drawer on the module page and a browse row on the settings page — the same component and API, never two stores. That last shape is already how follow-up cadences work.
- Deciders get one Autonomy card per property, one row per module, a four-rung ladder, and a read-only mirror in the module. Customers set up to "Act with approval"; "Autonomous" is staff-set.
- Inheritance is platform default → Organization → Property, absence meaning inherit, with the source rendered as a badge. It needs an Organization writer first; today the Organization row cannot be edited at all.
- Prompts stay code; customers edit the facts that feed them. Flags and kill switches are Admin only; capabilities are Organization and only ever make Clara do less.
- Bottom-up, for onboarding: the hard-coded → configurable table maps every customer fact still living in code or env (phone routing, the prompt clock, listings sync, the AppFolio subdomain, the company address in a prompt, recipient lists, voice agent ids) to the field and card it becomes; the onboarding checklist is the resulting field list per level — the working spec for the settings pages.
- What is mis-scoped today: Company Name stored per user; "hide from sync" (which deletes data) keyed on a per-user list; three "customer" toggles on Admin Settings that are one global row for every org and writable by any signed-in user; setup-guide state shared by everyone; about 35 per-property behaviour switches — including the hand-off mode, the property time zone and the escalation owner — with no UI at all, each behind its own script.
The five decisions, as multiple choice
Each has a recommendation. The criteria in the next section are what the recommendation is derived from; the applied table further down is what each choice implies knob by knob.
Q1 — Where do the deciders live (Clara acts / asks / stays off)?
- Recommended. One Autonomy card on Property Settings, one row per module (Leasing, Tours, Maintenance, Renewals, Collections, Conversations), each row a four-rung ladder — Off / Suggest / Act with approval / Autonomous — plus that module's decider parameters (spend ceiling, delinquency threshold, voice-call-on-offer). Each module page shows a read-only pill ("Clara here: Act with approval") linking to the card. One writer, one place; the constitution's one-switch-on-the-property rule (Art. IV.3) and shadow/live-on-the-record (VII.4) both land here. The ladder maps onto fields that already exist (
capabilityStage,handoffMode, the send arms) rather than a new one. - Each module page owns its own autonomy card as the writer. Rejected: it puts an arm next to a queue where a PM toggles under pressure, and it recreates the "which of the two places is real" problem Gera named.
- Settings only, no mirror. Rejected: the review queue already needed inline copy explaining why items are there; the mirror is that explanation.
Q2 — Do Property settings inherit from the Organization?
- Recommended. Yes, for knobs that are the company's habit with the building as the exception: platform default → Organization → Property. Absence is inheritance; a stored value is an override. One resolver returns
{ value, source }and the UI renderssourceas the badge ("Inherited from Organization" / "Overrides organization — Reset"), so the badge cannot disagree with what the runtime used. This is the vendors chain the product already runs (resolvePreferredVendors), generalised. Knobs that are physically per building (phones, calendars, Twilio numbers) and the autonomy rung itself never read the org row, so an org edit can never arm a property. - Property-only, as today. Rejected: every operator with more than one building re-enters the same tour length, application link and reminder cadence per building, and an org-wide change never propagates.
- Org-only. Rejected: pricing source, office phones and calendars are genuinely per building.
On Gera's three examples: tour duration and application link become org default with property override; transfer destination stays property-only — it feels org-level only because a single-property customer collapses the two levels, which the UI should handle by hiding the split for single-property orgs (ADR-0019 already names this), not by moving the field.
Q3 — Which autonomy rungs may a customer set themselves?
- Recommended. Customers move between Off / Suggest / Act with approval. Autonomous (anything that arms a send at a live property) is staff-set and customer-visible, greyed with "Ask PropFlow to enable". Keeps the one-field, one-card shape without handing customers an arm (Art. IV.6: activation is Fede's go). Org default applies to the parameters (spend ceiling), never to the rung — blast radius stays per property (VII.3).
- Customers set all four rungs. Rejected until a property has run a clean month on Act-with-approval.
- Customers set none; staff set everything. Rejected: it keeps 16 of 17 properties dark by default, which is exactly the failure hot rule 13 records.
Q4 — Where do prompts live?
- Recommended. Prompt text stays code (Art. VII.7; the ElevenLabs deploy lane is hard-blocked to merge-only). Per-property variation is data the prompt interpolates, and it already is (
{{property_office_hours}},{{pm_phone_number}},{{property_policies}}, the hand-off directive). A customer never edits a variable as a variable; they edit the fact or setting that feeds it. The only new prompt-shaped customer fields worth adding are bounded and eval'd: a greeting line with a length cap and the fair-housing screen, and a tone selector from a fixed set — both on a Property "Clara" card, org default. Admin gets a read-only "what Clara is told about this property" rendering of the resolved variables, which the voice personalization route already computes. - Customer-editable prompt text. Rejected: it bypasses evals and the deploy lane.
Q5 — Feature flags versus capabilities
- Recommended. Flags, arms and kill switches are Admin only, in the Access Inspector, catalogued in the arms registry (already enforced by a coverage test) — never on a customer page however nicely labelled. Capabilities are Organization, customer-facing, and they scope rather than arm: modules enabled, channels permitted, languages. Reviewer's rule of thumb: a capability off makes Clara do less; it can never make her send more. Consequence: the two toggles on
/admin/settingstoday (SMS, vendor emails) are global kill switches wearing capability labels — they move to the arms tab, and module flags migrate from the platform-wide row toOrganization.settings, which the type already declares. - Keep customer-facing toggles that arm sends. Rejected: that is how the message-delivery PR shipped wording nobody decided on.
What never belongs on a settings page
- Facts about the building — hours, amenities, fees, the phone the public dials — live on the property detail page under Policies & Knowledge, with provenance, because Clara cites them.
- Per-record fields — a tenant's language, do-not-contact, a vendor's quiet hours, a unit's availability.
- One-off actions — import, re-sync, delete, backfill, send now.
- Arms and kill switches (Admin only).
- UI state — setup-guide and Ask Clara dock state, dashboard layout (per-user prefs, stored on the user, no page).
- Anything with no reader. A setting ships only with its consumer (hot rule 13).
Placement criteria — eight testable rules
From the taxonomy sweep; each rule names a PropFlow example, a counter-example, and what would falsify it.
Each rule is a question you can answer yes/no about a knob. Apply them in order; the first "yes" wins.
C1 — Whose fact is it? (level). If the value would be different for another person at the same company it is User; if it would be the same for every building the company runs it is Org; if another building of the same company could legitimately differ, it is Property; if no customer may ever see or change it, it is Admin. This is the Vercel account/team/project test and it is already CLAUDE.md's "whose fact is this?" (customer-facts-as-data).
- Example:
displayZone(how times read to me) → User (UserPreferences.displayZone,AccountSettingsShell.tsx). Counter-example: Company Name → Org, yet today writesUser.companyName(OrganizationSettings.tsxheader) — a level bug the file itself names. - Falsifier: find a knob where two users at one company need different values and it sits at Org.
**C2 — Does it change what a customer hears across channels? (settings page vs. elsewhere).** A knob that changes Clara's spoken/written behaviour on more than one channel (voice+SMS+email) is a setting and goes on the settings page for its level. A knob that only changes what an operator sees in a queue stays on the module page.
- Example: office phone / transfer destination (voice, and the missed-call email) → Property Settings "Calls" card (
PropertySettingsShell.tsx:231). Counter-example: the collections review-queue filter default → module page (review/page.tsx), never settings. - Falsifier: a settings-page knob that no outbound path reads (today:
AppSettings.setupGuideState— a UI pref on the settings row).
C3 — Is it a fact about the building, or a rule about behaviour? (property detail vs. settings). Facts (address, hours, amenities, fees, phone the public dials, unit count) live on the property detail page under "Policies & Knowledge" because Clara cites them and they need provenance (scraped vs. manual). Rules (how long a tour is, when to auto-approve) are settings. AppFolio/Buildium put late-fee policy on the property record for this reason; the product already split "collections policy" into cadence (setting) and late fee (knowledge) with a scope line (MoneyRulesSection.tsx header).
- Example:
PropertyKnowledge.officeHours→ detail page (PropertyDetailClient.tsx:983). Counter-example:tourDayPolicy("can a tour be booked Saturday" — a rule, distinct from "is the office open") → settings (types.ts:13072says exactly this). - Falsifier: a fact that must be edited from settings to be honoured, or a rule stored in
PropertyKnowledge(today:renewalPolicy,turnoverPolicy— rules living in the knowledge blob; see Risk 5).
C4 — Is it only meaningful while looking at the work? (module page / drawer). If a PM would only ever change it with the queue in view (cadence while looking at 60-day-past-due accounts), it gets an edit-in-context drawer on the module page and a browse-and-compare row on the settings page — the same component, same API, never two stores. This is Intercom/Front's "rules live in the inbox" and Linear's team workflow settings; it is already the product's decided shape (PolicyDrawer.tsx header: "answered by construction rather than by picking one place").
- Example: collections chase cadence → drawer on
/collections+ Follow-ups card. Counter-example: the account-wide contact ceiling → settings only (FollowupPolicyEditor.tsxshowGlobalLimitdoc: showing it inside one module "would invite editing it as if it were that module's"). - Falsifier: a policy editable in a module drawer that is not visible on the settings browse page, or vice-versa.
C5 — Does it decide whether Clara acts or asks? (the Autonomy card). A knob whose only effect is who decides (Clara / Clara-then-approve / human) is a decider, not a policy, and every decider for a module sits together in one "Autonomy" card at Property level, read-only mirrored in the module. A threshold that feeds a decider (auto-approve $) is a decider parameter and sits in the same card.
- Example:
aiAutoApproveThreshold(PropertySettingsShell.tsx:544),handoffMode,capabilityStage.maintenance. Counter-example:tourDurationMinutes— changes the offer, not the decision → Leasing card. - Falsifier: an autonomy decision reachable from two cards, or one that has no property-level switch (Art. IV.3).
C6 — Is the default the company's and the exception the building's? (inherit). A Property knob inherits from Org when (a) most buildings of one operator share the value and (b) an org-wide change should propagate to un-overridden buildings. Then the property card shows the effective value with an "Inherited from Organization" badge and a "Reset to organization default" action; an override is a stored value, absence is inheritance. Vercel env-vars-per-environment and Linear team-overrides-workspace are the model; resolvePreferredVendors is the in-repo precedent.
- Example: tour duration, application link, PM-reminder cadence, auto-approve threshold, follow-up cadences. Counter-example: office phone, emergency phone, Twilio numbers, Outlook calendar — physically per building, no meaningful org default; and
handoffMode— an activation Fede makes one property at a time (Art. IV.6), so an org default would violate "blast radius is per property" (VII.3). - Falsifier: a property override that survives an org change it should have followed, or an org default that arms a real property Fede did not name.
C7 — Can a customer be trusted with it without a soak? (Admin vs. Org "capabilities"). If flipping it needs the bench, a replay, or Fede's go (Art. IV.1, IV.6), it is Admin (Access Inspector arms/kill switches), never on a customer page, even a per-property one. If it is a product capability the customer bought or opted into (module on/off, a channel enabled), it is an Org "capabilities" toggle — customer-facing, but it scopes, it does not arm.
- Example:
kill.tourPipelineV2(arms-registry.ts:509) → Admin. Counter-example:enabledModules['leasing.renewals']— a customer-legible capability that today is platform-wide and admin-only (/api/admin/modulesheader: "the flags are platform-wide, not per-organization"). - Falsifier: a customer page that can arm a send, or an admin page holding a knob a customer must set to onboard.
C8 — Is it a one-off action or a per-record field? (never settings). Connect/disconnect an inbox, import contacts, re-sync, delete property, "mark this tenant do-not-contact" are actions or record fields; they live where the record lives. A settings page holds standing values only.
- Example: "Import contacts" → property detail (
ContactImportModal). Counter-example that is borderline and stays: Outlook Connect/Disconnect on Property Settings — it is an action but it produces a standing connection whose status is the setting (PropertySettings.tsxcalendar/email status). - Falsifier: a settings card with a primary button that is not Save/Reset/Connect.
What the codebase already says
Most of the "open" question is already decided in data — this is the evidence table the criteria were derived from.
| Fact | Where | ||
|---|---|---|---|
| Four levels shipped; names/routes in one file | src/lib/domain/settings/settings-levels.ts | ||
Property settings are two DDB rows: PROP#id / LEASING_SETTINGS and MAINTENANCE_SETTINGS, resolved property → hardcoded default, no org tier ("No global fallback. Operational settings live per-property only.") | src/lib/platform/settings-resolver.ts, src/lib/data/types.ts:13060 (PropertyLeasingSettings), :13278 (PropertyMaintenanceSettings) | ||
Organization.settings already declares an inheritance layer — "Cross-property defaults — overridable per-property where the per-property setting type allows it" — but has no writer and nothing reads it | types.ts:12484 (OrganizationSettings), OrganizationSettings.tsx header (Company Name writes User.companyName) | ||
| Org-level preferred vendors with per-trade property override is the one working inheritance chain in the product | Organization.preferredVendors (ADR-0060), Property.preferredVendors "property-over-org … read only through resolvePreferredVendors" (types.ts Property block) | ||
Follow-up cadences are org-scoped rows edited from a property context (the API derives the org from propertyId); the same editor is mounted in Settings AND in each module's PolicyDrawer | src/app/api/automations/cadences/route.ts header, src/components/domain/policy/PolicyDrawer.tsx, FollowupPolicyEditor.tsx:760-800, mounts in collections/CollectionsClient.tsx, leasing/renewals/board/_components/BoardV2.tsx, maintenance/work-orders/(list)/WorkOrderListClient.tsx, maintenance/turnovers/TurnoversClient.tsx, leasing/prospects/ProspectsClient.tsx | ||
| The one property-scoped rule inside a module drawer (collections late fee) carries an explicit scope line because it sits next to org-scoped cadence | src/components/domain/policy/MoneyRulesSection.tsx header ("Applies to <name> only") | ||
Every on/off gate is catalogued with family / polarity / control; UI at /admin/dev/access-inspector tabs modules, arms, roles | src/lib/domain/admin/arms-registry.ts; src/app/(workspace)/(operations)/admin/dev/access-inspector | ||
AppSettings is ONE global CONFIG/SETTINGS row carrying kill switches, module flags, a leaked subscription, and two UI prefs (setupGuideState, askClaraState) | types.ts:13322, src/app/api/settings/route.ts header (the 2026-09-07 leak stopgap) | ||
/admin/settings "Clara AI → SMS Messaging" and the two vendor-email toggles write that global row via POST /api/settings (platform-wide, despite the customer-sounding labels) | admin/settings/page.tsx, _components/settings-sections.ts | ||
| The autonomy switch that exists is `Property.handoffMode: 'coworker' \ | 'autonomous'` — ONE switch, on the property, script-only writer, drift-fenced | types.ts Property block (handoffMode doc), src/lib/domain/escalation/handoff-mode.ts, scripts/set-handoff-mode.ts | |
| Per-module graduation exists as data: `Property.capabilityStage.{leasing,renewals,maintenance}: off \ | shadow \ | live` | types.ts Property block (G-1) |
Renewal / collections / new-lease arms are a global script-written row, some with a propertyAllowlist | types.ts:13390 (RenewalArmState), OperatingModeArmState :13635 | ||
Prompts are config-as-code: ONE fleet-wide voice prompt per agent, per-property variation only via dynamic variables ({{property_name}}, {{property_office_hours}}, {{pm_phone_number}}, {{property_policies}}, {{triage_greeting}}, the hand-off directive); deploy only on merge (hard-blocked) | agents/clara/lib/voice-agents/*.ts, handoff-policy.ts header ("there is no prompt-override path"), agents/clara/lib/agent/clara-personality.ts, scripts/lib/elevenlabs-deploy-lane.ts | ||
| Building facts and structured policies live on the property detail page, not settings: office hours, holiday policy, pricing/fees, renewal policy (rent strategy, term options, MTM premium, voice-call), turnover policy (inspection timing, default trades/vendors, dispatch cost cap), vendor job reference mode, handymen | PropertyKnowledge (types.ts — officeHours, holidayPolicy, pricingDetails, renewalPolicy, turnoverPolicy), properties/[id]/PropertyDetailClient.tsx (PricingDetailsEditor, TurnoverPolicyEditor, VendorJobReferenceCard, PropertyDetailsEditor) | ||
| Constitution: "One switch, on the property, or none … visible on the property's admin page" (IV.3); "configuration lives in data" (VII.1); "every customer has one kill switch" (VII.3); test/shadow/live on the record (VII.4); "Prompts, policies, and knowledge are code" (VII.7) | docs/OPERATING-CONSTITUTION.md Art. IV, VII | ||
Hot rule 13 / #4694: 16 of 17 properties sat dark behind per-property *Enabled opt-ins — the reason arms are now "halt" not "enable" | types.ts (renewalLapseDigestEnabled doc, collectionsHalt doc), docs/runbooks/renewal-orchestration.md:44 |
The upshot: the product already has the shape of the answer — org default → property override for vendors, org-scoped policy edited in module context, one autonomy switch on the property, prompts as code with data variables. What is missing is (a) a general inheritance resolver (only vendors have one), (b) an Organization writer, and (c) a consistent rule for which surface shows what.
Applied table — about 95 knobs, current and proposed
Level: U/O/P/A. Surface: SP = settings page for that level · PD = property detail (Policies & Knowledge) · MP = module page (read-only mirror) · DR = drawer inside a module (edit-in-context, also browsable on the settings page) · AI = Admin Access Inspector. Inherits: O→P = org default, property override with badge and reset.
Level: U=User, O=Org, P=Property, A=Admin. Surface: SP=settings page for that level, PD=property detail (Policies & Knowledge), MP=module page (read-only mirror or filter), DR=drawer inside module (edit-in-context, also browsable on SP), AI=Admin Access Inspector. Inherits: O→P means org default, property override with badge+reset.
Clara voice & messaging
| Knob | Level | Surface | Inherits? | Rationale | Exists today? |
|---|---|---|---|---|---|
| Company/brand name Clara signs as | O | SP | — | C1 org identity; C2 every channel | yes, but writes User.companyName (OrganizationSettings.tsx) |
Clara display name at the PMS (pmsMessagingSync.claraDisplayName) | P | SP Integrations | O→P | per-PMS-account identity; org default | schema only (Property.pmsMessagingSync) |
Greeting line (triage {{triage_greeting}}) | P | SP Clara card | O→P | C2 spoken on every call; templated text is a variable, not a prompt | variable exists, value derived — no editor |
| Persona / tone (CLARA_CORE_PERSONALITY) | A | code | — | VII.7 prompts are code; changing it is a replay-gated deploy | clara-personality.ts |
| Languages Clara will answer in | O | SP capabilities | O→P | product capability; per-property override where a building is bilingual | not a knob; language is per-person on the spine (ADR-0089) |
| Business hours (office open/closed) | P | PD | — | C3 building fact, scraped/manual with provenance | PropertyKnowledge.officeHours (PropertyDetailClient.tsx:983) |
| Holiday closures | P | PD | O→P (federal default) | C3 fact; org may standardise | PropertyKnowledge.holidayPolicy |
| After-hours behaviour (take message vs. dial) | A | code | — | one fleet policy by owner ruling; driven by hours data | voice-agents/after-hours-message-desk.ts |
| Transfer destination (office phone) | P | SP Calls | — | C6 counter-example: physically per building | yes Property.officePhone (PropertySettingsShell.tsx:237) |
| Emergency phone | P | SP Calls | — | same | yes Property.emergencyPhone |
| Vendor transfer-first on inbound | P | Autonomy card | — | decider; today an env arm | arms-registry.ts:465 (vendor.transferFirst, deploy-controlled) |
| Hand-off mode (coworker / autonomous) | P | Autonomy card (read-only until Fede flips) | no | Art. IV.3/IV.6 one switch, activation is Fede's; show it, don't let a PM write it | Property.handoffMode, script-only |
| Escalation owner + CC email | P | SP Notifications | O→P | who is paged; org default (ops inbox), property override | Property.escalationOwnerEmail/CcEmail (script-set) |
| SMS enabled (channel capability) | O | SP capabilities | O→P (shadow) | C7 customer capability, not an arm | global AppSettings.smsEnabled on /admin/settings (wrong level) |
| Email replies enabled | O | SP capabilities | O→P | same | global AppSettings.emailsEnabled |
Shadow mode per channel (emailShadowMode, smsShadowMode) | P | Autonomy card, read-only; A writes | — | VII.4 test/shadow/live is an activation state | Property.emailShadowMode/smsShadowMode |
| Test property flag | A | AI (per property) | — | behaviour switch that silences grading etc.; never customer-facing | Property.isTest |
| Clara's own numbers (Twilio) | P | SP Integrations (read-only list) | — | routing fact; provisioned by staff | Property.twilioNumbers (absent everywhere; env map fallback) |
| Messaging delivery path (direct vs via PMS) | P | SP Integrations | O→P | integration posture | Property.messagingDelivery (schema) |
Leasing
| Knob | Level | Surface | Inherits? | Rationale | Exists today? |
|---|---|---|---|---|---|
| Tour duration | P | SP Leasing | O→P | Gera's instinct is right: most operators run one length; building overrides | yes tourDurationMinutes |
| Tour minimum lead time | P | SP Leasing | O→P | same shape | tourMinLeadMinutes (API, no UI) |
| Per-weekday booking policy | P | SP Leasing | — | staffing is per building (types.ts:13072) | tourDayPolicy (API, no UI) |
| Tour buffers between slots | P | SP Leasing | O→P | not built | no |
| Application link | P | SP Leasing | O→P | one portal per PMS account, per-property override for sub-portals | yes applicationLink |
| Pricing & availability source | P | SP Leasing | — | which store is truthful is per building (scaffold rent rolls) | yes leasingSource + resolvedLeasingSource |
| Qualification rules (income multiple, pets, screening) | P | PD Policies | O→P | C3: facts Clara cites; org standardises | partial in PropertyKnowledge.pricingDetails / lease policy |
| Post-tour follow-up delay + channel | P | SP Leasing | O→P | policy | yes |
| Prospect follow-up cadences (inquiry, re-engage, tour-confirm, post-tour) | O | DR on /leasing/prospects + SP Follow-ups | O (P override not built) | C4; API is org-scoped | yes FollowupCadenceOverride |
| Account-wide contact ceiling | O | SP Follow-ups only | — | governs every lane | yes TouchBudget |
| Escalated tour booking (Clara books when escalated) | P | Autonomy card | — | decider | Property.escalatedTourBookingEnabled |
| Tour-request SMS | P | Autonomy card | — | send arm | Property.tourRequestSmsEnabled |
| Listing auto-publish on NTV | P | Autonomy card | — | send arm | arms-registry.ts:198 |
| New-lease pipeline (approved app → next-steps) | P + A | Autonomy card (P half); AI (global half) | — | two-factor arm | RenewalArmState.newLeasePipelineSending + property flag |
| Rent-roll auto-sync | P | Autonomy card | — | decider on PMS writes | Property.autoSyncEnabled |
| Website listings sync URL | P | SP Integrations | — | fact about the building's site | Property.publicListingsSync |
Maintenance
| Knob | Level | Surface | Inherits? | Rationale | Exists today? |
|---|---|---|---|---|---|
| Auto-approve threshold ($) | P | Autonomy card | O→P | decider parameter; company spend policy | yes aiAutoApproveThreshold |
| Turnover dispatch cost cap | P | Autonomy card (same row, "also for turnovers") | falls back to threshold | already "one autonomous-spend ceiling, not two" | turnoverPolicy.autoDispatchCostCap (PD today) |
| Emergency definitions (what counts as after-hours emergency) | O | SP Maintenance policy | O→P | legal/insurance posture is company-wide | no (issue types are code) |
| Vendor preferences by trade | O | DR on /vendors + SP | O→P (built) | the one working chain | Organization.preferredVendors/Property.preferredVendors |
| Handymen (in-house) | P | PD (roster) | — | facts about who works this building | handymanVendorIds (Maintenance Automation card, expandable) |
| Maintenance tech phone | P | SP Notifications | — | per building | yes |
| Handyman quiet hours | — (per person) | Vendor record | — | ADR-0053: per-handyman, not per property | VendorMembership.quietHours |
| Resident quiet hours | O | SP Clara card | O→P | company posture; state law varies per property | code default |
| Dispatch autonomy (Clara dispatches vs. asks) | P | Autonomy card | — | decider | capabilityStage.maintenance, vendor.dispatch arm |
| Vendor quote / dispatch emails | O | SP capabilities | — | capability, not an arm | global AppSettings.vendorQuoteEmails/vendorDispatchEmails on /admin/settings (wrong level) |
| Vendor chase cadences | O | DR on /maintenance/work-orders + SP | O | C4 | yes |
| Vendor job reference (WO vs PO) | P | PD | O→P | PMS convention per account | Property.vendorJobReferenceMode (VendorJobReferenceCard) |
| Custom turnover task types | P | PD / turnover module | O→P | templates | PropertyTurnoverSettings.customTaskTypes |
| Turnover policy (inspection delay/time, default trades, finishing trades) | P | DR on /maintenance/turnovers | O→P | rule, not fact → move out of knowledge (Risk 5) | PropertyKnowledge.turnoverPolicy via PD TurnoverPolicyEditor |
| Photo deferral at intake | A | AI | — | behaviour arm under soak | arms-registry.ts:291 |
Renewals
| Knob | Level | Surface | Inherits? | Rationale | Exists today? |
|---|---|---|---|---|---|
| Open window (days before lease end) | P | SP Leasing "Open renewal" | O→P | policy | yes renewalAutoStartDaysBeforeLeaseEnd |
| Rent strategy / max increase / term options / MTM premium | P | DR on /leasing/renewals | O→P | rules, currently in knowledge blob (Risk 5) | PropertyKnowledge.renewalPolicy (RenewalsPolicyCard on PD + renewal detail) |
| Delinquency threshold for manual review | P | Autonomy card (renewals row) | O→P | decider parameter | renewalPolicy.delinquencyThresholdMonths |
| Voice call on offer | P | Autonomy card | O→P | channel decision | renewalPolicy.voiceCallEnabled |
| Autonomous sending / holdover conversion / auto-start | A | AI arms | — | global script arms, Fede's call | RenewalArmState |
| Workflow-owned dispatch allowlist | A | AI | — | infra cutover | workflowOwnedDispatch |
| Renewal chase cadence | O | DR + SP | O | C4 | yes |
| Renewal contact email / phone | P | SP Notifications | O→P | who is told | renewalContactEmail/Phone (API, no UI) |
| Execution CC emails ("Lease signed") | P | SP Notifications | O→P | accounting team is usually org-wide | yes |
| Leases-ending digest | P | SP Notifications | O→P | temporary opt-in, remove per its own doc | renewalLapseDigestEnabled (script) |
Collections
| Knob | Level | Surface | Inherits? | Rationale | Exists today? |
|---|---|---|---|---|---|
| Chase cadence | O | DR on /collections + SP | O | C4 | yes |
| Late fee / grace days | P | DR extra (Money rules) | O→P | rule; today in knowledge with a scope line | pricingDetails.lateFee via MoneyRulesSection |
| Tone (firm/friendly) | O | DR | O→P | copy variant; must be an eval'd variant, not free text | no (copy is code) |
| Quiet hours (resident's clock) | O | SP Clara card | O→P | ADR-0129 | code |
| Enrollment threshold ($ / months) | O | DR | O→P | policy | constants |
| Human approval before every send | A | code (ADR-0125) | — | never a knob today; becomes the "act with approval" rung | /review queue |
| Halt brake | A | AI | — | operator stop, not a customer setting | RenewalArmState.collectionsHalt |
| Eviction-firm / assistance correspondents | P | PD Integrations | — | customer identity facts | Property.collectionsCorrespondents, demandParty (script) |
| Owner escalation recipient | P | SP Notifications | O→P | who is told | owner-escalation.ts |
Conversations
| Knob | Level | Surface | Inherits? | Rationale | Rationale / exists |
|---|---|---|---|---|---|
| Auto-reply on/off per channel | P | Autonomy card | O→P | = capabilityStage per domain | capabilityStage |
| Human takeover rules (mute on staff reply, unmute on resolve) | A | code | — | one fleet behaviour | escalation/stale-mute-sweep.ts etc. |
| Signature / sender identity | P | SP Integrations | O→P | sendGridSenderIdentity per property; org default name | Property.sendGridSenderIdentity |
| Operational senders to read-not-answer | P | PD Integrations | — | customer identity facts | operationalDataSenders, appfolioTrustedSenders (script) |
| Topic labelling / grading | A | AI runtime switches | — | observability | runtime.* arms |
Notifications (who is told what)
| Knob | Level | Surface | Inherits? | Rationale | Exists |
|---|---|---|---|---|---|
| PM action reminders on/off, count, interval | P | SP Notifications | O→P | ADR-0104; company cadence | yes pmActionReminders |
Property inbox (propertyEmail) | P | SP Integrations | — | fact | Property.propertyEmail |
| Weekly owner report recipients + window | P | SP Notifications | O→P | owners are per building; window is org habit | yes |
| My notification preferences (digest vs. each) | U | SP User | — | C1 | no |
| Slack/ops channel for holds | A | AI | — | PropFlow ops | env |
Integrations
| Knob | Level | Surface | Inherits? | Rationale | Exists |
|---|---|---|---|---|---|
| AppFolio / PMS credentials, subdomain | O | SP Integrations | — | one database per org (two DBs = two orgs) | yes (OrganizationSettingsShell.tsx:313) |
| PMS account binding per property | P | SP Integrations (read-only) | — | which DB this building lives in | Property.pmsAppfolioAccount |
| Outlook calendar / inbox | P | SP Integrations | — | property-keyed today; org connection is Fede's future seam | yes |
| Hidden-from-sync properties | U → should be O | SP Org | — | today User.ignoredPropertyIds — a per-user denylist governing org sync (Risk 6) | OrganizationSettings.tsx |
| Twilio number provisioning | A | AI | — | staff | env map |
Security / team
| Knob | Level | Surface | Inherits? | Rationale | Exists |
|---|---|---|---|---|---|
| Team members, invites, roles | O | SP Team | — | Linear/Vercel members | yes |
| Assigned properties per user | O | SP Team row | — | scope | User.assignedPropertyIds |
| Role → tab permission matrix | A | AI roles | — | platform policy ("hierarchy governs invites only, never Clara capabilities") | /api/admin/permissions |
| MFA / SSO / sessions | O (policy) + U (enrol) | SP | — | org mandates, user enrols | no |
| Impersonation audit | A | AI | — | staff | ADR-0019 |
Billing / plan
| Knob | Level | Surface | Inherits? | Exists |
|---|---|---|---|---|
| Plan, unit count, invoices, card | O | SP Billing | — | yes (/api/billing/status) |
| Sandbox / purpose | A | AI | — | Organization.isSandbox, purpose |
Admin / platform
| Knob | Level | Surface | Exists |
|---|---|---|---|
Module flags (enabledModules) | A today → O capability | AI modules | platform-wide row (/api/admin/modules header says org-scoping "is a separate migration") |
Kill switches (kill.*) with per-property false override | A | AI arms | tour-pipeline-flag.ts |
| Two-factor send arms, global env arms | A | AI arms / deploy | arms-registry.ts |
Staged cutovers (tour.deciderMode) | A | deploy | tour-decider-flag.ts |
| Test phones | A | AI | AppSettings.testPhones |
| Eval/bench toggles, trace capture | A | AI runtime | runtime.* |
| Escalation holds (bake gate) | A | /admin/escalation-holds | escalationBakeApprovalRequired |
User UI state (setupGuideState, askClaraState) | U, not A | not settings at all | on the global CONFIG/SETTINGS row today (Risk 6) |
Row count: ~95.
The recommended model in detail
(a) Inheritance chain and display
Chain: platform default (code constant) → Organization → Property. No unit or user tier for behaviour knobs — a unit-level exception is a fact on the unit record (C3), and a user-level one is a preference (C1), never a Clara behaviour. This matches today's vendors chain and settings-resolver.ts's "property → hardcoded default", with one new middle rung.
Mechanics, so the resolver stays one function:
- Storage stays per-row (
OrganizationSettingsgains the leasing/maintenance/notification fields marked O→P above;PropertyLeasingSettingsetc. stay). Absence is inheritance; a stored value is an override. Noinherit: truesentinel —undefinedalready means "use the default" everywhere in these types. - One
resolvePropertySetting(propertyId, key)insrc/lib/domain/properties/returning{ value, source: 'property' | 'organization' | 'default' }. Thesourceis what the UI renders as the badge, so the badge cannot lie about what the runtime used (the same reasonresolvedLeasingSourceis computed by the resolver, not the UI —leasing-settings/route.tscomment). - Display: on the property card, field shows the effective value; helper text "Inherited from Organization" (or "PropFlow default"); editing stores an override and the helper becomes "Overrides organization (Reset)". On the org card, a count chip "3 of 12 properties override this".
- Only knobs whose row says O→P inherit. Things marked "—" (phones, calendars, Twilio, handoffMode) never read the org row, so an org edit can never arm a property (C6 falsifier).
(a2) The company decides WHICH keys a building may override — so the resolver must return permission, not only value
Added 2026-09-09, from the founders’ thread. Fede: “right now the policies are on the property level… I don’t know if you want to rip that out and make it its own thing on the nav or nested under Clara… where you could visualize the global settings and then be able to visualize property.” Gera’s answer to the second half: an overridable key at property level shows “a little green indicator that it’s allowed to override by property firm” and is editable; a non-overridable one renders greyed.
That is not a styling note. It changes the read path. { value, source } answers “what is in force and where did it come from”; it cannot answer “may I write this here?” — and the screen has to answer that before it can decide whether to render a control or a lock. The management firm is the rule book: it sets quiet hours, pet fees and policies, and which of those a building may depart from. So:
- The resolver returns permission, and it takes TWO fields, not one. My first draft said
writableHere; the architecture side is landing the sharper split and it is the one to build against —resolveEffective(ctx, path, key, {writeTarget})returns awriteDispositioncarrying, among others,rungOverrideAllowed(does the FIRM permit this rung to override this key — derived independently of who is looking) andmayWrite(that AND the registered placement AND this actor’s own grants), pluspermissionSourcefor provenance andreasonfor the refusal text. Render the green indicator offrungOverrideAllowed; enable or disable the control offmayWrite. Collapsing them into one field is a real bug, not a simplification: a key can be overridable in principle while this person still cannot write it, and a single field would have shown a padlock on an overridable key and told the reader the firm had locked it when it had not. Computed by the resolver either way, never inferred by the UI — a locked key that renders editable is a broken promise the moment someone hits save. (Names near-final, not final: the rows were uncommitted 2026-09-09.) - Three display states, not two. Set here (this building authored it) · From company — you may override (the green indicator; inherited and editable) · From company — locked (inherited, greyed, no control). Today’s page has only the first two, so every inherited row on it is implicitly claiming to be overridable, which is now a claim the design must stop making by default.
- The lock list is itself a company setting — a per-key “buildings may override this” switch on the Organization card, defaulting to allowed. Otherwise the firm’s rule book is a code constant and no customer can be strict. It routes through the key slot’s existing
writerPolicyrather than a parallel switch beside it: one source for permission, with the value constraint staying where it already lives. - Ceilings are not locks. A ceiling clamps a value the building may still set (row 6: the company cap wins, the building clamps); a lock removes the control. Both exist and they read differently on screen — a clamped field shows its own value with a cap badge, a locked field shows the company’s value and no field at all.
(b) Deciders — argue for one Autonomy card per module at Property, with two amendments
The instinct is right and the constitution forces it: IV.3 wants exactly one per-property switch, visible on the property's admin page, and VII.4 wants test/shadow/live on the record. The codebase is already converging: handoffMode (one switch), capabilityStage (per-module off/shadow/live), aiAutoApproveThreshold + autoDispatchCostCap (one spend ceiling).
Recommended shape — one "Autonomy" card on Property Settings, one row per module (Leasing, Tours, Maintenance, Renewals, Collections, Conversations), each row = a 4-step ladder Off / Suggest / Act with approval / Autonomous plus that module's decider parameters (spend ceiling, delinquency threshold, voice-call-on-offer). Mirror: a read-only "Clara here is: Act with approval" pill in each module page header that links to the card (C4: read-only mirror, never a second writer — the two-place problem Gera named).
Amendments that matter:
- Not every rung is customer-writable. "Autonomous" (and any rung that arms a send at a live property) is Fede's activation (IV.6). The card shows the current rung to the customer; the write for Off↔Suggest↔Act-with-approval is a customer action; Autonomous is staff-only and greyed with "Ask PropFlow to enable". That preserves the one-switch rule (one field, one card) without handing customers an arm.
- Map the ladder onto what exists instead of inventing a new field. Off =
capabilityStage: 'off'; Suggest ='shadow'; Act with approval ='live'+ review-queue gating (ADR-0125 collections, vendor outreach first-dial); Autonomous ='live'+handoffMode: 'autonomous'/ the module's send arm. Today's scattered booleans (escalatedTourBookingEnabled,tourRequestSmsEnabled,autoSyncEnabled) become rows of this ladder or collapse into it. - Org default: only for the parameters, not the rung. Spend ceiling inherits; the rung does not (VII.3 "blast radius is per property").
Against a per-module page card as the writer: it would put an arm next to a queue where a PM toggles under pressure, and it re-creates the "two places" problem. Against a settings-only card with no mirror: the review queue's "why is this here?" already needed inline copy (review/page.tsx header) — the mirror answers that.
(c) Prompts
- Prompt text = Admin/platform code, as today (VII.7;
elevenlabs-deploy-lane.tshard block). No customer-editable prompt text, ever — it bypasses evals. - Per-property prompt variables = data the prompt interpolates, and they already are:
{{property_office_hours}},{{pm_phone_number}},{{property_policies}}, the hand-off directive (handoff-policy.tsheader spells out why). The rule: a customer never edits a variable as a variable; they edit the fact or setting that feeds it (office hours on the detail page, transfer destination in Calls). The only new "prompt-shaped" customer fields worth adding are bounded, eval'd variants: greeting line (short text with a length cap and a fair-housing screen likefairHousingScreenFailedAton knowledge sections) and a tone selector from a fixed set — both on a Property "Clara" card, O→P. - Admin surface: a read-only "what Clara is told about this property" rendering (the resolved dynamic variables) on the property's admin view, so staff can see the effect without a replay.
voice/personalization/route.tsalready computes it.
(d) Feature flags vs. capabilities
- Feature flags / arms / kill switches = Admin only, in the Access Inspector, catalogued in
arms-registry.ts(already enforced byarms-registry-coverage.test.ts). Never on a customer page, even labelled nicely. Today's/admin/settingsSMS and vendor-email toggles are global kill switches wearing capability labels — move them to the arms tab and delete the page's toggles. - Capabilities = Org, customer-facing, and scope rather than arm: modules enabled (today platform-wide
enabledModules— migrate toOrganization.settings.enabledModules, which already exists on the type), channels enabled (SMS/email as permission to use, with the per-property shadow state still staff-controlled), languages. Rule of thumb for the reviewer: a capability off makes Clara do less; it can never make her send more.
(e) What never belongs on a settings page
- Facts about the building — hours, amenities, fees, utilities, neighbourhood, phone the public dials (detail page, with provenance).
- Per-record fields — a tenant's language, do-not-contact, a vendor's quiet hours (
VendorMembership.quietHours), a unit's availability. - One-off actions — import, re-sync, delete, backfill, "send now".
- Arms and kill switches (Admin only).
- UI state —
setupGuideState,askClaraState, dashboard layout (per-user prefs, stored on the user, no page). - Anything with no reader: the "manual" leasing source is offered disabled for exactly this reason (
leasing-settings/route.ts); the rule generalises — a setting ships only with its consumer (hot rule 13).
What is mis-scoped today — the inventory's observations
From the inventory sweep of the four settings pages, the property detail page, every module drawer, the arms registry, env-only knobs and the data model. File and line references are in the sweep and were read at ad990d3cb5.
Added 2026-09-08 — a personal preference stored per building. “Reminders for you” (whether to be reminded about pending approvals, how many, how often) is pmActionReminders on the property’s leasing settings. It is the one thing on the settings surface that is about a person but keyed to a building: twelve buildings means twelve copies of one manager’s preference, and no Account-level default for them to inherit from.
- Company Name is an org fact stored per user (
User.companyName,OrganizationSettingsShell.tsx:172;OrganizationSettings.tsx:15-18).Organization.settingsandOrganization.namehave no writer insrc/app. - "Hide from sync" is an org-wide destructive action keyed on a per-user denylist (
User.ignoredPropertyIds,ignored-properties/route.ts:149) — another user still sees the property until sync re-imports it; deletion is real. - Admin Settings toggles are one global row for every org (
CONFIG/SETTINGS;api/settings/route.ts:26-33), was writable by anyrequireUsercaller (:183) — closed by #7365, staff-only on write; the page calls them "user-level" (admin/settings/page.tsx:15).OrganizationSettingstype duplicates the same fields (types.ts:12484-12496) and is dead. AppSettings.smsEnabledis not read by the SMS transport — the real per-property gate isProperty.smsShadowMode(script-only) (types.ts:3082-3083). GlobalemailShadowModedefaults to true (dynamo/settings.ts:30) and per-propertyProperty.emailShadowModeis PATCH-able but has no UI.- Follow-up cadences and contact limits are ORG-scoped but only editable from a property context (
PK=FOLLOWUP#<orgId>; drawer + Settings card requirepropertyId;PolicyDrawer.tsx:60-75explains the tension). Editing "Past-due balance" on Camellia rewrites it for every property in the org. - Late fee / grace days have two write paths — property page Pricing editor and the Collections policy drawer — both on
PropertyKnowledge.pricingDetails, with a documented clobber hazard (MoneyRulesSection.tsx:16-22). - Two "office phone" fields with different meanings on two pages:
Property.officePhone(transfer destination, Property Settings) vsPropertyKnowledge.phone(public line, property detail page); Property Settings seeds one from the other (PropertySettings.tsx:98). - Renewal autonomy switch lives in three places: RenewalPolicyEditor (
autonomousRenewalEnabled), the Arms tab (same flag), and the globalCONFIG/RENEWAL_ARMSrow (script only). The "Open renewal N days" setting in Property Settings does nothing unless the globalautoStartarm is on — not surfaced there. - ~35 per-property behaviour switches have no UI at all (§4b) — including the single most important one,
handoffMode, plustimezone,smsConsentMode, escalation owner, shadow modes, tour day/lead rules, PM notification mode, holiday closures, lease policy. Each has its ownscripts/set-*.tsor a hand DDB write. - Leasing-settings route accepts fields the page never shows (
tourMinLeadMinutes,tourDayPolicy,postTourFollowUpChannel, deprecatedrenewalOutreach*—leasing-settings/route.ts:75-93,226-235);turnover-settingsroute has no UI caller. - Global UI prefs stored globally:
setupGuideStateandaskClaraStateare on the sharedCONFIG/SETTINGSrow, so one user's collapse affects everyone (types.ts:13377-13383). - Per-property kill-switch overrides (
PropertyLeasingSettings.turnIntegrity*/tour*Enabled) exist but the Arms tab only writes the global half. - Env arms that need a deploy (leasing digest, vendor dispatch, tour chokepoint, preference capture, stated-name, tour window, voice callback, decider mode, NTV routing) are global across customers; the two-factor design means the per-property flag is the only per-customer control and it is Arms-tab/script only.
- Customer facts still in source/env: Twilio numbers with hardcoded fallbacks (
phone-lookup.ts), ElevenLabs agent ids, recipient allowlists (VENDOR_DISPATCH_RECIPIENT_ALLOWLIST,LEASING_REPORTING_DIGEST_REVIEW_RECIPIENT,BAKE_ALERT_EMAIL),TEST_PROPERTY_TRANSFER_TARGET. - Retired env names linger in tests only:
MAINTENANCE_AUTONOMOUS_SENDING,PM_ACTION_REMINDERS_ALLOWLIST,MAINTENANCE_HANDYMAN_QUIET_HOURS_DISABLED,RENEWAL_AUTO_START_ENABLED(8 non-test reads remain for the last one). - Handyman quiet hours are per vendor-membership, default ON, script-only (
set-handyman-quiet-hours.ts); tenant quiet hours are hardcoded TCPA onProperty.timezone, which itself has no UI and silently defaults to Chicago. - Turnover policy editor edits 2 of 7 fields on
PropertyKnowledge.turnoverPolicy(timing fields unreachable), while the Turnovers page drawer only carries follow-up cadence. - **Dashboard/user prefs sit under
/api/settings/* (dashboard-layout,dashboard-card-windows) next to the global admin route — same URL family, three different scopes (user / global / user).
Per-property behaviour switches with no UI
Each of these is a customer-relevant behaviour today changed only by a script or a hand write to DynamoDB. The applied table above says where each belongs.
| Field | What it does | Writer | Cite | |
|---|---|---|---|---|
| `Property.handoffMode: 'coworker'\ | 'autonomous'` | THE per-property "how Clara behaves when she needs a human" | scripts/set-handoff-mode.ts | types.ts:3206-3246 |
Property.escalationCoworkerModeEnabled | escalation coworker mode | set-escalation-coworker-mode.ts | :3204 | |
Property.escalatedTourBookingEnabled | book tours on escalated threads | set-escalated-tour-booking.ts | :3167 | |
Property.escalationOwnerEmail, escalationOwnerCcEmail | who owns decision requests | hand DDB write (no script named) | :3000-3016 | |
Property.escalationBakeApprovalRequired | temporary bake gate (hold team emails for Slack approval) | hand DDB write | :3018-3052 | |
Property.escalationRelayRephraseEnabled | relay staff answers in Clara's words | none found | :3722 | |
Property.emailShadowMode, smsShadowMode | process but don't send | PATCH route accepts emailShadowMode (no UI); set-property-sms-shadow-mode.ts | :3062, 3075 | |
Property.tourRequestSmsEnabled | text-back after un-bookable tour ask | none found | :3114 | |
Property.publicListingsSync{url,mode} | portfolio-line listings scrape | hand write | :3136 | |
| `Property.messagingDelivery: 'direct'\ | 'pms'` | which door messages leave by | set-messaging-delivery.ts (PATCH also accepts) | :3691 |
Property.pmsMessagingSync{enabled,…} | import AppFolio guest-card threads | set-pms-messaging-sync.ts | :3536 | |
Property.pmsAppfolioAccount{accountId,appfolioSubdomain} | which AF database | set-appfolio-account.ts | :3637 | |
Property.appfolioTrustedSenders[], operationalDataSenders[], inboundEmailAddresses[], twilioNumbers[] | routing/security lists | set-appfolio-trusted-senders.ts, set-operational-data-senders.ts, set-property-inbound-email-addresses.ts, set-property-twilio-numbers.ts | :3435, 3306, 2976, 2934 | |
Property.collectionsCorrespondents, demandParty | collections counterparties / who the demand is from | set-collections-correspondents.ts, set-demand-party.ts | :3343, 3375 | |
Property.timezone | TCPA quiet-hours zone (defaults America/Chicago) | none found | :3772 | |
Property.smsConsentMode, jurisdictionStateCode, capabilityStage{leasing,renewals,maintenance: off/shadow/live}, autoSyncEnabled, pmsSource, operatingMode, sendGridSenderIdentity, isTest | consent model; legal module; per-domain graduation; rent-roll auto-apply; PMS wiring; sender identity; test behaviour switch | mostly none (PATCH accepts capabilityStage; PMS inspector displays some) | :3794, 3799, 3440, 3445, 3451, 3470, 3067, 3059 | |
Property.autoDraftLeaseOnApprovalEnabled, newLeaseTemplateName, promiseLedgerCallerOutboundEnabled, transferMissedOutreachEnabled, topicScopedHoldUnmuteEnabled | lease auto-draft; AF template; promise-kept texts; missed-transfer paging; topic-scoped hold | set-auto-draft-flag.ts, none, set-promise-caller-outbound.ts, set-transfer-missed-outreach.ts, set-topic-scoped-hold-unmute.ts | :3867, 3891, 4013, 4063, 4115 | |
PropertyLeasingSettings.tourMinLeadMinutes, tourDayPolicy{allowSameDay,minLeadMinutes} per weekday, renewalContactEmail/Phone, renewalLapseDigestEnabled, ownerReportWindowDays, `pmNotificationMode: all\ | outcomes_only, leasingActivityChannel/Mention, postTourFollowUpChannel` | tour booking rules; PM escalation contacts; lapse digest; owner window; PM notification volume; Slack channel; follow-up channel | route accepts tourMinLead/tourDayPolicy (leasing-settings/route.ts:75-93) but no UI; set-property-lapse-digest.ts; set-pm-notification-mode.ts; set-leasing-activity-mention.ts | types.ts:13070-13079, 13105-13108, 13138, 13157, 13269, 13243-13250 |
PropertyKnowledge.holidayPolicy{observeFederalHolidays,openOn,extraClosures}, freeMonthAppliesTo, leasePolicy (ApplicationLeasePolicy: terms, deposit tiers, app fee) | tour holidays; special mechanics; lease terms | none / set-free-month-applies-to.ts / none (lease-terms/policy-store.ts:4-8) | types.ts:4418-4434 | |
PROP#/CONFIRMATION_REVIEW_RECIPIENT {name, phone}, PROP#/ONSITE_PRICING {propertyCode, baseUrl, enabled} | who reviews confirmations; On-Site scrape | none found | dynamo/settings.ts:212-227, 159-176; types.ts:2545, 4324 | |
emailIntegration.teamMonitoredInbox | mailbox the team also reads | set-team-monitored-inbox.ts | ||
| Per-role permission overrides | which roles may do which actions | Access Inspector Roles tab → /api/admin/permissions | PermissionOverride (types.ts:13957) |
Open questions for the founders — each with a recommendation
- Org level has no writer.
Organizationrows cannot be edited; Company Name writesUser.companyName; follow-up cadences are org-scoped but need apropertyIdto derive the org (cadences/route.ts), so the Org-level Follow-ups card is shaky when "All Properties" is picked (FollowUpsCardtakespropertyId: string | null). Recommend: Fede's "add a customer" back-office flow lands the writer first; until then no O→P inheritance can ship, so sequence it before the Autonomy card.
- Gera's org-level list is right for two of three. Tour duration and application link → O→P (C6). Transfer destination → Property-only; the "org" feeling comes from single-property customers, where the two levels collapse — handle with the "single-property org hides the property/org split" refinement ADR-0019 N2 already names, not by moving the field.
- Should customers ever write "Autonomous"? Constitution says no (IV.6). Recommend: customer-writable rungs stop at "Act with approval"; Autonomous is staff-set, customer-visible. Revisit once a property has run a clean month.
enabledModulesand the/admin/settingstoggles are platform-wide but read like customer settings. One org flipping them flips every org. Recommend: migrate toOrganization.settings(type already declares them) in the same PR that adds the org writer; delete the toggles from/admin/settings, leaving it as a pointer page to the Access Inspector.
- Rules are stored inside
PropertyKnowledge(renewalPolicy,turnoverPolicy,lateFee) — the knowledge route's sanitizer clobbers siblings on partial PATCH (MoneyRulesSection.tsx"CLOBBER NOTE"). Recommend: new rule fields go toPropertyLeasingSettings/PropertyMaintenanceSettings; move the three existing ones when their drawers ship, with the same single-home drift test used for lease policy (lease-policy-single-home.drift.test.ts).
- Wrong-level state already in the tables:
User.ignoredPropertyIdsgoverns org sync;setupGuideState/askClaraStatelive on the global config row;subscriptionand legacy company fields on the shared row (the 2026-09-07 stopgap inapi/settings/route.ts). Recommend: a one-time "level audit" ratchet test — every field onAppSettingsmust be declared platform-wide by a comment or fail — and move the three named fields.
- Time zone:
todayDenver()hardcodesAmerica/Denverfor every prompt (clara-personality.ts), while the user setting says "Times show on each property's clock". Not a settings-page question, but the first out-of-Colorado customer breaks on it. Recommend: property timezone becomes a required building fact on the detail page and the prompt helper reads it.
- Inheritance vs. hot rule 13. An org default that is absent must never leave a property dark. Recommend: the resolver's third rung is always a code default (never "off"), and the falsifier for every inherited knob is a test that an empty org row reproduces today's per-property behaviour byte-for-byte — the same "UNSET = today's behavior" contract every recent property field already documents.
Bottom-up: what is hard-coded to one customer today, and where it becomes a setting
Gera, 2026-09-08: "a lot of things are kind of hard-coded to Camellia or even JPCO; now that we're onboarding new organizations and properties, all that stuff needs to be at the right settings page, configurable and clear." This section works from the code upward. Sources: the customer-identifier fence's own ledger of known violations (customer-identifier-fence.drift.test.ts, categories B and C are the live coupling), a sweep for customer names, subdomains and time zones across src/lib, src/app/api, agents and lambda, and the inventory's env-only knobs. Prompt examples that merely mention a customer ("tour confirmed at Camellia Apartments") are hygiene, not settings, and are listed last.
| Hard-coded today | Where | What it really is | Becomes | Level · surface | Field exists? |
|---|---|---|---|---|---|
Phone → property routing table with two customers' property ids, plus TWILIO_NUMBER_* env fallbacks | src/lib/domain/properties/phone-lookup.ts | Which numbers belong to which building | The property's inbound numbers, read at runtime | Property · Integrations (staff-provisioned, read-only row) | Property.twilioNumbers exists, set nowhere |
| Test-property transfer target | env TEST_PROPERTY_TRANSFER_TARGET | Where an unknown caller on the bench property is sent | The bench property's own transfer destination | Property · Calls (already the card for real properties) | Property.officePhone — use it for the bench too |
Prompt clock: America/Denver in ~15 places (todayDenver, PROMPT_TIME_ZONE, FLEET_DISPLAY_TIME_ZONE, tour date resolution, email extractors, post-transfer); TCPA quiet hours default to America/Chicago | agents/clara/lib/agent/clara-personality.ts, context-message-filter.ts, tools-leasing.ts, email/extract-tour-*.ts, voice/post-transfer.ts, format.ts; Property.timezone default | A fact about the building | One required property time zone, set at onboarding; every prompt helper reads it (no fleet default) | Property · detail page (building fact), with an Organization default for single-market operators | Property.timezone exists, no UI, silently defaults |
Listings sync pinned to one property id and one AppFolio subdomain (LISTINGS_CONFIG) | src/lib/domain/leasing/listings-sync.ts | Where a building's public listings live | Per-property listings source URL + mode | Property · Integrations | Property.publicListingsSync exists (hand-written) |
| Properties that ingest rent rolls via the Data API — a hard-coded id list | agents/clara/lib/email/process-inbound-email.ts | Which store is truthful for this building's rent roll | Per-property rent-roll source | Property · Leasing (next to Pricing & availability source) | Property.rentRollSource accepted by PATCH, no UI |
AppFolio base URL jpco.appfolio.com in the browser agent | lambda/agent-runtime/vendors/appfolio/apiClient.ts | Which AppFolio database this building lives in (two databases since 2026-09-01) | Account subdomain threaded from the property's PMS binding | Organization · Integrations (credentials + database) and Property · Integrations (which account, read-only) | Property.pmsAppfolioAccount exists (script-set); Org AppFolio card exists |
| The management company's street address inside a shared voice prompt | agents/clara/lib/voice-agents/vendor-outbound.ts | A company fact Clara reads out to vendors | A prompt variable fed from the organization record | Organization · Company card (name, address, phone) | No — Organization has no writer at all today |
| Company name Clara signs as | User.companyName via PATCH /api/auth/me | A company fact | Organization.name | Organization · Company card | Wrong level today (per user) |
Vendor mail domains / vendor names to skip when loading costs (SKIP_DOMAINS, VENDOR_NAMES) | src/lib/domain/maintenance/load-cost-data-core.ts | One operator's vendor roster | The organization's vendor directory and operational senders | Organization · Vendors page (exists) + Property · Integrations "senders to read, not answer" | Organization.preferredVendors exists; Property.operationalDataSenders exists (script-set) |
| Per-property scraper modules carrying the property id and public web domain | src/lib/platform/scrapers/camellia.ts, yale25.ts | Where the building's website is and how to read it | Website URL on the property record; scraper config as data | Property · detail page (Website URL exists) + Admin for the scraper recipe | PropertyKnowledge.websiteUrl exists |
Known property names and aliases for email classification (KNOWN_PROPERTIES) | agents/clara/lib/email/classify.ts | What people call the building | Aliases on the property record, derived list at runtime | Property · detail page ("Also known as") | No field — new |
A prod property id named in order to REFUSE it (FORBIDDEN_PROPERTY_ID) | src/app/api/canary/suppression-probe/route.ts | "Is this a real customer?" | The property's test flag | Admin · Access Inspector (per property) | Property.isTest exists |
Recipient lists and staff addresses in env: VENDOR_DISPATCH_RECIPIENT_ALLOWLIST, LEASING_REPORTING_DIGEST_REVIEW_RECIPIENT, BAKE_ALERT_EMAIL, ADMIN_EMAILS | deploy env | Who gets which email at which company | Notification recipients on the property / organization; staff lists stay Admin | Property · Notifications (owner report, lease signed, escalation owner) · Admin for PropFlow-internal alerts | Partly: escalationOwnerEmail, ownerReportRecipients, renewalExecutionCcEmails exist |
| ElevenLabs agent ids as constants; one agent set for every customer | agents/clara/lib/voice-agents/*.ts, *.config.json | Which voice agents answer for which account | Per-organization agent mapping in data, prompts still code | Admin · Access Inspector (staff-provisioned per org) | No — new |
| Escalation owner / CC, collections correspondents, demand party, trusted senders, inbound addresses — all script-written | scripts/set-*.ts → Property fields | Company and building identity facts Clara relies on | The same fields, with a home on the settings pages | Property · Notifications (owner/CC) · Property · Integrations (senders, correspondents, inbound addresses) | Fields exist; no UI |
| Customer names inside prompt examples and tool descriptions ("tour confirmed at Camellia Apartments", "e.g. Yale 25") | clara-leasing.ts, tools-leasing.ts, tools-comms.ts, spam-fewshot.ts | Worked examples, not routing | Placeholders ({{property_name}}, "Example Apartments") | Code hygiene, not a setting | — |
Onboarding-ready checklist: what a new organization and a new property must have before Clara runs
The same list read the other way: the fields a customer (or staff, at onboarding) sets, at which level, and whether the product can take the value today. "No UI" means the field exists and only a script writes it; "hard-coded" means the code does not read a field at all. This is the working spec for the settings pages: every row is a field or a card.
Organization — set once per customer
| Field | Why Clara needs it | Card | Today |
|---|---|---|---|
| Company name, address, main phone | Signature on every email; what vendors are told; the voice prompt's "who we are" | Company | Name per-user; address hard-coded in a prompt; phone absent |
| First company admin (invite), then the team with roles | Who may manage the company's settings and people | Team | Invite exists; admin-level management shipping in the team-management PR |
| PMS: AppFolio database subdomain + API credentials | Every sync and every write-back | Integrations · AppFolio | Exists |
| Plan and billing | Unit count, invoices | Billing | Exists |
| Capabilities: modules on, channels permitted (SMS, email), languages | Scopes what Clara may do for this company — never arms a send | Capabilities (new) | Platform-wide global row today, admin-only |
| Company defaults that properties inherit: tour duration, application link, PM reminder cadence, auto-approve ceiling, follow-up cadences, contact ceiling | Most operators run one policy; buildings override | Defaults (new, once the resolver exists) | Property-only today; cadences org-scoped but edited from a property |
| Preferred vendors by trade | Dispatch and quotes | Vendors page (exists) | Exists — the one working inheritance chain |
| Which properties to sync / hide | Portfolio scope | Property sync | Keyed on a per-user list today; must move to the organization |
Property — set once per building
| Field | Why Clara needs it | Card | Today |
|---|---|---|---|
| Name, address, aliases, time zone | How Clara names the building; every date and every quiet-hours window | Property detail (building facts) | Name/address exist; aliases hard-coded; time zone no UI and defaulted |
| PMS account binding (which AppFolio database), rent-roll source, pricing & availability source | Which store is truthful | Integrations / Leasing | Binding script-set; rent-roll source no UI; pricing source exists |
| Inbound numbers (Twilio), property inbox and inbound addresses, sender identity | Routing every call, text and email to this building | Integrations (staff-provisioned, read-only) | Numbers hard-coded; inbox exists; addresses script-set; sender identity script-set |
| Office phone (transfer destination), emergency phone | "Get me a human"; after-hours emergencies | Calls | Exists |
| Office hours, holiday closures, website URL | Tour availability, "are you open", what to scrape | Property detail | Hours and website exist; holidays no UI |
| Outlook calendar and inbox connection | Booking tours; reading the property inbox | Integrations | Exists |
| Tour duration, minimum lead time, per-weekday policy, application link, post-tour follow-up | What Clara offers and sends | Leasing & tours | Duration, link, delay exist; lead time and weekday policy no UI |
| Escalation owner + CC, maintenance tech phone, owner-report recipients, lease-signed CC, PM reminders | Who is told what | Notifications | Tech phone, recipients, CCs, reminders exist; escalation owner script-set |
| Auto-approve ceiling, handymen, preferred-vendor overrides, turnover policy | Maintenance decisions and dispatch | Autonomy (ceiling) · Vendors page · Turnovers drawer | Ceiling and handymen exist; turnover policy 2 of 7 fields editable |
| Renewal window and offer rules; late fee and grace days; collections correspondents | Renewals and collections behaviour | Leasing (window) · Renewals drawer · Collections drawer · Integrations (correspondents) | Window exists; rules live in the knowledge blob; correspondents script-set |
| Per-module autonomy rung (Off / Suggest / Act with approval / Autonomous) and shadow state; test flag | Whether Clara acts or asks, and whether this is a real customer | Autonomy card (customer-visible; the top rung staff-set) · Admin for the test flag | capabilityStage, handoffMode, shadow modes, isTest all exist — none has a UI |
Admin — PropFlow staff only
| Field | Why | Today |
|---|---|---|
| Voice agent mapping per organization; Twilio number provisioning; scraper recipes | Infrastructure a customer never touches | Agent ids are constants; numbers in env; scrapers are modules |
| Arms, kill switches, staged cutovers, eval and trace toggles | Soak-gated behaviour | Access Inspector (exists) |
| Staff alert recipients | PropFlow's own ops | env |
Sequencing — what has to land first
- An Organization writer. Nothing at the org level can be edited today: Company Name writes
User.companyName, follow-up cadences are org-scoped but need apropertyIdto find the org. Fede's "add a customer" back-office flow is that writer; until it exists no org→property inheritance can ship, so it goes before the Autonomy card. Team management (add/remove people with roles from Organization Settings) is the first customer-facing org-level write and can land alongside it. - The settings resolver —
resolvePropertySetting(propertyId, key)returning value + source, replacing the "property → hardcoded default" resolver; its contract test is that an empty org row reproduces today's per-property behaviour byte for byte. - The Autonomy card on Property Settings with the read-only module mirrors, mapping the ladder onto
capabilityStage/handoffMode/ the send arms, and folding today's scattered booleans into rows. - Move the wrong-level state: the
/admin/settingstoggles to the arms tab;enabledModulestoOrganization.settings; setup-guide and Ask Clara state to the user; hidden-from-sync from the user denylist to the org. A ratchet test that every field on the global config row is declared platform-wide by a comment, or fails. - Give the 35 no-UI switches a home per the applied table — most become rows of the Autonomy card or the Notifications card; identity lists (trusted senders, correspondents, Twilio numbers) become read-only Integrations rows staff provision.
Method: two read-only research passes over the repository (an inventory pass and a placement pass), synthesised here. Product comparisons drawn on: Vercel account/team/project, Linear workspace/team/personal, Intercom and Front (inbox rules live in the inbox), Stripe, AppFolio and Buildium (company policy templates with property-record overrides). Related: the four-level split (#7324), one shell per level (#7326).