AppFolio Memory Seeder

A reusable pipeline to seed a new AppFolio customer's memory graph from their work-order history — entities → observations → beliefs. Mirrors the LAPM chat backfill (scripts/backfill-from-chat.mjs), adapted for structured WO data. First run: Green Oak, Jan-1 window (282 WOs). Built to re-run per customer with --workspace + --vhost.

The reference — how LAPM was seeded — from chat

LAPM's graph was backfilled from iMessage (backfill-from-chat.mjs), 4 phases. Each is LLM-heavy because chat is freeform — the model has to find the entities and events inside the text.

PhaseWhat it didCost
1 · sessionizeWalk chat.db, group messages into conversational sessions (chat_sessions/chat_messages)mini LLM (judge)
entitiesLLM reads each session, extracts named vendors/properties/owners → resolveEntity (find-or-create) + owner cascadefull LLM / session
2 · observationsLLM extracts past-tense events ("Kori fixed closet at 17 Ozone; Jose overrode Guox") → memory.addObservationfull LLM / session
beliefsrunBeliefFormer(ws, obs_id) consolidates observations → durable beliefsfull LLM / obs

Key properties we keep: entities before observations (so obs reference real entity ids), idempotency (watermark / extracted_at / belief-evidence checks), cost gates (--confirm-cost, --dry-run, --limit).

What's different for AppFolio — structured, not freeform

The entities are already columns. A work order carries vendor · property · unit · tenant · category · description as structured fields — so entity seeding needs no LLM (the chat version's biggest cost). resolveEntity runs straight off the crawl. The freeform layer that does want an LLM is the work-order notes (the activity log) — that's where the real events live ("David reset the router + fixed the entry keypad").

 LAPM (chat)AppFolio (WOs)
Unitchat sessionone work order (+ its notes)
EntitiesLLM-extracted from textstructured columns — deterministic resolveEntity
Observation sourceJose's repliesWO description (base) + each note (rich)
Beliefsbelief-formersame belief-former + your confirmed facts seeded directly

The pipeline

0

Crawl done (WOs) notes: todo

WO list via the JSON:API (scripts/appfolio-crawl.mjs) — 1,273 WOs in appfolio/crawl/. Still to add: the per-WO notes (activity log) — crack the notes API the same way ("Download Notes" implies one), so observations are rich, not just the thin description.

1

Entities — seed first deterministic

For each distinct vendor and property in the window: resolveEntity({workspace_id, kind, name}). Vendors match the loaded vendors table → real ref_id. Properties cascade to owners via owner_properties. Tenants are not graph entities (kinds are vendor/property/owner only — same as the chat seeder, which skips tenants).

Prereq: properties + owner_properties aren't loaded yet, so property entities would be informal (no legacy backing, no owner cascade). Load properties + owners first (small — 25 props in window) so property entities are real and owners cascade in. Cheap, and unblocks the cascade.
2

Observations — one per WO, plus one per note base: deterministic notes: LLM

Base obs (free): straight from the structured WO — "{vendor} did {description} at {property} ({status}, {date})", entities = [vendor +1, property +1], tags from category. Note obs (LLM): the activity-log notes are freeform events worth extracting (who came, what was actually done, tenant feedback, reschedules) — run an extractor like the chat one. Both go through memory.addObservation(ws, {title, summary, raw_text, entities, tags, salience, source_message_id}).

3

Beliefs LLM / obs

Seed your confirmed facts directly (no LLM): the 9 vendor-trade overrides + 2 in-house staff (Yesika = cleaner, José = handyman, Bondy = laundry, Roberto = handyman-leaning-leaks…) → high-confidence beliefs with the WOs as evidence. Then run runBeliefFormer over the observations to consolidate the rest (property→preferred-vendor patterns, recurring treatments). This is the only expensive step — gate + batch it.

Scope & batching — Green Oak first run

WindowWOsVendorsPropertiesTenants
All time1,2738428117
From Jan 1 (#1234)282292569

Graph = Jan-1 only (282 obs, not a 1,273 flood — your call). Full history stays in the flat vendor roster / cheat sheet. The trade overrides aren't all from the Jan-1 window, so only seed the ones whose vendor appears in it. Initial batch before the full run: --limit=20 → eyeball the entities + observations → then the rest. Mirror the chat seeder's flags: --phase=entities|observations|beliefs, --limit, --dry-run, --confirm-cost.

node scripts/appfolio-seed.mjs --workspace=greenoak --vhost=greenoakpropertymanagement.appfolio.com \
  --since=2026-01-01 --phase=entities --dry-run        # preview the entity set
node scripts/appfolio-seed.mjs --workspace=greenoak ... --phase=entities          # seed entities (cheap)
node scripts/appfolio-seed.mjs --workspace=greenoak ... --phase=observations --limit=20 --confirm-cost  # batch
node scripts/appfolio-seed.mjs --workspace=greenoak ... --phase=beliefs --limit=20 --confirm-cost

Reuse for the next AppFolio client

Every step is parameterized by --workspace + --vhost; nothing is LAPM/Green-Oak-specific. Onboarding customer #3 (AppFolio) = crawl → load vendors/properties → appfolio-seed --phase=entitiesobservationsbeliefs. The crawler + this seeder are the reusable spine; the only per-customer human work is confirming the handful of untagged vendor trades.

Open decisions