AppFolio test-tenant state map

Every state a test tenant on the Willows can be in, the AppFolio tool that moves it along each edge, and the read that tells you which state you are in and why. For whoever is driving the bench.

2026-09-05 · property 45 (the Willows) only · drafted from the live tool catalog and the browser-agent routes · cancel-notice edge shipped and live-proven the same day (occupancy 986: Past → Current)

The rule Gera asked for: a clean way to go to and from any state, so when a test tenant is "down" we can see why and drive it to the state we need — without opening AppFolio by hand.

Why this exists

Fede's own AppFolio profile (tenant 1832, occupancy 986, unit TEST-102) had been given notice on 2026-06-11 and a move-out date of 2026-06-25, then left there: status Past, move-out accounting never finalised, lease expired 2026-07-14, phone empty. The bench could not borrow it and nothing on our side said why. We drive that exact tenant-status form in one direction (file a notice) and had no tool for the other (cancel it). The same shape repeats for every state below.

The states, and the edge out of each

AppFolio's own words are in small caps. Each edge names the tool that drives it today, or the gap.

Vacant unit

unit · no occupancy
  • → Current appfolio.move_in_tenant (offline-signed lease, wizard end to end)
  • → Future run-new-lease-send (e-sign new lease; mock-proven, not live-proven)
  • create the unit first: appfolio.create_unit, appfolio.post_vacancy

Future tenant

move-in date ahead · lease out for signature
  • → Current now appfolio.edit_tenant_status (backdate the move-in to today)
  • read: lease-document-status

Current

status Current · no notice
  • → On notice appfolio.file_notice_to_vacate
  • lease expired → month-to-month: set-lease-mtm route only
  • renewal: send_renewal, change_term_renewal, cancel-renewal, list_renewals_ready_to_countersign
  • identity: tenant_phone_set, tenant_email_set, run-tenant-name-set route only

On notice

notice date + move-out date set · still Current
  • → Current appfolio.cancel_notice_to_vacate live
  • → Past happens by itself when the move-out date passes

Past, not finalised

move-out date passed · pendingMoveOut true · isMoveOut false
  • → Current the same cancel-notice edge — proven on occupancy 986 (2026-09-05). AppFolio refuses it when the unit already has a Current occupancy; allowOverlappingOccupancy posts AppFolio's own skip switch, which the Willows units need because they carry harness occupancies.
  • → Past, finalised move-out-tenants + move-out-charges routes only

Past, finalised

move-out accounting posted · isMoveOut true
  • → Current not reversible in AppFolio; the edge is a new move-in on the unit (appfolio.move_in_tenant)
  • read: read-move-out-charges

Reading the state, and why it is "down"

appfolio.get_tenant_details already answers the AppFolio half in one call. The fields that decide the state, in the order to read them:

fieldwhat it decides
occupancy.status / tenant.statusCurrent · Past · Future. The headline.
tenant.noticeDate, occupancy.moveOutBoth set = on notice, or Past once the date passes. Both empty = clean Current.
tenant.pendingMoveOut, occupancy.isMoveOutPast-not-finalised (true/false) is reversible; Past-finalised (·/true) is not.
occupancy.leaseExpirationDateIn the past on a Current tenant = month-to-month territory; the renewal edges apply.
tenant.phoneEmpty = nothing for the bench to bind. The borrow edge rewrites it; a released unit gets a synthetic number.
occupancy.currentBalanceDelinquent persona or not. The bench shows it per unit.

The PropFlow half is three more reads, and each has failed on us at least once this week:

Gaps, in priority order

Numbered 0 and 0b are the two the live runs surfaced after this page was first written; they outrank the rest.

#gapwhat closes itsize
1No reverse of a notice / an unfinalised move-outcancel-notice-to-vacate L4 route (browser-agent #317, #318) + appfolio.cancel_notice_to_vacate catalog tool (propflowai #7110), property-45 hard scope, dry-run first. shipped 2026-09-05 First live run: dry-run exact, live refused by AppFolio's overlap check (TEST-102 also holds fixture 1009), re-run with the overlap switch → Current, readback confirmed.done
2Nothing says why a unit is not borrowableA bench "diagnose" read per occupancy: the six AppFolio fields above + synced row + claim holder + sentinel check, rendered as one sentence ("Past since 06-25, not finalised → cancel the notice").half a day
3Five routes have no catalog tool (month-to-month, finalise move-out ×2, tenant name, unit fields)Catalog entries following the adding-a-new-tool runbook; each gets a fixture and the property-45 guard.half a day
4Property scope is per route, not per runtimeOne shared scope guard in the browser-agent that every mutating route re-asserts before the write; today each route carries its own.half a day
0The sync treats "former" as terminal. A tenant who moves out is archived; a tenant whose notice is then cancelled in AppFolio (occupancy back to Current) stays former in PropFlow forever — the rent-roll applier reconciles onto the old row but never revives it. Seen live on occupancy 985 (2026-09-06): AppFolio Current, PropFlow former on every 5-minute tick. This is customer-facing, not bench-only: a real resident who withdraws notice becomes invisible to Clara.Revive in the applier when a rent-roll row resolves by occupancy ref to a former row and describes a live tenancy (the reverse of the archive path, same writer). shipped #7174 Proven on production 2026-09-06 16:47Z: occupancy 985 flipped former → active on the first tick after deploy, same row, no duplicate.done
0bTwo Current occupancies on one unit are invisible to the sync. The rent roll is one row per unit, so after the overlap bypass on TEST-102 the sync reports 1009 and never sees 986. AppFolio also refuses any tenant-status save on either sibling while both are Current.Keep test units to one Current occupancy: end the sibling first (the notice route has the same overlap switch — shipped; 1009 was ended with it and 986 synced within a minute). The overlap bypass is for getting out of a wedge, not a state to stay in.shipped + rule
5Occupancy 986 is not in the bench's borrowable listAdded in #7110. shipped Becomes borrowable once the 5-minute rent-roll sync carries the reactivated row.done

The bench panel this becomes

One card per borrowable occupancy: the state pill (Current · On notice · Past · Past-finalised), the one-sentence "why", and only the edges that are legal from that state as buttons. Every button is an L4 route that refuses any property but 45. That is the "masters of AppFolio" surface: no AppFolio tab, no guessing which form to edit.

PropFlow Docs