Integrations · next big build
| Ask | Recommendation |
|---|---|
| D1. One shared core, one agent per vendor (option B), inside the existing robot repo as two packages? | Yes. Start with the two bug fixes and the account-key refactor; the AppFolio agent keeps shipping throughout. |
| D2. The sync job owns Yardi polling (option B), with the cadences in section 7 as the starting point? | Yes. |
| D3. Phase 0 at the Willows: build the list-showings read route now as the first "read through the robot" proof? | Yes, dark, when build days resume; it is also rung 2 of the calendar ladder. |
| D4. Mock-server API proof of concept for Yardi now? | Hold. The mock serves residents, balances, leases and work orders, none of the prospect family; it would prove phase 2, not phase 1. |
1 Today2 Yardi phase 13 Options4 Scope5 Phases6 Refactor now7 Polling8 Unknowns9 Decisions
Two lanes. Reads come from the official Reports API on a schedule (1 min, 5 min, hourly) with a per-account budget. Writes, and the few reads the API cannot do, ride the browser robot: it logs in as a staff user, keeps the session warm and replays AppFolio's own web requests. No clicking in production, no model in the loop.
No API, so the robot carries reads too. It logs into ConAm's Yardi as the PropFlow user, keeps the session warm the same way, and serves the same read and write contract. The sync job keeps the schedule and budget and calls Yardi robot routes instead of the Reports API. Everything above the ports does not change.
| Question | Options | Recommendation |
|---|---|---|
| One agent or two? | A. One robot app with a vendor switch inside. B. Shared core package + one deployed agent per vendor (AppFolio, Yardi), same route contract. C. A separate Yardi repo from scratch. | B. The robot's own April plan (ADR-0013), never executed. Separate deploys because sessions, login storms and breakers are per vendor and today's breaker is per server instance; a shared core because the session machinery is the hard part and it already works. C throws away the part that took months to get right. |
| Who polls? | A. The robot polls Yardi and pushes to PropFlow. B. The existing sync job keeps the schedule and calls Yardi read routes on the robot. C. Both. | B. Cadence classes, the request budget, the import record and the mirror all live in the sync job already. The robot stays a dumb, stateless door. Cost: a robot read takes seconds and runs one at a time per account, so Yardi cadences are slower than AppFolio's (section 7). |
| How do reads come out of Yardi? | A. Replay the JSON calls CRM IQ's screens make. B. Download the report exports Voyager already offers (rent roll, availability). C. Scrape HTML tables. | A for leasing, B for back office, C last resort. Decided by the first week with a login: open each screen with the network log on and record what it calls. Same method that produced the AppFolio routes. |
| Calendar | Decided 5 Oct: Yardi's property calendar is the building's calendar, read and written like Google or Outlook. | Ships as the calendar provider with vendor id yardi, backed by the agent's appointment routes. Not reopened here. |
| # | Capability | PropFlow port | Yardi screen (expected) | R/W | Phase |
|---|---|---|---|---|---|
| 1 | New guest cards since last poll | read client · guest cards | CRM IQ prospects list | R | 1 |
| 2 | Guest card detail: name, phone, email, source, unit interest, activity | read client · guest card | CRM IQ guest card | R | 1 |
| 3 | Create guest card for a Clara lead | guest-card writer | CRM IQ add prospect | W | 1 |
| 4 | Add a note / activity | guest-card writer | CRM IQ guest card | W | 1 |
| 5 | Calendar: list appointments (busy) | calendar provider | CRM IQ calendar | R | 1 |
| 6 | Calendar: book, move, cancel | calendar provider | CRM IQ appointment | W | 1 |
| 7 | Application status per applicant | read client · applications | CRM IQ stage / Voyager applicants | R | 1 |
| 8 | Unit availability and pricing | read client · units | Voyager availability report | R | 1 |
| 9 | Rent roll (residents, leases, balances) | read client · rent roll | Voyager rent roll export | R | 1 |
| 10 | Resident contact info | read client · tenants | Voyager residents | R | 1 |
| 11 | Work orders | work-order writer | Voyager service requests | R/W | 2 (API exists) |
| 12 | Renewals | read client · renewals | Voyager expirations | R | 2 (API exists) |
Per-screen build notes live in the PMS knowledge repo (pms-knowledge-base/yardi/, readable through the /yardi skill), one note per screen with URL, selectors and captured requests, marked verified only after a route round-trips on Yale.
| Phase | What | Proof | Gate |
|---|---|---|---|
| 0 · Willows | Prove "reads through the robot" on a PMS we can already log into: one list-showings read route on the AppFolio robot, surfaced as the calendar busy source (rung 2 of the calendar ladder). Same shape the Yardi reads will take. | Willows showings appear in the tour availability check. | None; dark. |
| 1 · Yardi browser agent | Shared core + Yardi driver (login, MFA, session probe, forms), then routes for rows 1–10 above in this order: calendar read, guest card read, guest card create + note, calendar write, applications, units, rent roll, residents. | Each route round-trips on Yale with ConAm's login; the Yale readiness tracker rows turn green. | Login from ConAm. Turning on at Yale is Fede's call (new capability, default off). |
| 1b · Product side | Remove the AppFolio-only assumptions in the sync job, credential rows, job names and tool handlers (section 6), so yardi is a real branch, not a null. | Yale polls on the Yardi cadences; the mirror fills. | None; fixes, no switch. |
| 2 · API where it exists | Work orders, renewals, balances and residents move to the SIPP interfaces already modelled (and served by the mock server) once ConAm licenses them; robot routes stay as fallback. | Mock-server tests plus one live ping. | ConAm's SIPP contract. |
| 3 · Prospect interface | Guest cards, activity and availability move to the paid Prospect interface; the calendar stays on the robot (Yardi has no calendar API). | Same routes, API-backed. | The $25K/yr quote. |
Rule (Fede, 5 Oct): the refactor is built for N vendors, not two. Drivers register by vendor id, accounts are rows with a vendor field, and no code branches on a vendor name. Adding a third PMS is a new driver folder and a config row.
| Robot repo layer | Verdict | Size |
|---|---|---|
| Route wrapper, token check, account binding, cookie store, login budget, locks, retries, keep-alive, logger, alert scrubbing, browser factory | Reuse as-is once AppFolio names become parameters | 15 core files measured at 4,870 lines; the audit's wider count of generic infrastructure is ~11k of 66k |
| Login steps, session probes, Rails forms, the 4k-line API client | Extract the shape, rewrite the AppFolio parts as a driver | ~7 files, ~7k lines |
| The operations (guest cards, showings, renewals, units, tenants, work orders, vendors) and their 77 routes | Yardi gets its own; nothing ports | ~95 files, ~48k lines, plus 78 routes |
| Housekeeping | Delete the demo UI and research route; fix or drop the stale Dockerfile; make the refresh breaker shared across instances; consider per-customer robot tokens | small |
Tracking issue: appfolio-browser-agent #469. Six small steps merged in one day, each proven before the next started: full test suite, the live preview probe on the Willows, then after merge the nightly renewal smoke and a quiet #alerts. Customers saw no change. Fede's bar: "the most important thing is testing and making sure nothing breaks."
| Step | What moved behind the engine boundary | PR | Proof |
|---|---|---|---|
| 1 | The boundary itself: lib/core/, the AppFolio driver, a lint rule that forbids the core from importing anything AppFolio-named | #470 | 3893 → 3909 tests; KV keys pinned byte-identical; Willows smoke green |
| 2 | MFA code parsing (SMS today; email, app code, SSO are driver choices) | #471 | → 3924; 27 tests fail if the parser is stubbed; smoke green |
| 3 | Login identity (the key behind login budgets and locks), hashes frozen | #472 | → 3935; smoke green |
| 4 | Read-only guard hosts, secret-stripping cookie names, login-page signature | #473 | → 3950; Situs write refusal unchanged; smoke green |
| 5 | Account record: vendor, base URL, database, username; host rule owned by the driver | #474 | → 3962; three customer rows resolve identically; smoke green |
| 6 | Last session checks (three login-body checks, off-tenant hosts, breaker key) + a session-manager seam, heartbeat as first caller | #476 | → 4008; breaker key string frozen; smoke green |
Real browser proof (6 Oct, 19:32Z): after all six steps were live, a fresh browser logged in to JP&Co's AppFolio with the SMS code, through the production robot, in 79 s (run; recordings on issue #469). The nightly smoke alone is HTTP over the stored cookies, so from now on every engine, session or MFA change is proven with this real login run as well, on Fede's go per run.
What a Yardi robot needs now: one driver file under lib/vendors/yardi/ that registers itself, plus its screens. No AppFolio code is touched.
Step 7, not started on purpose: the physical move to packages/browser-core + agents/appfolio (ADR-0013) changes the Vercel project root and the lockfile, so it cannot merge dark. Recommendation: hold until the Yardi login is dated; the lint rule already enforces the boundary.
Two decisions for Fede, nothing changed meanwhile: (1) the error classifier and the driver disagree on 8 of 28 "AppFolio sent us to log in" shapes (table on #476); both kept, since picking one loosens or tightens login detection at three customers. (2) Six JP&Co-only fallbacks remain in code (list on #474); none is provably dead without reading the JP&Co account row in production.
Still waits for the login: the Yardi screens themselves. Separate decision, not part of this chain: the request-pacing gate from section 9.
Nothing in Yardi will call us. The sync job polls, but each poll is a logged-in page or report, costs seconds and runs one at a time per account. Starting cadences, tuned after the first week:
| Data | AppFolio today | Yardi phase 1 | Why |
|---|---|---|---|
| New guest cards | 1 min | 3 min in office hours, 10 min outside | Clara's reply speed is the product; one list page per poll |
| Calendar / appointments | n/a (Outlook/Google live) | Before every booking, plus every 15 min | Read-before-write is what stops double-booking |
| Application status | 1 min | 30 min | Status changes are slow; one list page |
| Unit availability + pricing | 5 min | 1 h | One report export |
| Rent roll, residents | 5 min | Nightly | Large export; answers "who lives here" questions a day late at worst |
| Work orders, renewals | 1 min / 1 h | Phase 2 (API) | Interfaces exist; not worth a screen |
Writes stay on the existing outbox, so a robot outage delays a write instead of losing it. A per-database request budget on the robot ships with phase 1 as a guard, not a switch.
Read-only pass over 14 days of the AppFolio sync job's logs (22 Sep → 6 Oct) and its budget ledger, plus the robot's code. Source table with every query: pms-knowledge-base/appfolio/rate-limit-measurement-2026-10-06.md.
| What we allow today | Where AppFolio starts refusing (measured) | Refusals in 14 days | |
|---|---|---|---|
| Reads (sync job, Reports API) | 6 per 15 s ≈ 24/min per account | ≈ 9/min per account (WS: 0.15 % of requests refused at 7–9/min, 3.6 % at 10–14, 13 % at 15+; JP&Co starts at 7–9) | WS 1,135 · JP&Co 350 · Situs 4,219 (one hour, 29 Sep) |
| Writes (robot, staff web UI) | Pacer from #461: 2 in flight, 250 ms ≈ 240/min — never fires | Unknown. The web UI's limit is not the Reports API's; #468 now logs every robot request so it can be measured | 0 in the 30 min of logs Vercel keeps |
Facts that change the design: none of 5,704 refusals carried a Retry-After header; a retry 2.4 s after a refusal is refused again 68 % of the time at WS; the sync's retry attempts bypass the budget (a bug); the budget answers "open" on a cold start; Situs's sync has been paused by staff since 29 Sep yet its ledger was still being refused on 5 Oct from somewhere on Vercel (unidentified). AppFolio publishes no limit.
Proposed (yes / hold): one shared gate per PMS account used by reads and writes together: everything serialized, about 9 starts per minute (6 for accounts that refuse early, like JP&Co), reads capped at 6 of the 9 so writes always have room, one refusal pauses the account 15 s, a second within a minute 30 s. Cost: sync peaks queue, a renewal prepare takes about 3 minutes instead of under one. Gain: up to ~90 % of WS refusals (upper bound). For Yardi: start from the same gate and measure; its limits are unknown.
Sources: robot repo at commit 175309f (30 Sep) and product repo at origin/main de0f47ce (6 Oct), read by two audit passes on 5 Oct 2026 (architecture map; multi-PMS refactor audit); robot ADR-0013 (agent fleet topology, Proposed 27 Apr 2026); product ADR-0067, ADR-0072, ADR-0094 and docs/planning/yale-integration/research/02; Yardi public documentation list in pms-knowledge-base/yardi/sources.md; ConAm emails of 24 Sep and Fede's report of the 1 Oct call. Line references and the full verdict table are in the audit transcript of session ed890d2e (agents/005).