PropFlow Arms — full toggle inventory

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.

~55
flip points total
8
two-factor send domains
8
per-property arms
14
default-ON kill switches
11
Temporal schedules
3
conflicting idioms
Three idioms, no shared polarity. Reading one gate tells you nothing about the next. Fail-closed send arms require the exact literal 'armed' · kill switches test !== false (default ON) · the tour chokepoint tests === 'off' (inverted).

1 · Two-factor fail-closed send arms

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.

DomainGlobal env armDurable fieldPer-property flagShape
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 ⚠ shared turnoverSending ⚠ shared 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_SENDING
vestigial — arms nothing
autonomousProspectOutreachEnabled per-prop only
Scheduling re-engagement none autonomousProspectOutreachEnabled ⚠ shared 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

2 · Durable arm row — CONFIG / RENEWAL_ARMS

Survives 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.

FieldGatesEnv fallback
autoStartDaily renewal auto-start scannernone — retired 2026-07-07
autonomousSendingRenewal tenant sendsRENEWAL_AUTONOMOUS_SENDING
holdoverConversionHoldover → MTM billing mutationRENEWAL_HOLDOVER_CONVERSION
turnoverSending (optional)Turnover sends + listing auto-publishTURNOVER_AUTONOMOUS_SENDING

Type: src/lib/data/types.ts:6808

3 · Global-only arm — no per-property factor

ArmWhat it gatesBlast 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.

4 · Default-ON kill switches — opposite polarity

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)DisablesDefault
tourPipelineV2EnabledSpan-verified tour-date pipeline → reverts to legacy date resolutionON
turnIntegrityTypedToolResultsCurated typed tool errors → raw thrown errors reach the modelON
turnIntegritySingleWriterIngestion-side send suppression → agent no longer sole writerON
turnIntegrityReviewGateAnomaly Review GateON
turnIntegrityOutputGateOutput gate verdict logs (observe-only)ON
tourChannelMatchEnabledChannel-matched notifications → reverts to always-SMSON
tourTemplateDeferralEnabledOne-message-per-trigger deferral → template sends immediatelyON

Global: types.ts:6745–6770 · per-property: types.ts:6646–6663 · readers: turn-integrity-flags.ts, tour-send-flags.ts, tour-pipeline-flag.ts

5 · Per-property arms — the full set

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').

FlagLineOperator UI?
autonomousRenewalEnabled494read-only label
autonomousTurnoverEnabled517none
autonomousHoldoverEnabled528none
autonomousListingPublishEnabled541none
autonomousLeasingDigestEnabled551none
autonomousProspectOutreachEnabled559none
autonomousVendorDispatchEnabled565none
tourScheduleChokepointEnablednone

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.

6 · Module flags · schedules · capability rows

Module flags — 7

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

Temporal schedules — 11 · paused state IS the arm

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

Capability-by-row-presence

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.

Misc runtime switches

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)

7 · Follow-ups — 15 scenarios, 1 actually wired

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.

configurable — editable, consumer reads the config1
fixed — real logic, schedule hard-coded, shown read-only7
one_shot — sends once, nothing ever chases4
planned — no outbound exists yet at all3

The single configurable scenario is prospect.inquiry.no_response. Each graduation from fixedconfigurable is its own PR — the consumer must actually read the config, never just display it.

8 · Findings

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.

PropFlow Docs