Customer #2 — Green Oak (owner Brad · PM Brooke). Priority 0: mine their past work orders into a vendor roster + a dispatch cheat sheet so Andrew can hand-run Green Oak tomorrow — given an issue + property, who to send. Full setup (directory data → load → wire the live agent → email routing) follows. Cold-start: no Reporting API, empty memory graph.
vendors for Green Oak.scripts/seed-appfolio-beliefs.mjs --workspace=<x>. Cheat sheet artifact built for manual ops; read_memory ready for go-live.vault/decisions/2026-05-31-minimum-product-before-expansion.md.| Surface | Vanessa (LAPM) | Green Oak |
|---|---|---|
| Memory graph | Seeded from legacy data + months of learned beliefs / vendor prefs | Empty. Zero beliefs, no vendor-by-trade, no per-property quirks. |
| AppFolio read | Reporting API (read-only) + Playwright (send) | Playwright session only — no Reporting API. |
| Baseline data | appfolio-sync (Reporting API) | Sync is dead — must crawl the AppFolio UI. |
| WO intake + enrich | Email + Reporting API fills unit / clean description | Email-only; enrich's API step degrades to the raw email. |
| Vendor suggestions | Roster + beliefs → confident picks | Empty roster → zero suggestions until crawled + learned. |
Net: the two things that make the agent good for Vanessa — a populated roster/graph and API-enriched WOs — are absent on day one. That's the cold-start, and it's why Green Oak runs supervised while it fills.
What "set up" looks like, measured from LAPM's live data. Green Oak's counts will differ; the shape (which fields are filled) is the target.
| Table | LAPM rows | Fields that must come through | Skip |
|---|---|---|---|
properties | 63 | name · address · city · state · postal_code · appfolio_property_id · appfolio_property_number | — |
units | 381 | name (normalized) · appfolio_unit_id · property_id | — |
tenants | 404 | name · email (~89%) · phone (~93%) · unit_id | appfolio_tenant_id (0 at LAPM) · user_id (being dropped) |
vendors | 34 | name · email · phone · trade · address · appfolio_vendor_id · preference_index | — |
owners | 74 | name · email · phone · appfolio_owner_id | people_id (0 at LAPM) |
owner_properties | 69 | owner_id · property_id · workspace_id | — |
appfolio_tenant_id, so tenants are identified by the (unit_id, email, name) unique index. Capture the AppFolio tenant id if the UI exposes it (better idempotency), but parity doesn't require it.One Node process on one Mac mini with one iMessage account already serves every customer. No per-customer instance — the code is multi-tenant by construction.
chatGuidIndex maps each groupchat → its workspace; a message resolves its workspace from the GUID it arrived on. Green Oak chat: chat157769184179351526 — nico + Andrew = Bedrock side, Brad (owner) + Brooke (PM) = customer side; agent sends as bedrock/650 via is_from_me.issues_v2 per workspace; the mini's single Apple ID is already a participant in both the LAPM and Green Oak groupchats.workspace_id-scoped; every turn is scoped to the workspace the GUID resolved to. No cross-customer leakage.id 5406e04f-8e22-4ed8-a54e-a6d08ff45ef7 · slug greenoakpm · appfolio_enabled=falsegreenoak to core/workspaces.mjsdoneKeyed by the UUID above: label:'greenoak', chatEnv:'GREENOAK_CHAT_GUID', pm_handles = Brad +18472742377 / Brooke +18055046160, agent_handles = nico/Andrew, pm_label:'Brooke'.GREENOAK_CHAT_GUID in .envdeferredGUID identified: chat157769184179351526. Hold until data loads — the moment it's set the agent goes live in Brad/Brooke's chat with an empty roster. Poller picks it up automatically once set.storageState at appfolio/.state.greenoakpropertymanagement.json on the agent host. Drives every JSON:API crawl.tenants.user_iddoneBackend dropped the column. appfolio-sync is retired (0 invocations) so no companion edit needed. Tenants load with no user_id.andrew@usebedrock.co); pubsub-hook routes per-message by the To: alias (lapm@ / greenoakpropertymanagement@usebedrock.co) → workspace. Unrecognized alias is dropped (no fallback → no LAPM pollution). Handles the Google-Group From-rewrite. Live in prod; 21andrewch retired, single mailbox.agent/scripts/appfolio-crawl.mjsDisposable-but-reusable Playwright scraper. Phase 1 = crawl → review artifact; Phase 2 = --load. Prefer AppFolio's built-in CSV/Excel export per list page over DOM scraping; capture AppFolio ids from row-detail URLs as idempotency keys. Reuse scripts/appfolio-units-dump.mjs's deriveProposedName for unit naming. Priority by leverage: the WO-history crawl (Priority 0) comes first — one pass yields the vendor roster and the memory-graph seed. Then properties/units (resolve WO location + match email-intake addresses, which carry no API unit_id), then tenants and owners.
/api/*, header accept-version: v2, sideloaded relationships). scripts/appfolio-crawl.mjs + per-entity crawlers. Idempotent on appfolio ids./api/work_orders. Cheat sheet + all-WO viewer artifacts./api/properties|units|tenants; FKs from relationships; tenant phone/email come through./api/owners → 403 on the Maintenance Team login. Deferred until an owner-permission AppFolio account.Idempotent upserts on (workspace_id, appfolio_*_id) so re-runs don't duplicate. Each step depends on the one above it.
(workspace_id, appfolio_property_id).(workspace_id, appfolio_vendor_id); preference_index 0.(unit_id, email, name); no user_id; 212 with contact.issues_v2 → poller enriches → draft appears → shows under "Green Oak" in the selector.The crawl seeds the roster and tables; the memory graph still starts empty. Two parts: confirm the learning pipeline fires on a fresh workspace, then let the agent ask instead of only learning passively.
read_memory). Surfaced + fixed a real gap: the belief-former only minted beliefs from stated PM rules, never from repeated work-order patterns, so cold-start AppFolio data formed zero beliefs. Now it generalizes vendor→trade from behavioral history and links property footprints; the count-based "go-to" moved to a deterministic pass. Shared prompt — LAPM benefits too.enrich_issue's API step have nothing to call → WOs ingest from the email text only; unit + clean-description enrichment becomes address-matching, not an API fetch.From: "LA's Best Property Management, Inc. (Do Not Reply)" — robust, mailbox-independent key. AppFolio also stamps account id 600022 into return-path (bounces+600022-…@mailer.appfolio.com). Caveat: the current 21andrewch connection arrives via nicoluo@gmail.com, unrelated to the usebedrock aliases — so the alias-routing path is still untested (would need andrew@usebedrock.co connected).appfolio_property_number — their AppFolio account doesn't use them (the "180"/"294" numbers are LAPM-only). The intake shim (intake-agent) matches a WO's property by that number parsed from the email subject — so for Green Oak it can't, and may even drop the WO if the subject carries no number. Resolution must be address-based: match the WO email's property/unit address against the loaded properties/units (fuzzy + human-confirm early). Verify a real Green Oak WO email's subject format before intake goes live.ui/index.mjs + ui/page.html) shows one workspace via a prod/test toggle. The selector + per-workspace memory-graph view are how he reads Green Oak's seeded cheat sheet and picks a vendor — so this rides right behind P0, not "later."core/workspaces.mjs (listWorkspacesForUi()), injected server-side. Switching re-scopes issues, chat, turns, drafts, and KPI.display name; internal label stays 'prod'. Full rename + state/ migration deferred — runtime sites key on 'prod' (live-customer landmine) and it needs a coordinated server restart. Bundle it with Green Oak go-live (which restarts anyway)./api/clear-test endpoint, and deleteIssuesByWorkspace (core/supabase.mjs).Also shipped: per-workspace KPI strip (always shown, zeros for empty workspaces — /kpi?workspace=, needs a restart for real non-LAPM numbers); chat-poller shouldSessionize() made label-independent so every workspace sessionizes (was 'prod'||'test', would've dropped Green Oak); fixed switching bugs (stale toggle ref crashing init, KPI bleed, issues not clearing on an empty workspace).
appfolio/runner.mjs hardwires VHOST=lapm.appfolio.com + one shared session. Needed before the agent acts on Green Oak (tenant/vendor texts via the AppFolio WO Texts widget); not needed while Andrew dispatches manually.VHOST + storageState; the Conversations/Texts automation keyed off the selected workspace.