The document engine: Clara prepares the paperwork
Decided by Fede · Oct 21, 2026, 7:26 AM · building · new leases decided (14); four small questions open
0 · Morning report, Oct 23: the overnight, in one screen
Short version: everything Western Slope's December flight needs is built, merged dark, and proven at the Willows, including the two gaps the Oct 22 run found. The new-lease version of the engine is proven end to end for Camellia from a real approval, with the office told. The top rung, Clara sends, is merged and live; its first real send at the Willows fires at 8:01am Denver because the deploy landed after the send window. The one-screen version with diagrams: the renewals engine one-pager.
Lane Before (Oct 22 evening) After (Oct 23 morning) Proof
Western Slope shape Recipe v3 merged, not run Parked offer 376 at the Willows with the original start filled from our lease record; "respond by" day saved at start (replay on 130 real WS leases: 130 held → 0); pre-filled parts read from the page, not assumed WS shape at the Willows
Camellia new leases Engine merged, no caller, never run A real approval at the Willows starts the run on its own; lease parked unsent; the office emailed 5 s later; one run per application; replay after cancel answers "nothing parked" New-lease park at the Willows
Top rung: Clara sends Send verb built, never called Merged and live, behind the same emergency stop and Renewals switch as the letter; tenant texts point at the portal once sent; test tenant moved to a mailbox we read Proven at the Willows 2026-10-23: sent, tenant signed in the portal, signal seen, page shows the countersign pill; cleaned up
Faded mass-text bubble 60% opacity Removed #21776
Must close before any turn-on: both closed (Oct 23). Co-applicants on one unit now get one parked lease, not one each (#22035, merged as #22052; Camellia replay, and live at the Willows: two approvals on one unit, one parked, one held). A renewal started under 30 days before lease end no longer gets a "respond by" day inside the new term (#21987, merged as #22051; Western Slope replay). Open follow-up: the co-applicant choice depends on the order the approvals arrive (#22053).
Open for Fede, one line each: rotate the production Temporal API key (a builder printed it into a local transcript; recommend rotate). Start the supervised week at Western Slope (your call; recommended before December).
0b · Morning report, Oct 22: what shipped overnight, what we learned
Short version: the renewal paperwork chain is merged and dark everywhere. At the "Clara prepares, team sends" rung she builds the packet from the company's own recipe, the term always ends at month end, and the draft is parked in AppFolio for the team to press Send. Nothing is on at any customer. The learner read three customers' past paperwork read-only, with no manual work by Fede. The Willows live run is in flight; its proof lands below.
Before → after
| Before | After (merged, dark) | |
|---|---|---|
| Who builds the addendum | The team, by hand in AppFolio, for every renewal. | Clara, from the company's recipe, the moment the renewal decision is made. |
| Term end | Copied from the old list, exact day; some lists carried mid-month ends. | Always the last day of the month, everywhere, JP&Co included. A fix, no switch. |
| Who sends | The team. | Still the team at the "prepare" rung: the draft waits in AppFolio until a person presses Send. |
| Safety | None written down. | Read before write; an existing offer wins; the page is read back; a mismatch holds the packet and shows the reason on the renewal page. |
| Live at a customer | No. | No. Default off everywhere; turning on is Fede's call per company. |
Shipped overnight
| Repo | PR | What it does, in one line | State |
|---|---|---|---|
| app | #21605 | The door: the engine's contract with the robot (read, cancel, park, send-parked; five answers). | merged |
| app | #21640 | New-lease paperwork sender setting, default off. | merged |
| app | #21641, #21646 | Two small renewals fixes found on the way. | merged |
| app | #21652, #21653 | The engine: versioned per-company recipes, the print order, the ladder. | merged |
| app | #21655, #21693, #21696 | Term end is always month end: in the engine, everywhere else in the app, and in the renewal rent math. | merged |
| app | #21664 | The "Clara prepares, team sends" rung on the existing sender setting. | merged |
| app | #21672 | The first caller: at that rung the renewal workflow builds, reads, parks and records the packet on the lease. | merged |
| app | #21702 | The three reads Western Slope's one-pager needs, behind the PMS adapter. | merged |
| robot | #601, #602 | New-lease read; month-end lease end on the robot side. | merged |
| robot | #606 | Park and cancel for new leases, verified live at the Willows (draft lease parked, then cleanly cancelled). | merged |
| robot | #608, #609 | The two blocking review findings on #606: refuse a zero deposit before anything is written; one sender per AppFolio write. | open, armed |
| both | issues #21654, #21673, robot #607 | Review suggestions filed as follow-ups (page wording, held-versus-failed colour, retry a failed prepare, robot nits). | follow-up |
Willows proof
Done, 2026-10-22 06:00 UTC. The renewal workflow built the packet, the robot read the lease, parked the offer unsent in AppFolio, and every typed value and term read back as ordered. Seven seconds from start to parked. Then it was cancelled cleanly and the renewal state was reset; the lease keeps the packet record by design.
| Step | Before | After | Proof |
|---|---|---|---|
| Start | Unit 101 shows "Prepare Renewal Offer", no offer | Renewal saga_111ca06b… started, rung prepare frozen on the run | outreach route HTTP 200; Temporal workflow renewal-lease_e53751a8…-2027-09-30 |
| Build | no order | Order e0c97e09… from recipe renew-simple-willows-test@v1 | worker log "built from renew-simple-willows-test@v1; read then park" 06:00:38 |
| Read | — | "the lease shows no offer" | robot answer done, no writes |
| Park | no offer | Offer 375, document 899, "Renewal Status: Not Sent"; 12-month term 10/01/2027 to 09/30/2028 at $1,550 | robot answer done; screenshots below |
| Record | lease.renewalPacket null | state parked, plain-English reason stored on the lease | worker log "packet parked recorded on lease" 06:00:43 |
| Cleanup | parked offer 375 | "cancelled offer 375; the lease shows no offer"; workflow stopped; tenant renewal state reset; mint claim released; recipe taken out of use; rung back at team | robot cancel answer; reset route resetCount 1 |


What it took to start: no prod staff login could post the outreach route (the smoke account is view-only by ruling; the e2e test account was waitlisted with no second factor). The e2e test account was set active for the run and set back to waitlisted after. It also got an authenticator factor, which stays (secret in the Mac Keychain under propflow-mfa-e2e-totp). Both are on a throwaway test identity, separate from the preview-only MFA test, and are in the decisions ledger.
| Pre-run fact | Value |
|---|---|
| Unit and tenant | The Willows unit 101, Alder Wlhalpha (test tenant) |
| Current lease | Ends 2027-09-30, rent $1,550 (from the production lease record) |
| Proposed term | 2027-10-01 to 2028-09-30 at $1,550, respond by 2026-11-21 |
| Recipe in use | Willows test renewal, version 1, loaded and active |
| Robot's first look | "the lease shows no offer" (read only, nothing written) |
Western Slope shape at the Willows
Done, 2026-10-22 22:26 UTC. The same run, on a Willows copy of Western Slope's newest recipe (v3). The "original lease start" blank now comes from our own lease record through the new wiring, the parts AppFolio pre-fills come from the recipe's expected list, and the renters-insurance email is off. Built, read and parked in 5 seconds, every value read back as ordered, then cancelled and reset. Western Slope itself now has v3 in use, and stays at "team", so nothing runs there.
| Step | Before | After | Proof |
|---|---|---|---|
| Original-start blank | Held: "the original lease start is not on file" (same lease, same recipe, wiring removed) | Filled from the lease record: "Lease dated 10/1/2026" | planner dry run on the real lease; parked page below |
| Start | Unit 101 shows "Prepare Renewal Offer", no offer | Renewal saga_c67887ec… started, rung prepare frozen on the run | outreach route HTTP 200; Temporal input paperworkRung: prepare |
| Build | no order | Order 65906e73… from recipe renew-simple-willows-test@v2 | worker log "built from renew-simple-willows-test@v2; read then park" 22:26:37 |
| Read | — | "the lease shows no offer" | robot answer done, no writes |
| Park | no offer | Offer 376, document 900, "Not Sent"; 10/01/2027 to 09/30/2028 at $1,550 | robot: "every page, typed value and term reads back as ordered" |
| Record | last run's record | state parked on the lease | worker log "packet parked recorded on lease" 22:26:41 |
| Cleanup | parked offer 376 | "cancelled offer 376; the lease shows no offer"; workflow stopped; tenant reset; claim released; recipe out of use; rung at team | robot cancel answer; reset resetCount 1; final read "no offer" |


Pre-filled parts are read, not assumed (2026-10-23). Before the park, the robot now reads which parts AppFolio pre-fills on the lease, with their template names, and Clara builds the packet with those (app #21989, robot #616 and #617). Where they differ from the recipe's expected list (four Western Slope leases), the packet is built again with what AppFolio will attach instead of the park stopping.
| Check | Before | After | Proof |
|---|---|---|---|
| Read on Willows occupancy 986 (read-only, deployed robot) | blocked: "it pre-fills addendum 3, which its template list does not name" (first parser, robot #616) | done: "it pre-fills addendum 3", named "Willows TEST renewal (PropFlow)" (robot #617) | robot answers 2026-10-23, no writes |
| Respond-by day, 130 Western Slope leases ending Nov 1 or later, v3 recipe, read-only replay | 130 held on "the office picks the day the tenant answers by" | 0 held on it; 125 ready to park, 5 held on "the original lease start is not on file" | replay on production records, no writes (app #21981: the start saves start day + 30) |
Respond-by day stays before the new term (#21987, merged as #22051, no switch). The day saved at start is now capped at the last day of the current lease, and the packet is held with a plain reason if no valid day exists. Western Slope replay, read only: all 130 leases unchanged (125 ready, 5 held on the original start). Two real leases ending Oct 31 would have parked with respond-by Nov 21, inside the new term; now Oct 31, ready. With the cap removed, both hold on "the respond-by day is not before the new term starts".
Still open: a replay against the four Western Slope leases' own pre-fills (not captured yet).
Camellia new-lease park at the Willows
Parked, 2026-10-23 01:08 UTC. Clara built the new lease from the Willows recipe, the robot converted the approved application, saved the terms, made the lease document from the Camellia lease template and stopped before Send. The read-back matched, Clara recorded "parked", and the cancel cleared it. Nobody was messaged. It took three runs and two robot fixes to get there.
| Run (UTC) | What happened | Fix after it |
|---|---|---|
| Oct 22, 23:55 | Convert and lease terms saved; the lease document stopped: AppFolio answered 401 when the robot asked it to fill the template's fields | The robot now sends its session on that call (robot #612) |
| Oct 23, 00:47 | Past the 401; the same call got 403 | The call now carries the page it comes from, like every other robot call (robot #614) |
| Oct 23, 01:08 | Parked: occupancy 1099, lease 1558, lease document 901 (document 6941876), 11/01/2026 to 10/31/2027, $1,500 rent, $300 deposit, not sent | none |
| Step (run 3) | Before | After | Proof |
|---|---|---|---|
| Recipe | out of use | willows-test-new-lease@v1 in use for the run, then out of use again | release pointer set 01:08:29, deleted after the cancel |
| Build and read | application 128 approved, not converted | order e35b8097…; "ready to convert" | Temporal run 01a1664d-6448-7013-af92-5442f6b7c4e8 |
| Park | not converted | lease document 901 made and not sent | robot answer done; status read back after: parked, same ids |
| Record | held (from run 1) | parked: "Clara built the new lease and parked it unsent in the property software. It goes to the applicant when the office presses Send." | packet row at 01:08:52 |
| Cancel | parked on unit 102 | application unconverted, Convert offered, unit Vacant; occupancy 1099 hidden with a zero balance | robot cancel done |
| Leftovers from runs 1 and 2 | half move-ins 1097 and 1098 (lease saved, no document) | both cancelled, same read-back | robot cancel done twice (robot #613 added this) |



From a real approval, 2026-10-23 01:37 UTC. A fresh test application was approved at the Willows, and the sync started the workflow by itself, with nobody pressing anything. It parked the lease 25 seconds after the sync saw the approval.
| Step (real approval) | Before | After | Proof |
|---|---|---|---|
| Setup | recipe out of use; the Willows company on team | recipe in use, company on prepare | 01:36:26 and 01:36:28 |
| Approve | no application | application 129 "ZZTEST NewLease B", unit 102, move-in 11/01/2026, Approved | made and approved through the robot, 01:36:41 |
| Sync sees it | no prospect | prospect de1866a3 made; "New-lease packet started … no review item" | sync log 01:37:07 |
| Workflow | none | read done, park done: occupancy 1100, lease 1559, lease document 902 (document 6941911), $1,500 rent, $300 deposit | Temporal run 01a16667-977e-7e21-b3ac-c63b02fe794e, finished 01:37:32; packet row parked |
| Clean up | parked on unit 102; company on prepare | company back on team; cancel done (occupancy 1100 hidden, zero balance); recipe out of use | robot read-back before the cancel showed the same ids |


The office is told, 2026-10-23 05:59 UTC. With the office note live (#21977), one more Willows run parked a lease, and the leasing team's inbox got the note 5 seconds later. For this run the Willows team inbox pointed at a sandbox mailbox and was put back afterwards.
| Step (office note) | Before | After | Proof |
|---|---|---|---|
| Approve | no application | application 130 "ZZTEST NewLease C", unit 102, move-in 11/01/2026, approved | made through the robot, 05:55 |
| Sync | the card of application 128 (already ran) | 130 attached to that same card; logged "already ran", nothing started | Defect found: a second application on one prospect is dropped (#22014) |
| Park (started by hand, keyed per application) | not converted | occupancy 1101, lease document 903, $1,500 rent, $300 deposit, not sent | Temporal run 01a16757-2420-7154-a3bd-f260f44bb5bc; packet row parked |
| Office note | nothing told | "New lease ready to send: ZZTEST A. at The Willows" in the team inbox | received 05:59:15 from clara@propflowai.co |
| Clean up | parked; company on prepare | company back on team; read-back parked with the same ids, then cancel done; recipe out of use; Willows inbox back to its sink | robot answers done twice |

One run per application, 2026-10-23 07:39–07:46 UTC. With the per-application fix live (#22017), three more Willows approvals each started their own run with nobody pressing anything. The first ran while one worker on the old code was still shutting down, which taught two more lessons (below).
| Step (per application) | Before | After | Proof |
|---|---|---|---|
| Second application on a card that already ran | application 131 attached to the card that had 129; old id answered "already ran" | the sync started …-application-131 on its own and it parked (occupancy 1102, lease document 904) | sync log 07:39:06; Temporal run 01a167b2-feaa-71ee-a849-0479693790fb parked |
| Fresh applications | none | 132 and 133 each started their own run and parked (1103/905, 1104/906) | runs 01a167b7-9077-78b9-8abc-4d91b83a67d4 and 01a167b9-5eff-7d9f-a04e-edfb7de5ae8a |
| Office note names the application's applicant | "ZZTEST A." for application 130 (the card person) | "ZZTEST F." for application 133, "ZZTEST E." for 132 | team inbox 07:46:36 and 07:44:35 |
| Repeat approvals on the same cards | every sync re-attaches 128–131 to the two old cards | each answers "already ran" for its own application id; no extra run | sync log 07:44:05 |
| Clean up | 131, 132, 133 parked; company on prepare | all three cancelled done; company on team; packet rows corrected to held; recipe out of use; Willows inbox back to its sink | robot answers done; ledger |
Lessons. (1) For about 7 minutes after the deploy, one old worker still ran: it built the card's current application and named the card person ("ZZTEST B." for 131). A fix is proven only once the old worker is gone. (2) That old run replayed a cancelled order, and the robot answered "parked" with the cancelled ids (#22029, reproduced: 133 cancelled, replay answered parked 1104/906). Fixed in the robot: the same replay now answers blocked, with the live unconverted state (08:46 UTC).
| Proved | Not proved yet |
|---|---|
| Recipe → order → read → convert → terms → lease document, parked unsent, read back with the same ids | The office pressing Send on a parked lease |
| A real approval starts it: the sync saw application 129 approved and parked its lease with nobody pressing anything (#21941) | The office pressing Send on a parked lease after a rebind (not exercised) |
| One run per application: a second application on a card that already ran starts its own run and parks (#22014, closed by #22017) | The note naming the right applicant after a second application on the same card, on the new worker (the 131 note ran on the old worker; 132/133 were fresh cards) |
| The note names the parked application's applicant, read inside the note step, never through workflow history | |
A replayed park after a cancel is never answered "parked" (#22029, robot fix merged): 133 parked 1105/907, cancelled, replay answered blocked, nothing parked, nothing written | |
| The office is told: a parked lease sends the leasing team one note naming it (#21977), delivered at the Willows | The held path's card at the Willows (unit-covered only) |
| The lease document is filled from the property's data (dates, rent, deposit, unit, tenant) | |
| A failed write is held with a plain reason, never retried blind; the cancel clears a park or a half move-in | |
| Nothing is sent: the applicant got no message | |
| Co-applicants on one unit get one lease: 134 parked, 135 held behind it with a plain reason, no card, no note (#22052, live run) | Which co-applicant builds when the approvals arrive in different sync ticks (#22053) |
Dark: newLease.paperworkSender | feature: Clara parks the new-lease packet | proof: parked from a real approval and cancelled at the Willows 2026-10-23, #willows-newlease-park | turn-on: Fede
Top rung at the Willows: Clara sends
Proven end to end, 2026-10-23. Clara built the renewal, parked it, and sent it for signing herself. The test tenant got the portal activation email in our own mailbox, activated the portal, saw the offer there and signed it, and PropFlow saw the signature within 12 minutes. The tenant reminders stopped and the renewal page now reads "Tenant signed — pending your countersignature". Nobody countersigned. Everything was then cancelled and reset.
| Step | Before | After | Proof |
|---|---|---|---|
| Test tenant's email | propflowaicalendar+wlha@outlook.com (a personal inbox Microsoft flagged) | leasing+wlha@propflowtechnologiesinc.onmicrosoft.com, a plus-address on a mailbox we already read (no new mailbox) | robot email change, read back 02:06 UTC; robot #615 added occupancy 1081 to the Willows test list |
| Start, park, send | no renewal; Willows on team | packet sent: "Clara built the renewal packet and sent it to the tenant for signing in the property software." | start 14:21:36 UTC with no staff login; robot read "sent offer 377 with document 908 (out for signing)" |
| Portal login | the old Outlook address, set by the first invite and never moved by an email change | leasing+wlha@, verified by read-back | new robot action "tenant portal invite" (#619, CSRF fix #620): removes the pending login, re-invites, reads back; Willows list only |
| Tenant signs | out for signing | signed in the portal 10:09 MDT: "You have successfully signed your renewal" | portal activation email (15:23 UTC) opened in leasing@ via Graph; headless portal run, password in Keychain; screenshots below |
| PropFlow sees it | tenant reminder due in 1 day | workflow got pmsChanged → tenant_signed at 16:21:41 UTC; the 1-day reminder timer was cancelled and no outreach ran after it | Temporal history events 134 and 138; hourly sync tick at :21 |
| Renewal page | Sent | pill "Tenant signed — pending your countersignature" | the page's own loader, run with a Willows-scoped reader (not a browser screenshot; no staff login) |
| Cleanup | offer 377 at Ready to Countersign; run live | AppFolio "Renewal Status: Canceled", row back to Eligible; run stopped; saga, tenant and lease reset; mint claim deleted; pill "Ready" | robot print-order cancel "cancelled offer 377; the lease shows no offer" (needed fix #622); ledger 16:53 UTC |



| Proved | Not proved yet |
|---|---|
| Clara sends a parked packet and AppFolio shows it out for signing (#21956) | The office countersign nudge. It is silenced on purpose for sandbox companies, so it needs the sandbox-company rung. |
| The tenant gets the invite, signs, and PropFlow sees it within one sync tick | The held path when the emergency stop or Renewals switch is off (unit-covered only) |
| Tenant reminders stop on the signature; the page shows the countersign pill | Countersigning, by design (we never countersign a test renewal) |
| Tenant texts point at the portal once sent (#21960, unit-covered) | The reset leaves the lease's packet note behind (issue #22066; harmless to a re-run) |
Found and fixed on the way:
- The portal login does not follow an email change. The new robot action moves it (#619, #620).
- The robot read a tenant-signed offer as "no offer": AppFolio shows only a bare "Countersign" cell. That made a signed lease look free to prepare or resend, and the offer impossible to cancel. Fixed in #622; the print-order cancel now takes a parked, sent or tenant-signed offer by its exact ids, and never a countersigned one.
- The resend-for-signature call was not needed: the offer appeared in the portal as soon as the tenant activated it.
- Recording the demo found one more: a second run on the same lease with the same recipe version got the robot's saved answer for the cancelled offer 377 ("parked", then "sent"), while AppFolio showed no offer. Nothing was written in AppFolio. The demo used an identical recipe saved as version 3, which parked and sent offer 378 for real. Robot fix to come (#22110, the renewal side of #22029).
Direct start, the standard for Willows runs (no staff login):
- Take a robot read of lease 1081 and refuse if it is not empty.
- Put the Willows recipe in use and set the building to
clara. Turn Renewals on at the Willows building only. - Run the outreach route's own steps directly. The script is a copy of the route body after its sign-in checks, and refuses any tenant, building or lease other than the Willows test ones. It uses prod data and the prod Temporal key from Keychain.
- Put the building back on
teamright away. The run keeps the rung it started with. - After the robot reads back the send, take the recipe out of use and turn Renewals off.
- The Willows test tenant's portal is now active as leasing+wlha@ (password in Keychain
propflow-willows-tenant-portal). A new offer shows in the portal straight away: log in and sign, no invite step. The portal invite action refuses an active portal by design, so it is only for a different test occupancy. - To look at the renewal page without a staff login, run the page's own loader with an in-memory reader scoped to the Willows, and print the pill.
- Reset with the domain steps, not the reset route. Cancel the offer through the robot's print-order cancel, naming its stage and ids. Stop the run. Clear the saga, tenant and lease with the saga-aware reset. Delete the mint claim for the 2027-09-30 cycle.
Every write is ledgered. The scripts are local to the run and never committed.
Dark: renewal.paperworkSender=clara | feature: Clara sends the parked renewal packet | proof: sent, tenant-signed and seen at the Willows 2026-10-23, #willows-clara-sends | turn-on: Fede
Learn a customer's paperwork
The learner read each company's past renewals through the robot's existing session, read-only, and wrote a draft recipe plus the questions only the office can answer. Nothing was changed in any customer's AppFolio.
| Company | Past renewals | What it found | Zero manual work by Fede |
|---|---|---|---|
| Western Slope (control) | 55 (read 30) | Reproduces the hand-written recipe: one-page addendum, six typed blanks. | yes |
| Situs Group (blind) | 1,836 (read 30) | A whole new lease plus about 13 addenda, 27 blanks, 3 templates; 10 of 30 are month-to-month; every term runs 1st to month end. 23 questions for the office. | yes |
| Camellia | 527 (read 30) | A PDF form with 5 unlabeled boxes left blank 83–93% of the time. 11 questions. | yes |
Open decisions (yes / hold, one line each)
- Willows runs need a prod login that can write. Un-waitlist the e2e test account for good (it now has an authenticator), or give sessions the nightly smoke login? Recommend un-waitlist.
- Gera's "prepare" rung: Supervised and Live, or Live only? Recommend Live only.
- Approving in AppFolio counts as the human "send" decision? Recommend yes.
- Camellia's 10 stale renewal cards: recommend leave them.
- Western Slope office questions (for Kat and Jay, when Fede chooses): which email for insurance; which lease start goes in the "original start" blank; and four pre-fill mismatches to confirm, at 239 S Elm, 1009 Colorado, Northridge and one Pomona Park lease.
Next
- New lease: a real approval parks it and the office gets the note at the Willows (Oct 23; #21972 and #21977 merged). One run per application merged (#22017) and seen live. The robot replay fix (#22029) merged and proven by a park → cancel → replay cycle.
- Wire Western Slope's reads into the packet build and point their recipe at the newest version.
- The polish follow-ups: plain-English hold reasons, "held" not red, rebuild when the list is approved.
- A Situs-shaped practice round at the Willows, read-only everywhere else.
1 · TL;DR
- Clara fills the customer's own forms in their PMS, parks them unsent and reads the page back. Willows test form, Oct 20: 6 of 6 read back, no email.
- One engine in our app, for renewals and new leases; the robot only executes the engine's print orders and gives one of five answers.
- AI studies past packets once; every renewal after is a replay. The proof's hand-written rules got 24 of 25 unseen values right (22 of 25 if the original-start tie goes to move-in).
- Only Fede moves a document shape up the ladder: Shadow → Prepare (staff press Send) → Send.
- No new setting: the who-sends-the-paperwork setting gains "Clara prepares, office sends".
- Western Slope: first parked packets planned for December, for March.
2 · The 10,000-foot view
3 · What the last two weeks got wrong, and what stays
| Verdict | What |
|---|---|
| Keep | The Temporal workflows and reminders, reading each tenant's intent, notice-to-vacate, the renewal list where the office types its decisions, AppFolio's e-sign and countersign. In the robot: the session layer, read-only lock, scope guard, budget, breaker and pacer. Of its 71,406 library lines, the 63% that knows how to call AppFolio and the 27.5% that is session and plumbing stay. |
| Wrong: fold | About six gates had to agree before Clara could send one renewal by herself, while seven holds sat on the texts leg, which reached 12 of 59 tenants on Oct 19. After: one value per company or building, plus three rules in code: never park a blank, never build over an open offer, never send without a matching read-back. |
| Wrong: fix | The fill was never proven until Oct 20. The robot's live renewal path always sends (notice letter and insurance email on) and sends any prepared-but-unsent offer it finds, unchecked. 21 business rules sit inside the robot (rent math, a today + 30 deadline, odd terms become 12, JP&Co's accounting codes 229/83 and a $200 deposit when none is given, Western Slope's field map, a built-in default account), and per-customer setup moved back in from September, despite May's unwritten principle "Caller's policy is authoritative". |
| Wrong: the cut | The cause that explains the most of "so much built, nothing on": we treated the packet as the hard part and cut it on Oct 7 (Fede: "half of their properties don't have a template"). Simplify first (hub section 12.5) stays the first step. |
| Delete | Duplicate switches (the global autonomous-sending and auto-start arms, the holdover fallback, the duplicate building outreach-days field, the legacy policy blob on Nov 9), the robot's unreached files and 129 one-off, recon and proof scripts, the per-route customer lists. |
4 · Your questions from Oct 20, one line each
| Question | Answer | Where |
|---|---|---|
| Is the robot doing too much? | Yes in policy, no in plumbing. 21 business rules and a built-in customer live inside it, but only 96 code lines name a customer (77 of them in four config files), and the 63% that knows how to call AppFolio stays. | 8 |
| Another layer, or a separate thing? | The engine lives in the app, where the settings, Temporal and an unused renewal adapter already sit. Onboarding and learning are their own capability: Gera's ingest drafts, the app activates. The robot keeps the driver and the session behind a neutral interface. No new service; where the driver finally lives is decided last. | 8 |
| Browser agent or direct calls? | Direct calls, as since May 11: 67 of 91 robot routes make direct web calls; 9 still click through screens. The best AI browser agents score 75–80% on live sites, too low for writes into a system of record, so AI maps a new form once under the read-only lock and the replay is plain requests. We found no documented AppFolio way to create leases or renewal offers (unverified). | Notes |
| Why did automatic new leases at Camellia fail? | Traps the robot never checked before writing: a unit not ready or occupied, a deposit that posts the moment Lease Details is passed, no way back once an application is converted. Plus values it filled in silently. On Oct 20, on a Willows test application, a read-only readiness check wrote down "Unit Is Occupied" before the attempt, and AppFolio said exactly that. | 11 |
| How do we get to deterministic? | AI reasons once per document shape, at onboarding, into a versioned release: which parts go in, where each blank comes from (with its evidence count), and standing answers. Run time is a replay with no AI; the read-back is the check. AI returns only to repair a failed check, the repair must replay the past packets, and it stops after three tries. Four products moved this way (one from search results). | 5, 6 |
| "Don't go fully autonomous until we have high confidence that it works"? | The ladder: Off → Shadow → Prepare → Send, per company and document shape, with written evidence to move up. Only Fede presses; nothing moves down by itself. | 7 |
| Teams that want Clara to prepare but not send? | The Prepare rung, where some teams stay by choice. Western Slope asked for exactly this on Oct 20, in Fede's retelling of the unrecorded meeting: "prepare the lease package… don't send it until they review it". | 7 |
| Complex packets (cover letters, addenda, pets, smoking) or start simple? | Complex cases are data in the release (parts to include and remove, standing answers), never code. A setup report shows the customer where their PMS setup is bloated or wrong, and we fix it on their OK. Western Slope's one page is on 11 of 11 recent offers, and AppFolio already prints three of their six typed blanks itself. | 6, hub 12.5 |
| What did outside frameworks and vendors teach? | Explore once, then replay (Browserbase, Stagehand v4). Save the session back after each run. Never two sessions on one profile. A platform cron can skip or double a run, so Temporal owns every retry. No property-management vendor publicly claims to prepare documents from each customer's own templates (inference from public material), so this is open ground. | Notes |
| Determinism: "how do we arrive at that"? | Onboarding, then training on their examples, then AI reasoning once, then the customer confirms one plain page per shape, then replay. | Figure 2 |
5 · Learn once, then repeat
| Western Slope blank | Where it comes from (recipe learned Oct 20 from 11 past previews on disk) | Right on |
|---|---|---|
| Today's date | Derived: the prepare day, in the property's time zone | 11 of 11 |
| End date | Derived: last day of the month holding start + term − 1 day (Fede, 2026-10-21, from Kat: the mid-month ends on past previews were a mistake; PR #21655) | ruled, not learned |
| Original lease start | A lease fact; three rules tie, so it stays an ask | 9 of 11 |
| Start date | Office decision; default: lease end + 1 day | 10 of 11 |
| Rent | Office decision; default: the renewal's future rent stored in AppFolio | 10 of 11 |
| Special terms | Office decision | blank on 9 of 11 |
5b · Learn a company's paperwork, repeatably
One command now does section 5 for any company. It reads the company's past renewal documents through the robot's warm session (look only: no login, stops at the first sign-in page), learns where each typed blank comes from, and writes two files: a draft recipe that passes the app's release check, and the questions the office still has to answer. A blank counts as learned when one rule fits at least 90% of at least 5 documents; anything less becomes a question. Run on Oct 21 against all three companies, the 30 newest documents each, read only.
| Company | Past renewals | Read | Main paperwork | Typed blanks | Learned | Questions | One way or several? |
|---|---|---|---|---|---|---|---|
| Western Slope | 55 | 30 | One-page renewal amendment, on 26 of 30 | 6 | 3 of 6 | 7 | Several: 5 templates; on the main one, 3 of 6 blanks follow one rule |
| Situs Group | 1,836 | 30 | A full new lease plus about 13 addenda ("Updated Lease 2026"), on 19 of 30 | 27 distinct (about 21 per document) | 5 of 17 seen on 5+ | 23 | Several: 3 templates; on the main one, 5 of 17 blanks follow one rule |
| JP&Co (Camellia) | 527 | 30 (3 Willows tests skipped) | A PDF form ("Camellia Lease Renewal"), on 29 of 30 | 5 unlabeled boxes | 2 of 5 | 11 | One template; 3 of 5 boxes are filled more than one way |
The gate: Western Slope against the recipe we already ship. On the same 11 documents behind release v3, at v3's own bar (80% over 3), the learner finds all 6 blanks with the same field ids, the same rules and the same counts. The 11 leftover differences are all explained: part names come from the template title; it asks about the insurance email and the notice letter, because v3 got those from Fede and the setup, not from documents; v3's end date still carries a count from before the month-end ruling, which the learner correctly leaves off; and it learns the expected pre-filled parts (11 of 11). At the stricter 90% bar, original lease start (9 of 11) and special terms (blank on 9 of 11) become questions. Planted check: PASS. Swapping the start and end dates on one document flags that document, and only that one.
Situs, seen for the first time, with no manual work:
- A renewal is a whole new lease, not a one-page amendment: lease plus about 13 addenda, about 21 typed blanks.
- 10 of the 30 newest renewals used a month-to-month lease instead; the office must say when.
- Every term starts on the 1st and ends on a month end (19 of 19), the same as Western Slope's rule.
- "County of" is typed on every addendum and follows the building (89–100%); deposit, language, parking and occupants vary by tenant and are questions.
- Their login cannot open the template editor, so 6 blanks are named by the words around them until we read the field ids from the prepare form.
Camellia: AppFolio prints the dates, rent and names on the form itself; the 5 typed boxes are blank on 83–93% of documents and have no labels in what we can read, so the office has to say what each is for.
Next: a "same as their last lease" rule would likely turn Situs deposit and language into learned blanks. Building facts such as "County of" need a property-fact source wired in before any replay. Draft recipes, questions and per-document reports stay in the harness (local, no tenant names).
5c · Learner → onboarding (Situs test) In progress, Oct 23
Goal (Fede, Oct 23): when a new customer connects their PMS, Clara learns their renewal paperwork herself. She writes a draft recipe nothing uses, plus a plain "what Clara learned" card for the team. This supersedes the Oct 21 call D3 ("no in-app onboarding flow now").
Before → after
| Before (Oct 22) | After (Oct 23) | |
|---|---|---|
| Where the learner runs | A script on one Mac, reading AppFolio itself | App code (#22099), run by staff for one company, reading through the robot's read-only route (robot #625, merged) |
| What it writes | Two local files | Draft recipe versions with no pointer moved (nothing uses them), and one "what Clara learned" record per run |
| Situs blanks Clara fills (main template) | 5 of 27 | 15 of 27 once the Situs shape lands: deposit, language and parking follow "the same as the household's last renewal"; County of is learned on 9 of its 10 parts (by building on 4 addenda; as on the last renewal on the lease and 4 addenda; the pet addendum still asks) |
| Western Slope control | Harness only | App learner on the same 11 documents behind v3: blank ids and classes byte-identical to the shipped v3 (6 of 6). On the 26 newest documents, two v3 rules hold less well: "original start" 18 of 26 (69%, v3 learned 9 of 11) and the special-terms blank left empty 20 of 26; the learner turns both into office questions |
Situs recipe card (no tenant data)
- Book: 1,836 past renewals (671 signed, 44 out for signing, 5 printed, 1,116 cancelled). 30 newest read, read only.
- Main paperwork: "Updated Lease 2026", a whole new lease with 11 addenda (19 of 30). Month-to-month lease is its own recipe (10 of 30). A 6-month template was seen once and is too few to learn.
- Term: 12 months usual; every term starts on the 1st and ends on a month end (30 of 30).
- Office questions include: who sends, the respond-by day, the renewal letter, the insurance email, occupants, pet details and fees, and how amounts are written (with and without cents).
- For us: 6 blanks are named by their surrounding words until the prepare form gives their field ids.
Dry-run onboarding transcript (Situs read; every write at the Willows)
| Step | What Clara did | What the office would see | Evidence |
|---|---|---|---|
| 1 · Connect | The import has Situs's buildings (Gera's design A8) | Their buildings on the customer page | org_a3376147 · 53 buildings |
| 2 · Clara reads the book | Lists 1,836 renewals, reads 30, read only | Nothing yet | robot #625 route; 30 documents from the read-only cache |
| 3 · What Clara learned | Learns 2 recipes, asks the rest | The card: templates, who fills each blank, the open questions | #22123, live in production Oct 23: screenshots below |
| 4 · Office answers | Dry run: 7 answered from the read, each marked "assumed" | Answers beside each question | Learned record v1 + v2 at the test company Fairhaven (the Willows' company has no customer page). The card shows v2 with all 7 answers marked "assumed" |
| 5 · Sender set to prepare | At the Willows only | "Clara prepares nothing until you set the sender" goes away | pending |
| 6 · First packet prepared | Shape proof at the Willows: a renewal whose main part is a whole lease template, parked through the robot, read back, cancelled, reset. Not a packet from Situs's learned recipe: its field ids and template belong to Situs, and applying office answers to a recipe is not built yet | A parked lease in AppFolio | Blocked, nothing written. JP&Co's AppFolio has no lease template (only addenda and PDF forms), and the robot's field check never asks about a lease template (robot issue #626). Fede: may we add one test lease template to JP&Co's library? Recommend yes, after #626. |
The card in production (Fairhaven test company, Oct 23)
| Before | After |
|---|---|
| Only the renewal policy; nothing showed what Clara learned | "What Clara learned" sits under the policy: 3 templates, 15 of 27 blanks Clara fills, the terms, 18 open questions, 7 answered and marked "assumed". The sender warning shows because no sender is set |



QA on the live page: the card loads with no error; the policy card above it still has its controls; the customer list loads. Follow-ups: #22125, #22126.
Where the ingest app fits (Gera's portfolio design)
- The learner runs after the import and never repeats its reads; it reads only past paperwork, which the import does not hold.
- Conflict 1: "import complete" does not mean leases are there (they come on the nightly pass). An automatic trigger must wait for both. Asked on the engineering bridge; not built here.
- Conflict 2: the import's customer stamp can be blank when staff run it, so the learner takes the company from the page, never from the import record.
PRs: #22099 (learner), the job + staff entry point, the Situs shape, the card. Follow-ups #22107, #22108. Records stay local; no tenant names here.
Western Slope: the renewal form points at the new-lease template (read-only finding, Oct 24)
A read-only look at Western Slope's AppFolio (property 403, four paced GETs on the saved session, no sign-in, nothing written) shows their renewal defaults point at the lease template "Northridge Lease Agreement NEW LEASES" (template 3) rather than the renewals template (template 5), and the renewal addenda list does not include the one-page RENEW (SIMPLE) amendment Clara learned to fill.
Result: the typed-field check lists 9 fields today and 33 once robot PR #632 is live (24 are new-lease fields such as totals due at signing and 18 special-provision lines). Clara's filler knows only the six amendment blanks and none of these, so a robot renewal at this property would be refused before any write, on today's code and after #632 alike. The refusal is the safe direction; #632 does not change the outcome.
Open call for Fede: (a) point the property's renewal defaults at the RENEWALS template and add the amendment to the renewal addenda (a change in their AppFolio, their staff or Fede), or (b) teach Clara these fields from past documents. Recommended: (a). Until then, Western Slope renewals stay as they are today.
6 · How it works
In: the kind (renewal, amendment, month-to-month, new lease), the terms (prior lease plus the office's typed decision, or the application, unit and policy), and the shape's active release.
Readiness, read only, before any write: eligible; no open offer (theirs wins); signers have emails; rent above zero; no past start. New leases also need the unit empty when the application converts (that is what blocked the Oct 20 test; an Available On date was not tested), screening done and the deposit decided.
Assembly: the print order is the complete list of what to put on the paper and what to leave off: parts in and out (AppFolio adds the property's defaults and the original lease's addenda itself), every value by field id with its exact text, and the options.
Park and read back: one call parks the offer "Prepared, not Sent"; then every value on the finished page must match and no blank may stay unfilled (on Oct 20 AppFolio accepted a wrong field id and parked a blank rent).
Then: at Prepare, staff press Send in their PMS; at Send, Clara re-reads, then sends. After signing, the daily status read checks rent and charges against the order.
Worked example: occupancy 876, read only, Oct 20
The office promised a 12-month renewal on Aug 2; nothing was built. Worked by hand: the decision reader would fill the list row from that promise, and readiness would raise three flags: the promised start is past and differs from the form's default; the form offers month-to-month, which the promise excludes; the promise is overdue. One question goes to the Tuesday pre-read; nothing is written at Western Slope.
The executor contract: what the engine sends, what the robot answers
| Item | Rule |
|---|---|
| Print order id | Built from company, lease, kind, release and a fingerprint of the decision; stored in the app before the call; claimed in the robot's durable store before any write, as texting sends already are. Same id and same body returns the saved result; same id and another body is refused. Never kept only in memory. |
| Engine → robot | Account, property, occupancy or application, kind; the verb (read, park, send the parked offer, cancel); parts to include and remove, in order; every typed value by field id with its expected text; options; the expected state before (none, or an offer id and its fingerprint); an absolute deadline. |
| Robot → engine | PMS ids; the state before and after; the rendered values and part list; one answer class with a reason. |
| done | The PMS matches the order, proven by the read-back; a replayed id returns it too. |
| blocked | Nothing changed; a named PMS condition stops it (open offer, not eligible, unit occupied). |
| refused | Nothing remains: the order was incomplete or contradicted the PMS, or it rendered wrong and the robot cancelled its own draft and re-read it gone. |
| unclear-state | State unknown: the write may or may not have landed (killed mid-write, a server error on a write, a failed undo, a wrong new-lease draft). Never retried blind; the next step is a read with the same id. |
| transport | The write never started: login, breaker, rate limit. Carries a retry-after; the only class that is retried. |
| Read before write | The offer's state and the parts the PMS will pre-fill, against the expected state; a mismatch is blocked, and an offer the office built wins. |
| Read after write | The rendered document and the term line. A renewal mismatch is cancelled, re-read and refused; a new-lease mismatch is unclear-state, held for us, because a cancel can leave a rent charge behind. |
| Timeouts | Robot deadline under the route limit under the workflow step's limit (proposal: 240, 300 and 330 seconds). Today it is inverted: the app gives up at 540 seconds while the robot may run 800. |
| Retries | Temporal only, transport only, three attempts that honor the retry-after. Paper jobs call the robot from a Temporal step directly, so the queue's four attempts and the robot's own write re-sends drop out. |
| After unclear-state | A read decides: the ordered state is done; the before state releases the claim and may run again; anything else is held for us. A send is never repeated automatically. |
| No defaults | A blank or unknown value is refused before any write; the robot never fills a gap. |
| Closing the blind send | What changes | Switch |
|---|---|---|
| Stage 1, now | Before sending a parked offer it finds, the robot reads its terms and compares rent, term and dates with the request. A mismatch is refused; a match is sent as today, so Camellia's recovery still works. | None: a fix |
| Stage 2, with the contract | A send names the park's print order id, the offer id recorded at park and the order's fingerprint; the robot re-reads and refuses any difference. The old implicit send is deleted last. | None: nobody calls it yet |
Clara never sends an offer the office parked, and at Prepare she sends nothing at all.
7 · The ladder: who presses Send
Settings, head-on. Extend, don't add: the who-sends-the-paperwork setting already exists per company and building, unset means "their team", and Camellia's explicit "Clara" row stays untouched, so merging changes nothing. It gains one value: "Clara prepares, office sends". Shadow needs no setting because it writes nothing, and new leases keep the Oct 15–16 decision as built (the company's send-leases-for-signature switch plus the building's leasing status). Assumption for Fede: whether one new value counts as a "settings change" under the Sep 13 freeze (decision 2).
8 · Where it lives: the robot before and after
| Exit order | Where | What |
|---|---|---|
| 0 | Robot | Four fixes under the fix grant, no switch, one small PR each: a terms check before sending a found parked offer; heartbeat expiry 15 → 30 minutes (today's margin is about 3 minutes); "today" in the property's time zone, not UTC; the screenshot route locked so it cannot report "captured" on a sign-in page or overwrite the shared profile. The stale cookie copy is checked first, then fixed if the robot's own saves hit it. |
| 1 | Robot | Adds the verbs, the durable claim, the five answers, parts in and out, and values. The old request still works. |
| 2 | App | Releases as data, plus the replay harness. Western Slope's one-pager from the Oct 20 recipe; JP&Co's release v1 is today's constants. |
| 3 | App | The engine: readiness, assembly, compare, ids, and the Temporal step with aligned timeouts. Nothing calls it for a customer. |
| 4 | App | The new paper value, default off; build right after the typed decision; the "N packets ready" note; daily status on the list. |
| 5 | App, later | Camellia sends full print orders only after its last three offers rebuild byte-identical from data (check P8). |
| 6 | Robot | Logs every value it still fills in itself; one quiet week. |
| 7 | Robot | Requires the order. Deletes its defaults, the implicit send, the built-in account, the built-in template and letter ids, Western Slope's field map, the fallbacks, the new-lease property allowlist and dead switches. |
| 8 | Then | Other writes (new-lease accounting codes first), test fixtures bound to their account, dead files and scripts, and where the driver finally lives (decided last). |
Two repos, never a gap. Each robot step deploys first and accepts both the old and the new request. The app moves only after that robot version shows as live on its health check. A default is deleted only after the app has sent that value on every call and a week of logs shows zero fallbacks. Each step reverts alone, and Camellia's order is proven byte-identical before any of its constants leave the robot, so there is never a window with neither guard.
9 · Every gate, switch and robot rule: keep, fold or delete
Before → after: about six gates agree today before Clara sends one renewal; after, one value per company plus three rules in code.
Open the full table (25 rows)
| Item | Verdict | Why |
|---|---|---|
| Renewals module, per building | Keep | Runs the workflow and the messages; paper reads the paper value instead |
| Who sends the paperwork | Extend by one value | It is the paper rung |
| Review offers before sending | Keep for messages | Paper ignores it; the app reads it to open the monthly list |
| Offer terms, eligibility, auto-start days, contacts, cc list, lapse digest | Keep | Settings, not gates; seeded from PMS defaults and history |
| Building outreach-days field | Delete | A duplicate |
| Holdover: global arm, building field, fallback | Fold into the building field | Delete the arm and the fallback after a read shows no building changes |
| Global autonomous-sending and auto-start arms | Delete | The sending arm duplicates the module; auto-start's overlap is our read. Delete after a read shows both armed |
| Auto-start excluded-people list | Fold into do-not-contact on the record | A decision, not a setting |
| Legacy policy blob | Delete Nov 9 | Two homes for one answer |
| Monthly list approval click | Delete for paper, keep for messages | The typed decision is the approval |
| Your per-offer hold (hub D8), Supervised (hub D10), outreach hold, text door | Keep for messages | Off for paper is decision 5 |
| Change-review queue | Fold for paper | A term change cancels our draft and a new order parks (assumption: code not read) |
| New-lease leasing status and send-leases-for-signature | Keep unchanged | Decided Oct 15–16 |
| Robot rules 1–5 and 7–9 (rent math, deadline, terms, letter, insurance, month-to-month, field map, change-term) | Move into the order | The robot refuses an absent value |
| Robot rule 6: auto-send of a found parked offer | Fix now, delete last | The blind send, section 6 |
| Robot rule 10: accounting codes 229/83 and the $200 deposit | Move into the order first | Money values |
| Robot rules 11–17 (unit fee, move-out code guess, "Rent Income", work-order categories, inactive reason, vendor defaults, listing target) | Move to the caller, in their own chains after paper | Not paperwork |
| Robot rules 18–20 and literals (property 45 test modes, per-route customer lists, switch copies, a JP&Co-only daily refresh) | Bind to the account, or delete | Not paperwork; the daily refresh runs for every account |
| The built-in default account, JP&Co's environment fallback, the built-in template and letter ids | Delete | After JP&Co's row is seeded and P8 passes |
| New-lease property allowlist | Delete once the Oct 15–16 gate covers every call path | Keep the scope guard |
| A past date moved to today | Keep | An AppFolio rule; new leases hit it too, since they reuse the move-in form. On paperwork, a read-only readiness check blocks a past start before any write |
| "Today" computed in UTC | Fold into the print order | The engine sends the property-local date; the robot never computes it |
| In-memory duplicate cache; write re-sends | Replace with the durable claim | Only reads retry |
| Heartbeat 15-minute expiry; screenshot-route profile writes | Fix | Exit order step 0 |
| Re-mint window, per-account write stop, session layer, read-only lock, scope guard, budget, breaker, pacer | Keep | Safety |
10 · The MVP, and the PRs after your yes
The Oct 20 MVP ran locally, with no PR and no CI: the recipe runner learned Western Slope's recipe from 11 previews on disk, replayed them, and at the Willows filled, parked, read back and cancelled a ZZ TEST packet through the deployed robot's warm session.
| PR | Repo | What | Switch | Ladder rungs |
|---|---|---|---|---|
| Fixes | Robot | Exit order step 0, one small PR per fix | None: fixes | Unit test with a planted defect → read-only replay at JP&Co and Western Slope → Willows live |
| 1 | Robot | Park, send-the-parked-offer, cancel and read, taking the print order; the durable claim; five answers; rendered read-back with the unfilled-blank check; no write re-sends; the old request still works | None: verbs nobody calls yet | Unit tests (wrong id, unfilled blank, same id with another body) → Willows live park, read-back, cancel, same id twice |
| 2 | App | Releases as versioned company data; Western Slope's one-pager recipe from Oct 20; JP&Co's release v1 is today's constants; replay harness with controls; the PMS adapter widened from offers to documents | None: data and a harness | Unit → read-only replay: the 11 Western Slope previews; JP&Co's last three orders byte-identical (P8) |
| 3 | App | The engine: readiness, assembly, compare, ids; the direct Temporal step with aligned timeouts and three transport retries | None: nothing calls it for a customer | Unit → history replay → Willows live through the engine |
| 4 | App | The new paper value; build right after the typed decision; "N packets ready"; daily status on the list | One new value on the existing setting, default off: a new capability | The full ladder through the Western Slope test property before Fede turns it on |
Each PR is dark and mergeable alone, under about 600 lines (assumption). The compile tool (corpus pull, candidate rules and their counts, the setup report) runs locally on the subscription in November and enters Gera's ingest app as its own chain after his go. Camellia's move to full print orders and the robot clean-up are their own chain after the MVP (superseded by the Oct 21 note above). About a week for one builder (estimate).
Status. Merged 2026-10-21: #21605 door, #21652/#21653 engine + Temporal step, #21664 'Clara prepares, team sends' rung, #21655 month-end rule. Open: #21672 first caller. Next: Willows run.
11 · Proof from Oct 20 at the Willows
Before and after
| Question | Before Oct 20 | After Oct 20 |
|---|---|---|
| Can we fill typed blanks and park without sending? | Never seen at a property | Yes: 6 of 6 read back, offer "Not Sent", no email |
| Do AppFolio's own fields show the old or the new term? | Open design question | AppFolio's own start, end and rent fields show the new term; format differs (11/01/2026 and $1,615.00 vs 11/1/2026 and $1,615) |
| What does a wrong field id do? | Assumed it fails closed | Wrong field id accepted: AppFolio 200, rent left unfilled, the robot was silent |
| Does the recipe hold beyond its examples? | Untested | 24 of 25 (22 of 25 if the original-start tie goes to move-in); every control goes red |
| Can we predict new-lease refusals? | Two past incidents | 1 of 1 logged-ahead prediction (application 58); agrees with 2 more checked after the fact |
Proved, disproved, could not run
| Proved | Disproved | Could not run, and why |
|---|---|---|
| Fill six typed blanks, park as "Prepared, not Sent", read back 6 of 6, cancel cleanly; parking sends no email | "A wrong field id fails closed" | Tenant portal look: we own no portal login for the test tenant |
| The read-back catches a wrong value (a planted $1,616) | "AppFolio's field-value list holds the typed values after prepare": it stays blank, so the rendered page is the only place to read them | 48-hour reminder look: the brief said cancel the same night. Send control: we never send |
| The robot refuses bad input before any write | "The robot reads AppFolio's convert refusals": Unit Is Occupied is now a two-choice dialog, one option irreversible; the robot's parser misses it | PDF-form park, Camellia's shape: no renewal PDF form is set at property 45 |
| Camellia Lease is 408767 and Camellia Lease Renewal is 417515 (settled) | A new lease sitting prepared and unsent: the chain stopped at convert | |
| A property's renewal settings switch and restore exactly | P8, Camellia's orders built from data: optional, not attempted |
What we found in the robot
| Robot finding | How we know | What it means |
|---|---|---|
| 1. A wrong field id is accepted | Seen live | Read back the rendered page and refuse any send while a blank is unfilled |
| 2. The convert-refusal reader misses the "Unit Is Occupied" dialog | Seen live | The robot still stops, but reports a generic error instead of AppFolio's words |
| 3. "Unit Is Occupied" is a choice, not a dead end | Seen live | Add Additional Tenant cannot be undone; Move In Future Tenant asks for the current tenant's move-out date; neither without the office |
| 4. A stale cookie copy breaks form saves | Seen in our test browser | Whether the robot's own saves hit it is unverified: check, then fix |
| 5. "Today" comes out in UTC in the renewal flow | Read from the code | An amendment made after 6 PM Mountain (5 PM in winter) would carry tomorrow's date |
| 6. The renewal flow always sends, letter and insurance email on | Read from the code | A park-only step needs its own entry point |
The Willows now: offers 369 and 370 cancelled; occupancy 986 shows no offer; property 45 equals its snapshot from before the writes; the ZZ TEST template and fields 1–6 are kept; 37 units, none created; 0 emails dated Oct 20. Screenshots stay local: they show test-record names.
Still to check
| Still to check | Expected | If not, we learn |
|---|---|---|
| Portal look, with a test-tenant mailbox we own | Nothing visible before Send | Drafts wait in PropFlow |
| Re-park with an 8-day deadline; look 48 hours later | No reminder | Park with later deadlines |
| Removing a part per offer (a default plus a carried addendum) | Only the listed parts | Removal is setup, done on their OK |
| PDF-form park (Camellia's shape); property 45 needs a form first | Values equal | Camellia keeps today's path |
| A new test application on a vacant unit with Available On set | The draft stops unsent | The Oct 15–16 hold is the only stop |
| P8: Camellia's last three orders rebuilt from data | Byte-identical | Its defaults stay |
| Contract controls: same id twice; another body; kill mid-park | One offer; refused; unclear-state, then a read | The claim is not durable |
| Western Slope test property | 6 of 6 | Their template differs |
11b · Proof from Oct 24 at the Willows: a form Clara does not know
Setup. A seventh blank Clara has never seen was added to the Willows renewal form, plus a second addendum, with the Willows on the old prepare path. A renewal was started directly, no staff login.
Result. Clara's filler refused the form three times (one try, two retries, 21 minutes in all) and wrote nothing in AppFolio. The office then received one email: "Renewal not filled for unit 101. I couldn't fill the renewal for unit 101. The form AppFolio opened isn't the one I learned, so I left it blank." with a link to the renewal. The Willows form, addenda and setting were restored afterwards.
Two things found on the way, both fixed the same evening. (1) On the new engine path a held packet told nobody in the office; now the same one-line note goes out the same day (merged Oct 24, 20:30 UTC). (2) When the old path decides not to call the robot at all, it still reported "dispatched" and the office got a false "prepare timeout (42 minutes)" after a silent wait; a refused call now ends the renewal at once with the real reason (merged Oct 24, 20:45 UTC).
Western Slope read, Oct 24 (read only). Their live renewal setup is unchanged since Oct 20. What Clara learned fills the scattered homes, Courtyard and most of Pomona Park. Northridge, 239 S Elm, 1009 Colorado, one Pomona Park home, BCIE, Situs GJ and Ember stop with nothing written and need their own learned recipe. Plan for the week of Oct 26: Prepare rung at Western Slope (Clara fills, office presses Send). Two gates are Fede's: one park and cancel on their own form at their test property, then Renewals on and "Clara prepares, office sends".
12 · Risks
| Risk | Kind |
|---|---|
| Parked offers may show in the tenant portal or trigger AppFolio's reminders | Unverified (section 11) |
| Removing parts per offer may need property setup | Unverified |
| A new-lease draft has no clean undo: a cancel leaves the new rent charge | Known |
| The occupied-unit dialog has an irreversible option | Known |
| "Today" in UTC would misdate evening amendments (in the robot's code today) | Known; step 0 fix |
| Thin history: 11 previews, with slips (a voucher rent, a one-day typo, a renewal hidden inside a regular lease) | Known |
| A 4,000-a-day read budget, a past rate-limit storm, a stale cookie copy, a 3-minute heartbeat margin | Operations |
| Whether one new value counts as a "settings change" under the Sep 13 freeze | Decision (assumption) |
| Rungs per company versus the Oct 8 one-mode ruling: one engine and one path; only who presses Send differs | Decision |
| Western Slope's templates are "still an open item" for them | Customer |
13 · Decisions (Fede, Oct 21, 7:24–7:26 AM)
Decided: Fede saved all eight answers on this page. Seven follow the recommendation. On Camellia he chose (b): it moves onto the one engine inside the MVP, no old logic kept, proven at the Willows first. He also asked to start with the refactor and the robot cleanup, and for AI reasoning as a Claude Code skill for now.
1The design.
"Let's go ahead and start the build. In general I like it. We should start with the refactor and the cleanup and moving logic out of the robot."
He is "very happy overall with the plan", especially taking the business logic out of the robot, decoupling the robot from AppFolio, and calculating the fields elsewhere.
Fede's voice note, Oct 21 morning, as relayed in two messages through the Trinity DM
2The paper gate.
3Where AI reasons.
Fede's ruling, as relayed from his voice note (the relay's words, not a direct quote): lower priority than getting the pieces correct. For now: build the way to extract the examples from past packets; the AI reasoning over those is a Claude Code session, as a skill. He onboards customers manually for now, learns, makes it repeatable; a real end-to-end onboarding flow comes later (around when Situs Group gets there), possibly inside Gera's ingest. Do not build the automatic onboarding capability now.
What this means: no in-app onboarding flow now; the deliverables are the extraction of examples from past packets plus a Claude Code skill that reasons over them into a release.
4The four robot fixes. We ship the four robot fixes as separate small PRs this morning unless you say hold (recommended): terms check before sending a found parked offer; heartbeat expiry 15 → 30 minutes; today's date in the property's time zone; screenshot route locked. Alternatives: fold them into PR 1, or wait for the full contract.
5Your per-offer hold (hub section 10, D8) and Supervised (hub section 10, D10) for paper.
6Camellia.
"I don't think we should leave old logic. We should clean up and refactor everything including Camellia and then make sure Camellia still works with the same framework. Prove it at the Willows."
"And I want Camellia to be part of the MVP."
Fede's voice note, Oct 21 morning, as relayed in two messages through the Trinity DM
What this means: Camellia moves onto the one engine as part of the MVP, no old path kept; the byte-identical rebuild proof (P8) is inside the MVP and runs at the Willows before Camellia switches.
7Western Slope's six typed blanks.
8The MVP cut.
Your question: a chosen subset of properties?
"not all properties will be enabled, do we need a chosen subset of properties eligible for the renewal engine?"
Fede's voice note, Oct 21 morning, as relayed through the Trinity DM
Today the who-sends-the-paperwork answer is read once when each renewal starts, building first, then company: a building's own row wins, a building without one follows its company's row, and a building with neither means "their team", so Clara writes nothing into its PMS (renewals as a whole can also be switched off per building or company). So no separate list is needed: the chosen subset is the buildings whose answer says Clara, and the design keeps exactly that shape, adding only the value "Clara prepares, office sends" to the same rows.
Not re-asked here
D13 is Fede's Oct 20 ruling; D14, D16, D17 and D20 stay as recommendations on the hub (section 11) and are not decided by this page.
- D13: the first fill is on a one-unit test property inside Western Slope's AppFolio; their OK is asked Oct 21 (Fede ruled Oct 20).
- D14: which documents: copy the office; Northridge and Section 8 stay with the office in v1.
- D16: texts at Western Slope wait for paper, then aim at the tenants who have not signed.
- D17: the renewal list replaces the Monday board, seeded once.
- D20: simplify first: the section 12.5 setup changes, on their OK, before the first packet.
- Moving down a rung is Fede's press only (both designers agree).
- The ZZ TEST template and its six fields stay in JP&Co's library as the Willows canary unless Fede says delete.
14 · New leases on the same engine (decided by Fede, Oct 21, ~10:00 AM)
Decided: new leases come back on the one engine. The office approves in AppFolio; Clara prepares the lease where that customer has it on. Level 1: Clara prepares, the office checks and sends. Level 2: the approval sends it. Off by default for every customer. Four small questions stay open (decisions 10 to 13).
A new lease is the same job as a renewal: get the data, prepare the contract, send it. Same engine, same ladder. Only AppFolio's path differs, and it brings three traps: the unit, the prorated charges, rent on the ledger.
The ladder, in Fede's words
One rung per customer; only Fede moves it.
| Rung | What happens | Setting today |
|---|---|---|
| Off | Nothing | Leasing off |
| Shadow | Past approvals rebuilt and compared with the office's leases; the Willows. Writes nothing | None |
| Prepare | The office approves; Clara fills every field and parks the lease; the office presses Send | Leasing Live, "Send leases for signature" off |
| Send | The office approves; the lease goes out | The same, switch on |
Fede's level 1 is the Prepare rung; level 2 is Send. Today Clara drafts only after a click on our review card (Gera, Oct 15); decision 9 removes the click. The "Setting today" column becomes one new company setting (below).
AppFolio's path and its three traps
Convert copies the tenant and unit from the application and cannot be reversed; leaving Lease Details posts the deposit. So everything is decided before Convert, and the park stops before Send to Tenant. These are checks and standing answers in the release, never robot rules.
| Trap | What AppFolio does | Checked before any write | Proved after |
|---|---|---|---|
| The right unit | Takes the unit from the application, which staff can change (Joanna, Aug 20). Stops, or asks, when the unit has a current, future or hidden tenant or a set move-out; a posted charge locks it | The unit, re-read at prepare time, is empty; a placeholder move-in counts as occupied (Darrin, Jul 16). Else blocked | The prepared lease names that unit. Else unclear-state, held for us |
| Prorated charges (charged twice, Jul 16) | Prorates the first month on a 30-day month unless the database says actual days; rounding is a database setting | Camellia's rule: start the 1st after move-in, end on a month end (office, Aug 6); their proration method; the free month as an addendum plus a later credit. No confirmed rule: an ask | The payment schedule shows the order's stub and amount; the lease, the same dates. Else unclear-state |
| Rent on the ledger (no rent, Aug 13) | Posts rent on the 1st from the lease start, never in the move-in month; a wrong start date skips a month | Rent above zero, from the application or the confirmed page; accounting codes and deposit from the release. Else refused | At Prepare: the schedule and deposit line. After Done: the rent line's dates, and a first-of-month check that every occupied unit is charged rent. A miss alerts us |
The ledger check's control is the Sep 1 audit's seven misses: a first month skipped (about $1,000) and six move-ins at $0 rent (about $11,800, estimate). It must flag all seven.
9New leases: the go, and what starts the prepare.
"Bring back and extend." The office approves in AppFolio and we watch for that event; it "doesn't have to be instant". Build the lease packet "if that's enabled" for that customer.
"Part of the leasing module… separate from renewals, but I hope it's the same engine." "Everything shipping off by default for new customers."
Level 1: "we go prepare it and do everything and then just not send it yet… the office still has to go check it and send it out." Level 2: "clicking approve just sends it out." Tiers later: "doesn't matter".
Fede, by voice, Oct 21, about 10:00 AM
What this means: one engine for renewals and new leases, with a separate setting per kind. Off everywhere until Fede turns a customer on. Level 1 needs no new ruling. Level 2 waits on decisions 11 and 12.
What we already built
We built almost all of this once. On Sep 6 Clara drafted the lease in AppFolio as soon as the office approved, stopped before sending, and emailed the office. That was level 1. On Oct 15–16 it was retired: drafting moved behind a click on our review card, and nobody has clicked since. Clara sent one real lease, with the congrats text and email, once, at the Willows on Aug 26. No customer has ever had a lease drafted or sent by Clara. Camellia has 10 approval cards nobody acted on. Western Slope and Situs never get a card, because a missing lease policy quietly switches them off.
| Verdict | Piece | Today | Next |
|---|---|---|---|
| Fold | Approval trigger | Live, every company; fires once | Add a state check so a missed approval is not lost |
| Replace | Approval card and its Slack alert | Live at Camellia (10 waiting) and the Willows; silent skip at Western Slope and Situs | The print order becomes the record. Deleted once level 1 is proven |
| Fold | Dates and proration math | Live, on the card only | Reused. A confirmed rent replaces the estimate; a missing policy reads "blocked" |
| Fold | Draft step and its refusal readers | Behind the card; 10 Willows drafts, Sep 6–7 | Its refusal readers become the engine's answers |
| Delete | Draft on approval, no click (Sep 6's level 1) | Retired; nothing calls it | The engine rebuilds it, reading before and after. Its helpers move in |
| Keep | Office "lease ready" email | Retired with the draft | Revived as level 1's office note |
| Replace | Send for signature and its yes/no switch | Dark, 0 switch rows; sent once, Willows, Aug 26 | The engine's "send the parked lease"; the switch folds into the new setting |
| Keep | Leasing gate (Off, Supervised, Live) | Live | Stays the ceiling: Off still means nothing |
| Keep | Congrats text and email | Live behind the send; sent once | As is, level 2 only |
| Delete | Cancel check and the "change a sent lease" lane | Never proven; 0 records | The engine's own read of the lease replaces them |
| Replace | Unit check at capture ("Available On") | Live; that robot route can trigger a full login | Read occupancy instead: the occupant caused the Sep 7 refusal |
| Fold | Robot lease form and convert chain | Its own defaults: $200 deposit, codes 229 and 83, late fee, insurance box ticked, rent not prorated | Every value comes from the print order; the defaults are deleted |
Open the full per-file table (32 rows)
State words: live = runs in production today; dark = built, switched off; retired = no production caller; never proven = never ran end to end on a real lease; dead = runs, has never produced anything.
| Piece | Files | Built | State | Verdict | Why |
|---|---|---|---|---|---|
| The app | |||||
| Approval trigger | pms/writers/rental-application.ts, sync handler.ts | #6078 Aug 22, #6172 Aug 23, #6358 Aug 26 (Fede's sessions) | Live, every company | Fold | Keep the status mapping; add a state check so a missed approval is not lost |
| Approval card capture | new-lease/on-application-approved.ts, new-lease-package-capture.ts | #6078, #6358 (Fede's sessions); #20186 Oct 16 (Gera) | Live at Camellia (10 waiting) and the Willows; silent skip at Western Slope and Situs | Replace | The print order becomes the record; the card is the click Fede's flow removes |
| Package and lease math | approved-applicant-package.ts, lease-terms/, lease-wizard-dates.ts | #6358, #6726, #6728 Aug 26–31 (Fede's sessions) | Live (card only) | Fold | Reuse the date and proration math; a confirmed rent replaces the estimate; a missing policy is a visible "blocked" |
| Review card and approve button | /review page, new-lease-review-rows.ts, api/new-lease-package-reviews/[id]/decision/route.ts | #6078, #6162, #6349, #6759, #6763 Aug 22–Sep 1 (Fede's sessions); Oct 16 (Gera) | Live, never clicked at a customer | Delete after level 1 is proven | Today's only path, so it stays until the engine replaces it |
| Slack alert per card | new-lease-review-alert.ts | #6723 Aug 31 (Fede's sessions) | Live | Delete with the card | Replaced by the office note |
| Draft step | execute-lease-draft.ts | #6358, #6757, #7202 Aug 26–Sep 7 (Fede's sessions) | Live behind the card; 10 Willows drafts Sep 6–7 | Fold | Its refusal readers become the engine's answers; the robot call becomes the print order |
| Draft on approval (no click) | auto-draft-on-approval.ts (910 lines) | #7178 Sep 6, #7202, #7221 (Fede's sessions); retired Oct 15–16 (Gera) | Retired | Delete the draft function; keep its helpers | It was level 1; the engine rebuilds it, reading before it writes and after |
| Office "lease ready" email | new-lease-draft-notify.ts | #7178 Sep 6, #18115 Oct 9 (Fede's sessions) | Retired | Keep, revive | Level 1's office note; already goes through the staff-notice door |
| Send for signature | tools/handlers/handle-send-new-lease.ts; robot run-new-lease-send | #6142, #6349 Aug 23–26 (Fede's sessions) | Dark (0 switch rows); sent once, Willows Aug 26 | Replace | Level 2's send becomes "send the parked lease" |
| "Send leases for signature" switch | lease.send_for_signature, lease-send-switch.ts, admin row and route | #20153 Oct 15 (Gera) | Dark, 0 rows | Replace | Folds into the one three-step setting; nothing to migrate |
| Leasing gate | new-lease/new-lease-gate.ts | #13544, #12260, #13703 Sep 29; Oct 15–16 (Gera) | Live | Keep as the ceiling | Off must still mean nothing; its send half folds into the new setting |
| Congrats text and email | new-lease/congrats-message.ts | #6078, #6142, #6349 Aug 22–26 (Fede's sessions); #12260 Sep 29, #21233 Oct 20 (Gera) | Live behind the send; sent once | Keep as is (level 2 only) | Good copy, no amounts, sent after the lease |
| "Where's my lease?" answer | lease-signing-status.ts, prospect page | #6199 Aug 24 (Fede's sessions) | Live (reads); "Viewed" read but never written | Keep, re-point | Should read the order's state |
| Cancel check | lambda/appfolio-sync/new-lease-cancel-signal.ts | #6359 Aug 26 (Fede's sessions) | Never proven: the only sent lease is at the Willows, which that job skips; no log line in 30 days | Delete | The engine reads the order's state through the robot's new read contract |
| Countersign reminder | lambda/appfolio-sync/lease-countersign-signal.ts | #4297 Jul 22 (Fede's sessions) | Live; ran Oct 15, refused where the session needed a sign-in | Keep as is | Shared with renewals; works after either level |
| "Change a sent lease" lane | change-review-capture.ts, capture-from-escalation.ts, change-execution.ts, api/new-lease-change-reviews | #6174 Aug 23 (Fede's sessions) | Dead: 0 records; cancel never built | Delete | Nothing uses it |
| Move-in date rebuild | recapture-stale-package.ts | #6728 Aug 31 (Fede's sessions); #20189 Oct 16 (Gera) | Live (waiting cards only) | Fold | The engine re-reads the terms right before it prepares |
| Unit check at capture | check-unit-availability-unknown.ts | #6738 Sep 1 (Fede's sessions) | Live | Replace | The occupant, not "Available On", caused the Sep 7 refusal; that route can trigger a full login. Read occupancy |
| No card once move-in started | capture and writer | #20394 Oct 16 (Fede's sessions) | Live | Keep | The state check uses the same test |
| Cards never expire | review rows | #10978 Sep 23 (Gera) | Live | Fold, with a move-in rule | The engine acts only while the move-in date is ahead |
| Lease template per building | new-lease-template-chain.ts | #16153 Oct 3 (Gera) | Live | Keep | Readiness reads it; a missing template means "blocked" |
| Human-in-the-loop guard | human-gate.drift.test.ts | #6358 Aug 26; changed Sep 4 to allow a draft (Fede's rulings) | Live test | Amend for level 2 | Fede's own hard block; only his ruling changes it (decision 11) |
| Test move-in tool | tools/appfolio.ts appfolio.move_in_tenant | Robot-era admin tool | Test only | Keep | Willows clean-up; never for a customer |
| Two harnesses | scripts/new-lease-draft-harness/, scripts/willows-approved-applicant-harness.ts | #7178, #7200 Sep 6–7; #20089 Oct 15 (Gera) | Stale for this path | Fold / keep | The 17 draft scenarios become the engine's test set |
| The robot | |||||
| Lease Details form | lib/runMoveIn.ts | #290–#297, Aug 23–26 | Live route, with its own defaults | Fold | Every value from the print order; defaults deleted (robot exit step 7) |
| Convert-to-lease chain | lib/runNewLeaseFromApplicationL4.ts | #297 Aug 26, #302 Aug 31, #322 Sep 6, #354 Sep 10 | Behind the card; refusals proven at the Willows | Replace | Becomes the engine's new-lease verbs; keep the refusal readers; the new occupied-unit dialog answers "blocked" |
| E-sign sender | lib/runNewLeaseSendForSignatureL4.ts | #290–#297, Aug 23–26 | Mock-proven; one live send, Aug 26 | Replace | Becomes "send the parked lease"; drop the advertised-rent fallback |
| Lease reads | lease-document-status, fetch-lease-pdf | Aug | Live | Keep | The read-back source |
| Countersign list | list-leases-ready-to-countersign | Jul | Live | Keep | Shared |
| Willows test tools | run-create-rental-application (property 45 only), scripts/create-willows-synthetic-application.ts | Sep | Test only | Keep | Step one of the Willows proof |
| New-lease building allowlist | lib/newLeasePropertyAllowlist.ts | #298 Aug | Live guard | Delete once the engine's gate covers every path | Already decided (section 9) |
| Application link sender | run-send-rental-application | Aug | Live | Keep | Out of scope |
Co-applicants: one lease per unit (Oct 23, closed, live-proven)
Co-applicants file separate applications for one lease, and each approval starts its own run. Now a run holds, with no review card, when another application on the unit already has its move-in started, or when a lower-numbered approved co-applicant builds the lease. An application that already ran under the old run name does not run again (#22035, merged as #22052, no switch).
| Camellia, read-only replay (36 approved applications) | Before | After |
|---|---|---|
| Leases that would park | 9, two on one unit (applications 113 and 114, one household) and one beside a household member already converted (73 next to 72) | 7, one per lease: 114 and 73 held with a plain reason |
| Willows applications that already ran (130 to 133) | would start again under the new run name | "already ran", nothing starts |
Live at the Willows, 2026-10-23 17:06–17:08 UTC. Two test applications were approved on unit 102 four seconds apart, both for a January 1, 2027 move-in. One sync tick started a run for each, with nobody pressing anything. One lease parked; the other held with a plain reason, no review card and no office note.
| Step (two approvals, one unit) | Before | After | Proof |
|---|---|---|---|
| Approve | no applications for January | applications 134 "ZZTEST CoApp G" and 135 "ZZTEST CoApp H", unit 102, move-in 01/01/2027, both Approved | made through the robot, 17:06:53 and 17:06:57 |
| Sync sees both | no cards | cards 538b808a (134) and 9d247fa2 (135); "New-lease packet started" for each | sync log 17:07:08 and 17:07:09 |
| 134 (lower id) builds | not converted | parked: occupancy 1106, lease 1565, lease document 909 (document 6945994), 01/01/2027 to 12/31/2027, $1,500 rent, $300 deposit, not sent | Temporal run 01a169bb-0c01-7cbd-8bd5-8fac225ce381: read event 13, park event 19, record 25, note 31 (noted) |
| 135 rides on it | would park a second lease on the same unit (Camellia 113 and 114) | held: "Clara did not build the new-lease packet: application 134 is a co-applicant on the same unit and builds the one lease, so this application rides on it." | Temporal run 01a169bb-0eca-76aa-9aa8-f17d11c4e13d: build event 7 held, no robot call, note event 19 none; robot read after: 135 still approved, not converted |
| Review card | a held run could open one | none since the run started (the same read finds the two older Willows cards) | new-lease review rows at appfolio-45 since 17:06:33: 0 |
| Office note | one per parked lease | exactly one: "New lease ready to send: ZZTEST G. at The Willows" | sandbox leasing inbox 17:07:40; nothing for H |
| Clean up | 134 parked; company on prepare | company back on team at 17:08; cancel done (occupancy 1106 hidden, zero balance, 134 unconverted); 134's row corrected to held; 135's held row kept; recipe out of use; Willows inbox back to its sink | robot cancel read the same ids before; ledger |
What this run proves: the no-group path (neither application has a co-applicant group, so the same move-in day makes them co-applicants and the lower id builds). The same-group path is proven on the Camellia replay only; the robot cannot make a grouped test application. The move-in was set to January 1 instead of December 1 so it sits 61 days from the Nov 1 test applications 128 to 133, outside the 31-day "move-in already started" window. Both approvals landed in one sync tick, so the order question (#22053) was not exercised. Open follow-up: which co-applicant builds depends on the order the approvals arrive (#22053).
Camellia practice round, Oct 23 Lease parked unsent, office told
Fede said go ("ok go ahead with the practice round", then "Go."). Rerun at 7:36 to 7:38 pm MDT, after the robot's Willows-only rule came out (robot live on that merge, checked on its health page first). Clara prepared the new lease for Camellia application 126 in AppFolio, left it unsent, and emailed the office. Nobody pressed anything.
| Before (this morning) | After (7:38 pm) | |
|---|---|---|
| Application 126 | Approved Oct 16, "Convert To Tenant" open, no lease | Converted by Clara: a future tenant on unit 124 (occupancy 1108), move-in started and stopped at "Generate Lease" |
| The lease | None | Lease document 913 on the Camellia Lease form, Nov 1, 2026 to Oct 31, 2027, $1,200 rent, $300 deposit. Not sent: the office presses Send |
| Ledger | Nothing | $300 deposit charged. Rent posts once the office finishes the move-in |
| The office's inbox | Nothing | "Lease prepared for <applicant>, unit 124", delivered 7:38 pm, unread |
| PropFlow | No review card for 126 | Still no card. The prospect page shows "parked" with the AppFolio ids |




The note, as sent (name replaced; to the Camellia office address, hello@ on a hidden copy; SendGrid message vdmi9pWIRtO6-lsz8i-6Fg):
Subject: Lease prepared for <applicant>, unit 124 <applicant>'s new lease is filled and waiting for your Send You approved <applicant> for unit 124. I filled the new lease in AppFolio and left it unsent. What I filled Term Nov 1, 2026 to Oct 31, 2027 Rent $1,200/mo Deposit $300 Open the lease in AppFolio (jpco.appfolio.com/occupancies/1108) Nothing goes to them until you press Send in AppFolio. Clara
No Documents row: the change that adds it is not merged yet.
| Write (in order) | Ids |
|---|---|
| 1. Recipe version 2: version 1 plus the rent charge description "Rental Income", marked assumed until the office answers question 1. Then put in use | DOCRELEASE#new-lease#camellia-new-lease#V#00002, pointer …#ACTIVE → 2 (revision 1). Version 1 still plans "held" on the description; version 2 plans ready |
| 2. JP&Co's new-lease setting: team → prepare (company level) | 01:36:55 UTC |
| 3. Run started for application 126 | new-lease-packet-org_jpco-a8c1984a-2d40-5dd2-adb4-b2240118cde1-application-126, run 01a16b8e-0e91-77e5-879f-ffb128752aca: build event 7, read 13 (second try), park 19, record 25, note 31 (noted), result parked |
| 4. The run's own writes in AppFolio | occupancy 1108, move-in 1697, lease 1567, lease document 913 (document 6951409), not sent |
Monday morning: the office finds the note unread in their inbox. Opening the link shows the move-in at "Generate Lease". They check the lease and press Send, or cancel it. Correction (QA pass below): AppFolio has no Send button for a parked document. Left as Fede approved: JP&Co stays on prepare and recipe version 2 stays in use. So every new Camellia approval from now on gets a parked lease and a note, with no click in PropFlow (old approvals do not start runs). The same setting covers Yale 25 Station, which has no property software connected, so nothing starts there.
Not to plan, or worth knowing:
- The robot's first read try failed and its second passed 31 seconds later. The history does not say why.
- The "Rental Income" description went in on the robot's order, but nothing reads it back yet. Rent posts, and so becomes visible, only after the office finishes the move-in.
- Unit 124 also has application 120: approved, never converted, move-in Oct 2. Clara correctly ignored it (different move-in, not a co-applicant). The office may want to close it.
- The robot screenshot route was not used. The CLAUDE.md prefers warm-session page reads (GET only, no login) for look-only work, so these screenshots come from that path, cropped to hide the name.
QA pass on the parked lease, Oct 23, 8:20 pm Terms right, the hand-off is broken
Read only: warm-session page reads, GET only, no sign-in page, no budget errors, about 12 reads. Compared with the office's own move-ins at the same stage: occupancy 1088 (lease 885, sent, waiting for the tenant to sign, Nov 1 start) and occupancy 1090 (lease 883, move-in finished, Nov 1 start).
| Check | Expected | Found | |
|---|---|---|---|
| Move-in steps | Profile, Select Unit, Lease Details done; Generate Lease next | Profile, Select Unit, Lease Details ticked. Generate Lease current. Additional Tenants and Resident Onboarding not ticked (optional steps; a converted application starts at Lease Details) | OK |
| What "Generate Lease" is | The step where the lease is made and sent | AppFolio clears it only when a lease is sent (then countersigned), uploaded signed, or skipped. A made-but-unsent lease leaves it pending. That part is on purpose | OK |
| Is 913 the lease this step uses? | Yes | Yes, linked to this move-in and to lease 1567. But the page hides it: the "Lease Status: Not Sent / View Lease" panel is in the page and kept hidden; AppFolio only shows it after a send | Gap |
| How the office sends 913 | A Send button, as our note says | None. The lease preview offers only Print to PDF and Cancel Lease (AppFolio shows Resend only once a lease is out for signing). The Leases page has no Not Sent tab. The office's only path is Prepare Lease, which opens a new lease. AppFolio's "a lease already exists" block ignores unsent leases, so Send there makes a second lease and leaves 913 behind | Gap |
| Anything sent to the applicant | Nothing | No emails, no texts, portal never invited (the office's 1088 shows four lease emails) | OK |
| Lease 1567 terms | Nov 1, 2026 to Oct 31, 2027, $1,200 | Same. One lease, one document | OK |
| Rent vs unit 124 | Listed rent | Market rent $1,200 (since Aug 2024), advertised $1,200 | OK |
| Deposit | $300 for 0 to 1 bed | Unit 124: 1 bed, 1 bath, 540 sq ft; unit's own deposit $300; lease $300 | OK |
| Deposit charge | $300, deposit account, posted at Lease Details | $300 balance on the occupancy, same as the office's 1088. Account and due date not readable by a GET (the ledger report has to be started) | OK (amount) |
| Monthly rent charge | $0 until the move-in is finished | $0, no rent charge yet. The office's 1088 shows the same at this stage; 1090 shows it after finish: $1,000, Rent Income (4410), "Rental Income", from Nov 1. Our $1,200 / "Rental Income" entry is not readable until finish | OK (as office) |
| Late fee | Camellia's usual | "Set by the property": flat $0.00, posts after 1 day. Same on 1088 and 1090. AppFolio posts no late fee at Camellia for anyone; the $50 seen at move-out is typed by hand | OK |
| Proration, move-in fees | None for a 1st-of-month start | None on the move-in; the unit lists no required fees | OK |
| Lease form and addenda | Camellia Lease, no addenda | Camellia Lease only; autofilled start, end, rent $1,200, deposit $300, unit 124 | OK |
| Typed boxes | As the office types them | All 7 blank. The office types total rent (12 × rent, here $14,400) on 18 of 19 leases that start on the 1st, and 0 in the part-month boxes on 15 of 19 | Gap |
| Application 120 (same unit) | Closed or denied | Still Approved, desired move-in Oct 2, "Convert To Tenant" still offered. Not Clara's; with a future tenant on 124 now, converting it would collide. The office should deny it | Office |
Decided (Fede, Oct 24, Trinity DM): "For new leases we are fine with just preparing." Clara prepares the new lease in AppFolio and leaves it unsent; the office sends. The missing Send button on a Clara-prepared lease is not treated as a problem. Camellia's renewal sending is unchanged and was never part of this question. The options below are closed and kept for the record.
Plain answer. Stopping before the send is on purpose: JP&Co is on "prepare". But AppFolio has no button to send a lease that was made and left unsent, and the move-in page does not show it. Pressing Prepare Lease, the office's natural step, builds a second lease. Options considered (closed):
- Recommended: at "prepare", stop after the lease terms and make no document; the note says "Continue Move In, Prepare Lease, Send" and lists the terms and the total-rent box to type.
- Keep 913; the note says "cancel the parked lease, then Prepare Lease and Send".
- Find an AppFolio route that sends an existing unsent lease (none found in its pages or code).
Whichever: type the total-rent and part-month boxes the way the office does (recipe follow-up), and fix the note's "press Send" line.
The office note, fixed and proven at the Willows (Oct 24) Live everywhere, no switch
Fede, Oct 24: "Too verbose." Two fixes merged and reached production the same day: the note leads with one line and the lease button sits right under it; the "What I filled" table names the documents the park made. A real Willows run proved both: the test application was parked (occupancy 1109, lease document 915) and the team inbox received this note. Everything was cancelled and the Willows left as found.

| Before (Camellia practice note, Oct 23) | After (Willows run, Oct 24) | |
|---|---|---|
| Lead | Two sentences: "…new lease is filled and waiting for your Send" and "You approved … I filled the new lease in AppFolio and left it unsent." | "Lease prepared in AppFolio, not sent. Review and send for signature." |
| Button | After the table | Right under the lead |
| Footer line | "Nothing goes to them until you press Send in AppFolio." | Gone |
| Documents row | None | "Documents: Camellia Lease" (AppFolio's own template name) |
Camellia new-lease recipe (read-only learn, Oct 23) Version 2 in use (Oct 23 practice round)
We read Camellia's 33 office-finished new leases (Jun 24 to Oct 23, 2026) through the warm session, GET only. That was about 41 paced reads, with no sign-in page and no budget errors. From them we wrote a recipe. Ids only on this page; the applicant boxes typed on each form were never copied here.
| Blank | Where the value comes from | How often the past leases agree |
|---|---|---|
| Lease form | Fixed: template 408767 "Camellia Lease" | 33 of 33 |
| Start | The move-in day, which must be the 1st. A mid-month move-in is held for the office | 33 of 33 start on a 1st; 19 on the move-in day |
| End | 12 months, ending the day before the start's anniversary | 28 are 12 months, 5 are 6 months; 31 of 33 end on a month end |
| Rent | The unit's listed rent | 31 of 33 |
| Deposit | Camellia's policy: 300 for 0–1 bed, 400 for 2+ | 31 of 32 that print one |
| Rent account | Fixed: 229 (000-4410-000 Rent Income) | 27 of 27 rent charges, on 26 leases (777 has two; 7 have no charge yet) |
| Deposit account | Fixed: 83 (000-2171-000 Security Deposits), the only deposit account on the form | Not readable per lease (the ledger report is async); only option offered |
| Rent charge description | Asked. "Rental Income" leads but is under the 80% bar | Rental Income 16, blank 7, Rent 4 |
| Addenda | None. The engine has no addenda slot | 0 of 33 carry the Rental Concession Addendum 464793 |
| Typed boxes (part-month dates, part-month amount, total rent) | Left blank. The engine does not type them | Filled by hand on the 14 mid-month leases |
Replay. We ran the draft over all 33 past leases and compared it with what the office typed. A bold value is the office's; the arrow points to the engine's.
| Field | Same | Different | Not applicable |
|---|---|---|---|
| Start | 19 | 14 (mid-month, the office prepares) | 0 |
| End | 26 | 7 (5 six-month, 2 end on the 30th) | 0 |
| Rent | 31 | 2 (882, 694) | 0 |
| Deposit | 31 | 1 (761) | 1 |
| Rent account | 26 | 0 | 7 (no charge yet) |
| Lease form | 33 | 0 | 0 |
The engine would prepare 19 of the 33 (the move-in-on-the-1st ones). 14 of those 19 match the office on every field. The other five are 722, 749 and 753 (6-month terms), 761 (deposit) and 882 (rent). Planted checks all went red: ending one day early → 31 ends differ; the Willows deposit of 200 → 32 deposits differ; rent account 848 → 26 differ.
All 33 leases, one row each (document id, ids only)
| Doc | Move-in | Start | End | Rent | Deposit | Rent acct | Form | Engine |
|---|---|---|---|---|---|---|---|---|
| 694 | 2026-06-15 | 2026-07-01 → 2026-06-15 | 2027-06-30 | 1150 → 1000 | 300 | — | 408767 | holds (office prepares) |
| 722 | 2026-07-01 | 2026-07-01 | 2026-12-31 → 2027-06-30 | 1000 | 300 | — | 408767 | prepares |
| 723 | 2026-07-01 | 2026-07-01 | 2027-06-30 | 1600 | 400 | — | 408767 | prepares |
| 743 | 2026-07-27 | 2026-08-01 → 2026-07-27 | 2027-07-31 | 1500 | 400 | 229 | 408767 | holds (office prepares) |
| 748 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1200 | — | 229 | 408767 | prepares |
| 749 | 2026-08-01 | 2026-08-01 | 2027-01-31 → 2027-07-31 | 1200 | 300 | 229 | 408767 | prepares |
| 753 | 2026-08-01 | 2026-08-01 | 2027-01-31 → 2027-07-31 | 1000 | 300 | 229 | 408767 | prepares |
| 761 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1500 | 300 → 400 | 229 | 408767 | prepares |
| 780 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1200 | 300 | 229 | 408767 | prepares |
| 788 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1000 | 300 | 229 | 408767 | prepares |
| 792 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1200 | 300 | 229 | 408767 | prepares |
| 793 | 2026-08-01 | 2026-08-01 | 2027-07-31 | 1600 | 400 | 229 | 408767 | prepares |
| 754 | 2026-08-10 | 2026-09-01 → 2026-08-10 | 2027-08-31 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 777 | 2026-08-12 | 2026-09-01 → 2026-08-12 | 2027-08-31 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 794 | 2026-08-15 | 2026-09-01 → 2026-08-15 | 2027-08-31 | 1200 | 300 | 229 | 408767 | holds (office prepares) |
| 776 | 2026-08-20 | 2026-09-01 → 2026-08-20 | 2027-02-28 → 2027-08-31 | 1200 | 300 | 229 | 408767 | holds (office prepares) |
| 802 | 2026-08-24 | 2026-09-01 → 2026-08-24 | 2027-02-28 → 2027-08-31 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 806 | 2026-08-31 | 2026-09-01 → 2026-08-31 | 2027-08-30 → 2027-08-31 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 799 | 2026-09-01 | 2026-09-01 | 2027-08-31 | 1000 | 300 | 229 | 408767 | prepares |
| 861 | 2026-09-01 | 2026-09-01 | 2027-08-31 | 1000 | 300 | 229 | 408767 | prepares |
| 864 | 2026-09-01 | 2026-09-01 | 2027-08-31 | 1000 | 300 | 229 | 408767 | prepares |
| 865 | 2026-09-01 | 2026-09-01 | 2027-08-31 | 1200 | 300 | 229 | 408767 | prepares |
| 872 | 2026-09-01 | 2026-09-01 | 2027-08-31 | 1200 | 300 | 229 | 408767 | prepares |
| 863 | 2026-09-16 | 2026-10-01 → 2026-09-16 | 2027-09-30 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 868 | 2026-09-17 | 2026-10-01 → 2026-09-17 | 2027-09-30 | 1200 | 300 | — | 408767 | holds (office prepares) |
| 862 | 2026-09-18 | 2026-10-01 → 2026-09-18 | 2027-09-30 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 866 | 2026-09-25 | 2026-10-01 → 2026-09-25 | 2027-09-30 | 1000 | 300 | 229 | 408767 | holds (office prepares) |
| 874 | 2026-10-01 | 2026-10-01 | 2027-09-30 | 1200 | 300 | — | 408767 | prepares |
| 882 | 2026-10-01 | 2026-10-01 | 2027-09-30 | 1150 → 1000 | 300 | 229 | 408767 | prepares |
| 884 | 2026-10-23 | 2026-11-01 → 2026-10-23 | 2027-10-31 | 1200 | 300 | 229 | 408767 | holds (office prepares) |
| 911 | 2026-10-30 | 2026-11-01 → 2026-10-30 | 2027-10-30 → 2027-10-31 | 1000 | 300 | — | 408767 | holds (office prepares) |
| 883 | 2026-11-01 | 2026-11-01 | 2027-10-31 | 1000 | 300 | 229 | 408767 | prepares |
| 885 | 2026-11-01 | 2026-11-01 | 2027-10-31 | 1000 | 300 | — | 408767 | prepares |
Application 126 in a dry plan (in memory, nothing sent). With the draft as stored, it is held: the rent description is under the bar. Once the description is named, it is ready: Nov 1, 2026 to Oct 31, 2027, rent 1200, deposit 300, accounts 229 and 83, form 408767.
Questions for the Camellia office (where past leases disagree):
- Rent charge description: "Rental Income", "Rent", or blank?
- When does a lease get 6 months instead of 12? (722, 749, 753, 776, 802)
- Two leases end on the 30th instead of the month end (806, 911). Typo or rule?
- 761 printed a 300 deposit; its tenancy holds 400. Which is right?
- Is the "1 month free" special and the concession addendum 464793 still offered? No lease carries it.
- Must the part-month and total-rent boxes be typed on a lease that starts on the 1st?
- 882 and 694 were 1150 against a listed 1000. Was that a rent increase since, or a one-off?
What was written. One row in production, nothing else:
PK=ORG#org_jpco SK=DOCRELEASE#new-lease#camellia-new-lease#V#00001. Read back identical. No pointer (…#ACTIVE), no learned record, and the new-lease setting stays team. No code change and no PR.
Update, Oct 23 evening (Fede's go): all three blockers are cleared. The robot rule is gone. Version 2 assumes "Rental Income" until the office answers question 1, and it is now in use. Application 126 parked; see the practice round. When the office answers, store version 3 with their answer.
Engine gaps this exposed (follow-ups, not blockers for application 126): no addenda slot; typed boxes left blank; mid-month move-ins are held. The learner reads renewals only, both in the app and in the robot's history route, so this read ran as a local read-only harness. Teaching the learner new leases is a follow-up.
The congrats text and email
Keep the copy as is. The text (English or Spanish) says the applicant is approved, links the resident portal to "review and sign your lease", and ends "Congratulations, and welcome home!" The email has the same lines and a "Review & Sign Lease" button. No fee amounts anywhere. It fires only after a real send: lease first, congrats second.
| Level | The applicant | The office |
|---|---|---|
| Off | Nothing from Clara | Works as today |
| Level 1: Clara prepares | Hears nothing from Clara | Gets the "lease ready" note, checks and sends |
| Level 2: Clara sends | Gets the congrats text and email right after Clara's send succeeds | Countersigns, as today |
Proof: Willows, Aug 26, lease out at 18:23:37 UTC, congrats at 18:23:38. A congrats at level 1, once our read sees the office sent the lease, would be a new outbound: Fede's call, not in this cut.
The event: how we see an approval
| Step | What happens |
|---|---|
| 1. The office approves in AppFolio | Nothing on our side yet |
| 2. Within about 1 minute | The sync reads every application for that AppFolio database, one call per database |
| 3. Our record turns approved, first time only | The trigger fires |
| 4. What it makes | Today: an approval card. Next: one print order per application |
Speed is fine; reliability is not yet. The trigger fires once. If that step fails, the error is logged and never retried; the next minute sees "approved → approved" and does nothing. Also, "approved at" is when we first saw it, not AppFolio's time, so after an outage or a first connect every old approval looks new.
The fix: an hourly state check, plus a nudge from the trigger. It looks for any application that is approved, not converted, without an order, and moving in on a date that has not passed. Each gets exactly one order, keyed by the application. A missed trigger is caught within the hour.
The move-in-date rule. The move-in date is the age test, not "approved at". It keeps a first run at a customer away from approvals whose move-in has passed. AppFolio's own approval date would be better, if its report carries one (unverified).
The setting: one value, three steps
One new company setting, built in the shape of the renewal setting (who sends the paperwork). It replaces the unused yes/no send switch, which has 0 rows, so nothing migrates.
| Fede's words | Value | When the office approves |
|---|---|---|
| Off (the default, and every new customer) | team, also when unset or unreadable | Clara prepares nothing |
| Level 1 | prepare | Clara builds the lease in AppFolio, parks it "prepared, not sent", reads it back and tells the office. The office checks and sends |
| Level 2 | clara | The same, then Clara re-reads and sends. The congrats go right after |
- Company level only (Fede: "if that's enabled for that customer"). A building override can come later.
- Never reuse the renewal key. Camellia's renewal row says Clara, so a shared key would put Camellia's new leases at level 2 the moment the code lands. Same three words, separate key.
- Leasing Off stays the ceiling. The setting acts only at Live (decision 10).
- New customers are off twice: their modules start off, and the setting is unset.
Proposed key: newLease.paperworkSender. One entry in the existing registry and one admin picker under Leasing; no new settings registry.
Harnesses
| Harness | What it proves | Last proven | Gap for the engine |
|---|---|---|---|
| New-lease draft harness | 17 scenarios against a fake robot: nothing is sent, nobody is left uninformed. A live leg at the Willows: refusals and drafts against the real sync | 17 of 17 offline, Sep 6. Live, Sep 7: happy path passed; each of three failures was a finding, one fixed the same night | Drives the retired draft function, not the engine. Points at a robot checkout that no longer exists. No rent, deposit or ledger check. Its 3 controls guard the harness, not the product |
| Willows approved-applicant harness | Clara's answers to an approved applicant's lease questions, locked to the Willows and Fede's test person | Oct 15 | Never touches the AppFolio lease |
Neither runs today's path (the card click) or the engine's print order. The Willows path the engine needs:
| Step | What exists |
|---|---|
| 1. Create and approve a test application at the Willows | A robot script, dry run by default |
| 2. The sync sees it | Yes: the applications job runs every minute at the Willows too |
| 3. Readiness, read only | The occupancy read exists; the new occupied-unit dialog is not parsed. Vacant test units 372 and 382 qualify (Oct 20) |
| 4. Park "prepared, not sent" | Proven for renewals Oct 20; never for a new lease (that run stopped at Convert) |
| 5. Read back rent, dates, deposit, proration, no blank left | Built for renewals; not for new leases |
| 6. Cancel and clean up | No clean undo: Convert cannot be reversed and a cancel leaves the rent charge. A clean-up recipe is needed (move-out or void) |
Planted defects the proof needs: a wrong rent on the read-back, an occupied unit, a past start date, a blank left unfilled, proration that differs from the lease.
Traps already handled vs not
| Already handled: reuse, don't rebuild | Where |
|---|---|
| Drafting twice for one approval; a crash mid-draft | A claim marker keyed by the record id; resume from the recorded stage |
| An application already converted | Adopted, not converted again |
| An occupied unit (the old dialog) | Stops with a reason; re-drafts once the unit frees up (Willows, Sep 7) |
| A blank rent sent | The app skips a zero or unknown rent; the robot refuses a blank rent or a bad date |
| No term, a term not allowed, no deposit | Refused |
| The move-in date changes, or move-in already started | A waiting card is rebuilt; no card once move-in started |
| Leasing status unreadable | Skipped, never guessed |
| Congrats before the lease | Lease first, congrats after |
| Not handled | What we found | Engine answer |
|---|---|---|
| The new occupied-unit dialog | Since Oct 20 it offers two choices; one cannot be undone. The robot misses it | Readiness reads occupancy first (1 of 1 called ahead, Oct 20); the robot answers "blocked" and clicks neither |
| Dates differ from the card | The draft starts on the move-in day; the card uses the policy's rule | One date rule, from the policy, in the order; read back after |
| Proration | The robot drafts "not prorated"; the card shows a prorated first month | The order carries the proration line; the read-back checks it |
| Rent source | A market estimate, with a fallback to the advertised rent; neither is the approved rent | Readiness needs a confirmed rent |
| No ledger check after move-in | Sep 1 audit: 7 misses | A first-of-month check that every occupied unit is charged rent |
| Robot defaults; no read-back | Codes, $200 deposit, late fee, insurance box; only the e-sign state is read | Every value from the order; a rendered read-back |
| When the deposit posts | Help pages say leaving Lease Details posts it; never checked on a Willows ledger | Verify at the Willows before the first customer |
| No clean cancel | A cancel leaves the rent charge | A wrong draft is "unclear, held for us", never a blind retry |
| The trigger fires once; lease policy as a hidden switch | A failed step is lost; Western Slope and Situs skipped without a word | The hourly state check; a missing policy reads "blocked: no policy" |
The PR cut
Four PRs, each dark, mergeable alone and under about 600 lines. Level 1 needs no new ruling: drafting without a click has been allowed since Fede's Sep 4 change. Level 2 does, so it stays a local branch until decisions 11 and 12.
| PR | Waits on | What | Proof |
|---|---|---|---|
| 1. The setting (about 350 lines) | Nothing | The three-step company setting, its reader, an admin picker. Replaces the yes/no switch. No behavior change: no company has a value | Planted defects go red (unset reads Clara; the renewal key read by mistake); replay: every company reads "team" and the gate is unchanged; the picker at the Willows' sandbox company |
| 2. The new-lease print order (about 600 lines) | Engine PR 3; the robot's new-lease verbs | Readiness, assembly, park, read back, cancel. Moves the retired draft's helpers in and deletes it. Nothing calls it | Five planted defects; replay of the 32 recorded packages and the 17 scenarios; Willows: approve, park, read back, cancel, clean up |
| 3. Level 1: prepare (about 450 lines) | PR 2; decision 10 | The hourly check plus the trigger's nudge make one order per approval at "prepare"; the office gets the note; the card stops there | 30-day replay: zero orders for "team" companies; Willows at "prepare": a parked lease and the note within the hour; sandbox in Camellia's shape; then Fede turns it on per customer |
| 4. Level 2: send (about 300 lines) | Decisions 11 and 12; a local branch until then | At "clara": re-read, send, then the congrats. Changes the guard test | Planted defects (congrats before the send; a send without a re-read); Willows at "clara": congrats reach Fede's test person; sandbox; then Fede per customer |
Cleanup (any time, no decision): delete the "change a sent lease" lane, the cancel check, the "Available On" check at capture, and the harness's live leg. After level 1 is proven at a customer: delete the card, the approve route, the Slack alert and the "Handled in AppFolio" wording.
Robot (its own chain): new-lease verbs on the print-order contract: park (convert, Lease Details with the order's values, prepare, stop before send), read back, send the parked lease, cancel. The new occupied-unit dialog answers "blocked". The defaults are deleted. Later the allowlist and the old send route go.
Open for Fede and Gera
10Gera: does level 1 run at Supervised, or only at Live?
11Fede: your Aug 26 guard, "All human in the loop before anything goes out", must change for level 2.
12Fede: "approving sends it out" means the office's approval in AppFolio?
13Fede: Camellia's 10 old approval cards (Sep 4 to Oct 16).
14b · Office note: three options B chosen by Fede, Oct 23
The email the office gets when Clara has prepared a new lease and left it unsent. Real Camellia example: unit 124, the applicant (application 126), Nov 1, 2026 to Oct 31, 2027, $1,200 rent, $1,200 deposit, lease + 3 addenda. Each one is rendered through the production note wrapper and was sent to Fede on Oct 23 as "[Demo A/B/C]". hello@ gets a BCC copy in production; the demos went to Fede only.
Chosen by Fede, Oct 23: B. It answers "what did Clara fill" at a glance, and flags the one blank still open. A and C were not chosen. The production change is a separate PR.
| Option | Subject | Shape | Words |
|---|---|---|---|
| A · Minimal (not chosen) | the applicant (application 126), unit 124: lease ready for your Send | Three lines, one link to the lease in AppFolio | 41 |
| B · Structured (chosen by Fede, Oct 23) | the applicant (application 126), unit 124: lease ready for your Send | "What I filled" table, "Still needs you" line only when a blank is open, one button to the lease in AppFolio | 71 |
| C · Checklist (not chosen) | To do: check and send the applicant's lease, unit 124 | The office's three steps, what Clara does next, her PropFlow page in small print at the bottom | 56 |
A minimal, not chosen · B structured "What I filled" table with the AppFolio lease button, chosen by Fede, Oct 23 · C checklist, not chosen. Screenshots removed: they carried a real applicant's name; the chosen design is now the live template (PR 22118).
Demo only, nothing merged. The lease link is a placeholder in the shape of a real AppFolio lease link. "Still needs you" is shown once with an example blank; in production it appears only when a blank is open. In code the PMS name and the lease link come from the PMS adapter, never a literal. SendGrid ids: A J48jiitJTY66V1WWntiWYQ, B pZ_pBAbKTpua5j19sthRVw, C AVPZG-KGRx24DeaW33UvQg.
15 · What we can remove now (Fede's Oct 21 question)
About 76 files and 10,600 lines can go now, all in the robot. The app's renewal code is almost all live; the rest goes when the engine replaces it.
Every row was proven: nothing imports it and no script, workflow, doc, test, skill or app file names it. Anything Gera touched or reads goes to him first. Files the four robot fixes (now merged) touch were left out.
| Verdict | What | Size | When |
|---|---|---|---|
| Delete | Robot: 43 one-off probe, recon and proof scripts nobody runs | 43 files, 4,498 lines | Now |
| Delete | Robot: the old Situs data dump, retired by its own README in favour of the onboarding capture | 28 files, 4,995 lines | Now |
| Delete | Robot: the April feature-flag switchboard and shadow runner, and the old "set unit availability" path the newer one replaced, with their tests | 5 files, 1,026 lines | Now |
| Delete | App: one renewal-batch read nothing calls | 6 lines | Now |
| Ask Gera | Robot: the old move-out path, a cancel move-in runner, two helpers, 12 scripts. App: two routes no screen calls | about 3,000 lines | After his yes |
| Later | Robot policy (rent math, a customer field map, a property allowlist, a default account, two route lists). App: the global arms, duplicate building fields, the old policy blob, the change-review queue | about 760 robot lines plus about 30 app files | After exit step 7 or Camellia's move |
| Keep | The unused renewal adapter: the engine plugs into it (PR 2). The local renewal worker, the error taxonomy, cost and usage trackers, replay fixtures, the onboarding capture | Needed | |
| Unsure | A login-storm harness that never runs in tests, a new-lease orchestrator, 10 hand-run tools, 70 scripts named only in comments | One look each |
Open the per-file list
| Item | Path | Evidence |
|---|---|---|
| Feature-flag switchboard, shadow runner, their test | lib/featureFlags.ts, lib/shadowModeRunner.ts, tests/featureFlags.test.ts | Only importer is their own test; a planning doc already calls them "read nowhere". From #11. |
| Old set-unit-availability path and test | lib/setUnitAvailability.ts, tests/setUnitAvailability.test.ts | The route imports runSetUnitAvailabilityL4 instead; other mentions are comments. From #204. |
| Retired Situs dump | scripts/situs-dump/ | Its README: "Retired, use scripts/appfolio-onboard/". Nothing runs it. |
| 43 scripts | scripts/_*.ts and similar, mostly from the May renewal lifecycle work (#94) | No package.json script, workflow, script, doc, test, skill or app file names them. Four were kept: an app proof script names one, a README names three. |
| Unused batch read | getRenewalBatchDecisions in renewal-batch-pointer.ts | Only its own test calls it. From #18422. |
| Old move-out path (ask Gera) | lib/moveOutTenants.ts and two tests | Replaced by runMoveOutDraftL4, but one of Gera's tests still imports it. |
Full table with line counts, PR origins and the exact searches: the delete list from Oct 21.
16 · What to show Western Slope Proposed — pending Fede's review
Customer-facing version: /a/western-slope-renewals-with-clara
Engineering notes (for engineers; names, keys and paths live here)
Dark-features register line: Dark: renewal.paperworkSender = "Clara prepares, office sends" | feature: Clara builds and parks renewal packets, staff press Send | proof: Willows offer 370, Oct 20; Western Slope test property pending | turn-on: Fede
- Settings. The who-sends-the-paperwork setting is
renewal.paperworkSender(company and building rows; valuesclara|team; absent means team), in the app's portfolio registrykeys.ts(about L1349–1363). New leases keep ADR-0143'slease.send_for_signatureplus the building leasing status (Oct 15–16). - Step-0 fixes in the robot (line numbers from origin/main
be5aad7): found-offer terms check atlib/apiRenewalOps.ts:411-418(rule 6;lib/runRenewalL4.ts:131-133); store expiry to 30 minutes atlib/sessionJarStore.ts:153; property-local "today" for the renewal flow's date helper (it returned 2026-10-21 at 22:28 MT on Oct 20);screenshot-work-orderwithout profile writes or a caller profile id (app/api/screenshot-work-order/route.ts:126-137). - Contract. Verbs
read,park,send-parked,cancel. The print order id is the idempotency key, claimed in the robot's KV like texting sends; today'sIdempotencyCacheis in-process memory (30-minute TTL). Answer classes:done,blocked,refused,unclear-state,transport. - Timeouts and retries today. App 540 s for renewal calls; robot
run-renewalmaxDuration800 s. Temporal 5 attempts on renewal activities, SQS up to 4,withRetry2 on renewal writes: up to 40 prepare POSTs on paper (arithmetic, not observed). - Routes. 91 routes: 48 HTTP with the saved session, 19 HTTP from inside a live browser page, 9 screen-driving, 6 login and session, 9 with no AppFolio contact. Library 71,406 lines: 63.4% AppFolio operations, 16.5% session, 11.1% plumbing, 4.8% unreached, 3.2% harness-only, 1.1% customer data; scripts 49,574 lines.
- Outside practice. Browser Use's live benchmark: best 79.6%. Stagehand v4 makes explore-then-script a product feature. Browserbase contexts with
persist: trueare the documented login pattern (ours since Sep 9). Vercel cron is best effort: runs can be missed, doubled or overlap. AppFolio's Stack API creates and updates guest cards and updates units; no documented create for leases or renewal offers (unverified). - PoC evidence. Ledger with AppFolio request ids:
LEDGER.mdin the harness copy listed under Sources; wrong-id control POSTfef6654d(offer 369, document 890); park of offer 370 (document 891) at 22:44:44; application 58 convert PUT63a69c04, prediction logged 22:57:58, attempt 22:58:13. - New leases: routes, keys, ADR (section 14). Today the AppFolio approval only makes the review card (
on-application-approved.ts); the draft runs fromapi/new-lease-package-reviews/[id]/decisionon a person's approve (ADR-0143 §4). Keys:leasing.prospects(the building's Leasing status) andlease.send_for_signature(lease-send-switch.ts). Robot routes:run-move-in,run-new-lease-send. Decision 9 saves asd9. - New leases: reuse and findings. Reuse
computeLeaseDatesandauditLeaseDates(lease-terms/dates.ts) andcomputeProration(actual days, whole dollars). The robot's lease form (lib/runMoveIn.ts:310-353: rent lineprorated: "0", codes 229/83, deposit $200) moves into the print order. Today's draft sends start = move-in and end = move-in + term − 1 day (route.ts:375-378); the card previews the policy's date convention (approved-applicant-package.ts:82-86). Willows, Sep 7: occupancy, not Available On, is the convert refusal (execute-lease-draft.ts:115-119). The robot's rent falls back to the unit's advertised rent (lib/runNewLeaseSendForSignatureL4.ts:195-201).
Sources
- Proof of concept, FINAL report (Oct 20, 23:11 MT):
poc-report.md; harness copy, ledger, results and local-only screenshots at~/.claude/corpora/western-slope-renewals-2026-10-20/poc-document-engine/. - The two round-3 designs and the referee merge:
pairing/design-opus-r3.md(base),pairing/design-fable-r3.md,pairing/MERGED-OUTLINE.md; both round-1 designs and both critiques;pairing/BRIEF.md(Fede's words). - Research:
robot-inspection.md,robot-history.md,vendor-best-practices.md,determinism-research.md,ws-renewal-transcripts.md,module-layers-audit.md,document-prep-audit.md,pms-document-building-research.md,pms-renewal-config-research.md,ws-af-setup-inventory.md,gera-ingest-meetings.md,session-jar-report.md(session 006 scratchpad). - Hub: Renewals with the team in the loop, sections 10 to 12.
- Corpus:
s3://propflow-training-data/renewals-corpus/western-slope/2026-10-20/research/(private; tenant details inside).