Every switch that can turn behavior on or off · audited 2026-07-25 against propflowai@main (36cddd202)
An "arm" is anything that gates whether PropFlow actually does something. They are not one mechanism — there are 7 distinct families with 3 mutually incompatible on/off idioms. This is an audit of the code surface: which arms exist and how they compose. It does not report which arms are currently armed in prod.
'armed' · kill switches test
!== false (default ON) · the tour chokepoint tests === 'off' (inverted).
The main family. All built by one factory — createDomainGate in
src/lib/temporal/autonomous-gate-core.ts:90. A send fires only if the global is armed
AND the per-property flag is exactly boolean true. Anything else — unset, empty,
"true", 1, a typo — means disarmed. The global is checked first (no I/O), so a disarmed
worker does zero per-property reads.
| Domain | Global env arm | Durable field | Per-property flag | Shape |
|---|---|---|---|---|
| Renewal tenant sends | RENEWAL_AUTONOMOUS_SENDING |
autonomousSending |
autonomousRenewalEnabled |
2-factor |
| Holdover → month-to-month billing mutation |
RENEWAL_HOLDOVER_CONVERSION |
holdoverConversion |
autonomousHoldoverEnabled |
2-factor |
| Turnover sends | TURNOVER_AUTONOMOUS_SENDING |
turnoverSending |
autonomousTurnoverEnabled |
2-factor |
| Listing auto-publish on NTV | TURNOVER_AUTONOMOUS_SENDING |
turnoverSending |
autonomousListingPublishEnabled |
coupled |
| Stale-lead / leasing digest | LEASING_DIGEST_AUTONOMOUS_SENDING |
— | autonomousLeasingDigestEnabled |
2-factor |
| Vendor dispatch | VENDOR_DISPATCH_AUTONOMOUS_SENDING |
— | autonomousVendorDispatchEnabled |
2-factor |
| Prospect outreach cadence | PROSPECT_OUTREACH_AUTONOMOUS_SENDINGvestigial — arms nothing |
— | autonomousProspectOutreachEnabled |
per-prop only |
| Scheduling re-engagement | none | — | autonomousProspectOutreachEnabled |
coupled |
Sources: autonomous-gate.ts · holdover-gate.ts · turnover-gate.ts ·
listing-publish-gate.ts · vendor-dispatch-gate.ts ·
domain/leasing/stale-lead-digest/digest-gate.ts · domain/leasing/prospect-outreach-gate.ts ·
domain/leasing/scheduling-engaged-gate.ts
CONFIG / RENEWAL_ARMSSurvives worker deploys and beats the env var when written. Falls back to env
per field, not per row — so a row written before a field existed defers to env rather than silently
disarming that domain. Written by scripts/set-renewal-arms.ts; read from a synchronous in-memory cache
so no gate call site can fail open on a missed await.
| Field | Gates | Env fallback |
|---|---|---|
autoStart | Daily renewal auto-start scanner | none — retired 2026-07-07 |
autonomousSending | Renewal tenant sends | RENEWAL_AUTONOMOUS_SENDING |
holdoverConversion | Holdover → MTM billing mutation | RENEWAL_HOLDOVER_CONVERSION |
turnoverSending (optional) | Turnover sends + listing auto-publish | TURNOVER_AUTONOMOUS_SENDING |
Type: src/lib/data/types.ts:6808
| Arm | What it gates | Blast radius |
|---|---|---|
EMAIL_NTV_FIRM_ROUTING_ENABLED |
Firm NTV routing of tenant formal-notice emails and the AppFolio move-out draft write — one switch, both behaviors | fleet-wide |
src/lib/domain/leasing/ntv-gate.ts:25 — fires real move-out machinery (renewal decline + turnover + inspection + PMS draft) off an LLM read of free text, so it is deliberately never default-on.
These are not arms you turn on; they are escape hatches you turn off during an
incident with no redeploy. Read at runtime, cached ≤60s, fail-safe ON (a read error keeps the
behavior running). Each exists at two levels — global AppSettings plus a per-property
PropertyLeasingSettings override — so 7 flags = 14 independent flip points. Only an
explicit false disables; a global false reverts every property.
| Flag (both levels) | Disables | Default |
|---|---|---|
tourPipelineV2Enabled | Span-verified tour-date pipeline → reverts to legacy date resolution | ON |
turnIntegrityTypedToolResults | Curated typed tool errors → raw thrown errors reach the model | ON |
turnIntegritySingleWriter | Ingestion-side send suppression → agent no longer sole writer | ON |
turnIntegrityReviewGate | Anomaly Review Gate | ON |
turnIntegrityOutputGate | Output gate verdict logs (observe-only) | ON |
tourChannelMatchEnabled | Channel-matched notifications → reverts to always-SMS | ON |
tourTemplateDeferralEnabled | One-message-per-trigger deferral → template sends immediately | ON |
Global: types.ts:6745–6770 · per-property: types.ts:6646–6663 ·
readers: turn-integrity-flags.ts, tour-send-flags.ts, tour-pipeline-flag.ts
Eight boolean fields on Property. Seven are the autonomous* family;
the eighth uses a different shape (fail-closed per-property flag plus an inverted global kill switch
TOUR_SCHEDULE_CHOKEPOINT === 'off').
| Flag | Line | Operator UI? |
|---|---|---|
autonomousRenewalEnabled | 494 | read-only label |
autonomousTurnoverEnabled | 517 | none |
autonomousHoldoverEnabled | 528 | none |
autonomousListingPublishEnabled | 541 | none |
autonomousLeasingDigestEnabled | 551 | none |
autonomousProspectOutreachEnabled | 559 | none |
autonomousVendorDispatchEnabled | 565 | none |
tourScheduleChokepointEnabled | — | none |
Only autonomousRenewalEnabled surfaces anywhere in the app — as a colored status label at
PropertyDetailClient.tsx:1245, not a toggle. Every other arm is flipped by DDB write or script.
Written only through the requireAdmin-gated PUT /api/admin/modules
(the legacy POST /api/settings hole was closed). Operator UI: Modules tab of the Access Inspector.
dashboard · leasing.prospects · leasing.renewals ·
maintenance.workorders · maintenance.turnovers · maintenance.routine ·
maintenance.costs — MODULE_FLAGS, types.ts:6858
For several walkers this is now the only arm — their env flags were deliberately retired, so pausing/unpausing the schedule is the entire control surface.
renewal-auto-start-daily · renewal-cohort-walker-daily · tour-cohort-walker ·
turnover-cohort-walker · scheduling-engaged-walker · onsite-pricing-daily ·
voice-injection-cache-refresh-daily · conversation-review-daily ·
maintenance-eval-daily · pms-sync-eval-daily · pm-action-reminder-backstop
Not a boolean at all — the absence of a config row is the disarmed state.
A property with no OnsitePricingConfig row is never scraped. Arming = write the row + unpause the schedule.
TEMPORAL_SHADOW_ENABLED (default off, emergency mirror kill-switch) ·
CLARA_TRACE_CAPTURE_PROMPT (default ON, only 'false' disables) ·
TEMPORAL_NONDETERMINISM_METRICS_ALERT (default off) ·
TOUR_SCHEDULE_CHOKEPOINT (inverted — 'off' kills)
src/lib/domain/automation/followup-registry.ts registers every outbound follow-up path
in the product, each honestly self-labeled by how much chasing it really does. The Follow-ups settings card renders
all 15 rows — but only the configurable one is a live lever. Whether any of it sends is
separately gated by autonomousProspectOutreachEnabled.
The single configurable scenario is prospect.inquiry.no_response. Each
graduation from fixed → configurable is its own PR — the consumer must actually read the
config, never just display it.
turnoverSending durable field.
Arming turnover silently arms listing publish on any property with autonomousListingPublishEnabled.
Same shape: scheduling re-engagement shares prospect outreach's per-property flag. Neither coupling is visible
at the point of arming.autonomousRenewalEnabled appears in the app, and only as a read-only label. Arming a property is
an engineering task — a DDB write or a script run — not something an operator can do. This is the practical
bottleneck on onboarding a property to any autonomous behavior.PROSPECT_OUTREACH_AUTONOMOUS_SENDING is defined and passed to the gate factory, but the deploy pipeline
always renders it disarmed and the domain gates per-property only. It reads like a live safety control
and is not one.=== 'armed' (fail-closed) vs !== false (default-ON kill switch) vs === 'off'
(inverted). Nothing in a gate's name signals which family it belongs to, so a reader must open the file to know
whether unset means on or off.autonomousMaintenanceEnabled survives only inside
maintenance-on-temporal.drift.test.ts as a guard that fails CI if anyone reintroduces it. That is the
pattern the other arms should retire into once their domain is trusted.Audited from source, not from documentation — CLAUDE.md's arm table was found to undercount
(it omits the vendor-dispatch, leasing-digest, listing-publish and tour-chokepoint arms). Verified against
propflowai@main commit 36cddd202 on 2026-07-25. Scope: this is the code
surface — which arms exist and how they compose. Live prod values of env vars and the
CONFIG/RENEWAL_ARMS row were not read.