0094 — Sync AppFolio guest cards with PropFlow prospects

Context

JP staff are creating guest cards manually in the AppFolio CRM for leads that Clara has already touched — or that never reach Clara at all — and PropFlow is blind to them.

Two live examples from jpco.appfolio.com (2026-07-15):

Today, the only AppFolio→PropFlow leasing ingestion is the rental_applications poller, which creates/updates a ProspectInquiry only after an application is submitted (matching on af.rentalApplicationId / af.inquiryId). A guest card whose application was merely sent — or never sent — is invisible to PropFlow.

The guest_cards report is available on the Data API we already use (v1 only: GET /api/v1/reports/guest_cards.json, verified live 2026-07-15; 404 on v2). It returns GuestCardId, GuestCardUuid (the CRM URL id), InquiryID, name, email, phone, unit, Source, Status, and received date. The Data API is GET-only — guest cards cannot be created through it; a write would have to go through the L4 browser agent.

Forces:

  1. Leads die outside PropFlow. Office-originated leads (walk-ins, direct calls, manual entry) never become prospects, so Clara can't nurture them and leasing KPIs undercount.
  2. Duplicate identity + lost attribution. Staff re-key Clara-touched leads into AppFolio with No Source Recorded, severing Clara's attribution and creating a second, unlinked record of the same human.
  3. Split-brain comms. Staff text from the AppFolio guest-card inbox while Clara texts from PropFlow — two threads, no shared history, and PropFlow's suppression/quiet-hours/TCPA logic (ADR-0092) can't see the AppFolio side.
  4. Voice transfers drop the lead. A leasing call that transfers to a human creates a Person but no ProspectInquiry, so the earliest Clara touchpoint is lost.

Decision

PropFlow is the CRM. Leasing prospects live and get worked in PropFlow; the PMS is a lead source, not a peer CRM we mirror into. We ingest PMS-side guest cards so no lead exists only in the PMS — we do not push prospects back.

Treat PMS guest cards as an external prospect source and sync them into PropFlow. The writer is PMS-agnostic: a neutral PMSGuestCard shape + guestCards adapter capability in src/lib/domain/pms/, with AppFolio as the first adapter that implements it (Yardi/RealPage slot in later).

Ingest (read path). Add guest_cards (AppFolio Data API v1) to the poller alongside rental_applications:

Superseded in part (2026-09-15/18, https://docs.propflowai.co/a/syndicated-guest-card-replies — Decision section): the outreach guard above governed guest cards ingested from AppFolio's own staff-facing CRM. Fede's later ruling scopes a narrower, syndicated-listing population back in — a card AppFolio's own Actions Log says the SYSTEM created (not staff-typed), at a company that has explicitly opted in via a per-company Organization.settings.leadSource setting (default email, i.e. today's behavior everywhere until a company is switched). That population gets a first-touch EMAIL reply, held (not sent) while the company is dark. This does not reverse or re-argue the consent-grounds rejection above for the general, staff-facing ingestion population this ADR covers — it is a separate, additive decision for a narrower population, not an amendment to this one. See the linked decision page for the read-path filtering (leadSource gate on inbound email) and the entity classification note below.

Write-back is explicitly out of scope (decision 2026-07-16): we are NOT creating guest cards in the PMS when Clara creates a prospect. PropFlow is the CRM — pushing prospects into the PMS would re-establish the split-brain this ADR exists to end. The earlier "Phase 2 — L4 write-back" idea is rejected, not deferred.

Also fix the voice-transfer gap surfaced by Jake's calls: a voice conversation classified as leasing intent (tour, availability, pricing) that ends in transferred must still produce a ProspectInquiry, not just a Person.

Entity classification

No new entities. ProspectInquiry gains no new fields beyond the existing af pointer block (guestCardId/guestCardUuid slot into the existing af pointer block on ProspectInquiry alongside rentalApplicationId/inquiryId).

Superseded in part (2026-09-15/18, https://docs.propflowai.co/a/syndicated-guest-card-replies): the first-email-reply work scoped in above by the outreach-guard note needs its own lifecycle (pending / held / sent) that does not belong on ProspectInquiry — a held draft is not a fact about the prospect, it is a fact about one outbound touch attempt. That work adds one new entity, ClaraFirstEmailIntent, keyed independently in DynamoDB, scoped strictly to the narrower syndicated-first-touch population above. This is additive to, not a reopening of, this ADR's "no new entities" ruling for the general guest-card ingestion this ADR governs.

Consequences

Alternatives considered