Replying to syndicated guest cards

Approved by Fede 2026-09-18

Brief standalone version of the design in section 5 of the Western Slope go-live page.

Decision

Fede's ruling, 2026-09-18 evening: Clara replies only to guest cards whose oldest activity-log entry is "Guest Card Received" by System. No setting, no value, no transition path. Staff-typed cards are parked. Once the client stops typing cards, every card passes on its own.

Contents

  1. The rule
  2. The one exact signal
  3. How it works
  4. What exists vs. what is left
  5. Rollout
  6. Three decisions, answered
  7. After Clara replies: the card in AppFolio
  8. Tonight's build
  9. Guest-card sync audit (2026-09-18)
  10. Replay results (held drafts)
  11. Readiness report — 2026-09-19

1. The rule

Clara replies only to guest cards a listing site (Zillow, Apartments.com) created, never a staff-typed card. AppFolio's own "new lead" email is not the trigger; Western Slope gets none of those. The card's "Source" label doesn't work either: 772 staff-typed cards and 381 listing-site cards carried identical labels.

2. The one exact signal

Every guest card's activity log starts with one line naming who made it: "Received" for a listing-site card, or "Created" by a staff name otherwise. That's the only reliable tell, so a reply goes out only once that line reads "Received."

3. How it works

flowchart LR
  A[Feed checks AppFolio\nonce a minute] --> B{New card?}
  B -- no --> A
  B -- yes --> C[Feed calls the robot service's\nread endpoint, 1-2 sec]
  C -- "staff name" --> D[No reply]
  C -- "Received by System" --> E{Company's live\nswitch is on?}
  E -- no --> D
  E -- yes --> F[Clara sends the first reply]

A new card gets one quick, read-only HTTP call to the hosted robot service's read endpoint, on its already-logged-in session, about a minute after the card lands. That call normally takes 1 to 2 seconds; a cold re-login happens rarely and takes up to 2 minutes when it does. Expected load at Western Slope: one to three look-ups a day.

4. What exists vs. what is left

PieceState
Feed that copies every new card into PropFlow every minuteBuilt, running
Robot read endpoint the feed callsBuilt for other work; not yet wired to this feed
Recording who actually created each cardNot built
The switch that starts a reply when a new card landsExists in code, deliberately turned off
The message Clara sends as the first replyBuilt, currently paused by Fede
The company-wide on/off switch for live repliesBuilt
Graded test run against real past cardsIn review

5. Rollout

Ships dark, off by default, as a company setting. Before it's turned on, the team replays 47 real Western Slope cards through a safe copy of the system and publishes the graded result here. Turning it on for real prospects is Fede's call, made after reading that result.

6. Three decisions, answered

QuestionAnswer
Let the robot read each new card to check who created it?Yes, read-only, via the hosted robot service's read endpoint.
Un-pause the work that drafts and sends the first reply?Yes, un-paused 2026-09-18 evening.
Include Zillow's referral-network cards?No, excluded.

7. After Clara replies: the card in AppFolio

Once Clara's first reply goes out, two things happen back in AppFolio.

Assign

On Clara's first reply, the card's inquiry is assigned to Clara in AppFolio (Fede, 2026-09-18: "we should assign clara"). That needs a "Clara" user set up in Western Slope's AppFolio — that's the client's step, not ours. The target user to assign to is an org setting; if it isn't set yet, the assign write parks instead of failing or guessing a user.

Notes — cadence pending Fede

Where this gets proven

Both mechanics — assign and notes — get wired and proven at the Willows (JP&Co test property 45) while the note cadence above is being decided. Never a test write into Western Slope's live database.

Who builds what

PieceOwner
Reply path — intake, composer, replaySession 000
AppFolio write path — note and assign intents, drain, robotSession 003

Hand-off between them is one call at reply-send completion, carrying the card id, conversation id, outcome, reply body, and whether to assign.

8. Tonight's build

StepState
Red mainFixed (#9632)
Robot actor fieldMerged, armed (#387)
createdBy stampArmed (#9633)
Lead-source settingFixing tests (#9484)
ComposerArmed (#9425)
Intake + seamPR opening
Replay driverPR opening
Note + assign write path (session 003)PR opening
AliasesArmed (#9643)
Rent-sweepArmed (#9634)
Lock-in promiseArmed (#9635)
Weekend dateArmed (#9642); #9475 landing

9. Guest-card sync audit (2026-09-18)

Read-only audit of the code that copies AppFolio guest cards into PropFlow, prompted by a Western Slope card that looked six days late. Nothing was changed. The fixes are a separate step.

What the six-day gap actually was

The evidence, in order:

Ruled out explicitly: a date window, a paging cutoff, a throttle, a failed run, and "the sync was switched on today". The 30 day rule that blocks creating a record from an old card never applied either, since the card presented itself as same-day.

What we cannot see is why AppFolio held it back. That needs someone to open card 2910 in their interface. The likely shapes are a card that sat in a state their report excludes until something promoted it, or a card their system re-received.

How it runs today

Findings

#What breaksWhereFix in one line
P0-1The first minute after a new AppFolio account is connected, every card received in the last 30 days is made as if it were a brand new lead, with nothing marking it as backfill. The day the reply trigger is switched on, that is a month of stale leads getting a first text at once. Not what happened here, but it is waiting for the next client.guest-card.ts:1572, writers/utils.ts:161, lambda/appfolio-sync/handler.ts:4224Record when we first saw each card and make the reply trigger require that the card is new to us, not merely new in the list.
P0-2A lead's created date is AppFolio's Received value, so we cannot tell a card we just discovered from a card that just arrived. Every funnel number keyed on created date is AppFolio's clock. This case shows the field can also be six days off the card's own history.guest-card.ts:1631, seeded once at dynamo/inquiries.ts:711Add our own "first observed" timestamp instead of overloading one field with two facts.
P1-1The writer states that a bookkeeping-only save leaves "last updated" alone. The storage layer overwrites it on every write regardless, so closed leads keep reading as "just updated" in every recent-first screen. Visible on this very record: made at 22:46, rewritten at 22:47 with nothing meaningful changed.guest-card.ts:1501 against dynamo/inquiries.ts:605Let the caller opt out of the automatic stamp, or drop the claim from the writer.
P1-2The "already handled" marker is saved before the identity repairs run. If anything throws in between, the marker survives and every later minute skips that card. The person stays nameless or phone-less, silently and indefinitely.guest-card.ts:1518 and :1527; the roll-back at :1857 cannot be reached from that throwSave the marker only after the repairs report success, or move the roll-back into the catch.
P1-3A card that moves to another property makes a second lead under the new property and the first is never retired. Two live leads, one card.guest-card.ts:1612 (the id includes the property); the snapshot at :928 is property-scopedLook the card up across the client's properties before creating, and retire the record on the property it left.
P1-4Nothing on the synced card says who created it in AppFolio, so the "only reply to listing-site cards" rule in section 2 has nothing to read. The activity type that would carry it is defined and used nowhere.domain/pms/types.ts:649-679; prospect-activity-log/contract.ts:296Carry AppFolio's creator field on the synced card, the way the application sync already does.
P1-5The per-property fetch throws away AppFolio's "there was more" signal. A very large property could silently lose rows with only a log line nobody watches. The purchase-order fetch guards against exactly this; guest cards do not.pms-adapter.ts:797, appfolio/client.ts:537Use the metadata-returning fetch and fail the tick when the answer is truncated.
P1-6A card's status is judged by hunting for words like "inactive" or "application complet" inside whatever string AppFolio sends. AppFolio also sends a separate last-activity field we ignore entirely. A dead lead whose status wording contains no recognised word stays live in Clara's eyes.guest-card.ts:504 and :519; unused fields at appfolio/types.ts:716Confirm what each client's status column actually contains, then match a known list instead of substrings.
P1-7A card deleted or merged in AppFolio simply stops appearing and we never notice, so the lead lives on pointing at a card that is gone. The same blindness that let this card's six-day absence go unremarked.no disappearance handling anywhere in guest-card.tsCompare each run's card numbers against what we hold for the property and flag the ones that vanished.
P2-1The roll-back save is the one save in the file that does not advance the row's version counter, and the stale row is then handed to the rest of the batch. A second card for the same person in the same minute loses the write race and is skipped for that tick. This is the issue the reviewer flagged.guest-card.ts:1858-1859; every other save bumps at :1328, :1511, :1540, :1667Bump the version after the roll-back save, like every other save site.
P2-2When an identity repair fails, the reason is thrown away. The warning names the card but never says what went wrong, so the roll-back cannot be triaged.guest-card.ts:817-821Log the error before swallowing it.
P2-3AppFolio sends the card's notes in the row we already fetch and we drop them. Notes are what the leasing team actually reads.appfolio/types.ts:740, not mapped at pms-adapter.ts:1917Carry the notes field through, read-only.
P2-4If two lead rows ever carry the same card number, the lookup quietly keeps one and the other becomes invisible to the sync for good.writers/utils.ts:10-17Detect the collision and log it rather than letting last-one-wins decide.
P2-5The schedule file still lists guest cards as every 15 minutes. That line stopped firing on 13 Sep and the real cadence is every minute. Six comments in the writer size their read cost "per 15-minute tick", so the load they are reasoning about is 15 times smaller than the real one.the guest_cards entry in lambda/appfolio-sync/schedules.json; comments throughout guest-card.tsDelete the dead cadence line and correct the comments.
P2-6Cards whose date did not parse sort to the front of the batch, so they write first and can be overwritten by better data later in the same run. Harmless today, undocumented.guest-card.ts:951Sort dateless cards last.

P0 means it can reach a customer. P1 means data goes wrong quietly. P2 is cleanup.

Questions the audit answered along the way

Read-only audit against main at 456eddbb, plus production records and sync logs. No prospect names appear on this page.

10. Replay results (held drafts)

Section 5's replay, run: 47 real Western Slope guest cards (the 48th window read is included below; one card is out of window) replayed against org_ws_mirror_test, a sandbox copy of the Western Slope roster used only for this kind of test — never the client's own AppFolio account or database. The company's live-reply switch stayed off the entire time, so nothing was ever delivered to anyone; every reply Clara composed was held and written to a record instead of sent.

Replayed against commit 3f8aa1e67b70ca59a794e352f644858dcf976bd0 on fede/gca-assign-clara-engage; grading logic as of commit 65c53d93d7 on PR #9712.

MetricCount
Cards replayed (held intents graded)48
Composed a real reply (draft body on file)14
Not graded (never reached the send door)34
Held (company not live)14
Passed grading12
Failed grading2

Four representative held drafts (inbound → draft)

Prospect and staff names replaced with [prospect] / [staff]. Inbound is the prospect's own first words, taken from the AppFolio guest-card record, exactly as Clara read them.

1. Hallucinated tour confirmation — FAILS the new rule

Inbound: "We were told to come by between 2-4 on Thursday??"

Subject: Interested in Ember Estates HZ

Draft: "Yes, that's right — you're all set for Thursday, September 24th at 2:00 PM at Ember Estates HZ, 2858 C 1/2 Road, Grand Junction, CO. See you then!"

Nothing in the card or the inbound message names September 24th, 2:00 PM, or a confirmed booking — this is Clara's first-ever touch with this prospect, so there is no prior thread to be echoing. She invented a specific date/time and asserted it as already-booked. A second card in the same replay (address and other names redacted here) did the same thing with a different fabricated date. Both now fail grading.

2. Bilingual (Spanish) tour scheduling — passes

Inbound: "Friday"

Subject: Interested in 2978 Krista Street

Draft: "Perfecto, el viernes 25 de septiembre tengo disponibilidad desde las 9:00 AM hasta las 4:30 PM. ¿Qué hora le acomoda mejor? Y para terminar de agendar, ¿me podría compartir su nombre?"

Proposes a time and asks — never asserts anything is already booked.

3. General availability follow-up — passes

Inbound: "Hi, sorry. I've been applying to a lot of places. Which complex are you with?"

Subject: Interested in Northridge Apartments

Draft: "No worries at all! I'm with Northridge Apartments, at 565 Redcliff Cir in Ridgway, CO. You'd previously looked at a one-bedroom here. Want me to check on current availability, or would you like to come take a look in person? I've got openings tomorrow between 9 AM and 4:30 PM if that helps."

4. Not graded — reply pipeline succeeded, bookkeeping write never landed

Inbound: "No thank you I think I found something else. Curiosity though how much is a two bedroom two bath or a one bedroom one bath"

Subject: Interested in Northridge Apartments

Draft on the intent record: (none — the intent stayed pending with no body)

The intent record looks like Clara never got here, but the conversation it points to tells a different story: it already holds a real, non-composed-of-nothing reply. The compose step worked; the step that was supposed to save that draft back onto this held-intent record never ran. Not a routing decision, not an empty reply — a bookkeeping gap, tracked as a follow-up.

The full 48-row inbound-and-draft table lives with the grader in PR #9712 (stacked on #9425's branch, not yet merged — review in progress), which also carries the corrected grader and the new hallucinated-booking rule.

11. Readiness report — 2026-09-19

When you flip Western Slope's company switch, Clara starts answering — by email — new guest cards that AppFolio creates from syndicated listing sites (Zillow, Apartments.com). That's it; nothing else about the client's setup changes. Turning it on takes two settings: the company's live switch, and a lead-source setting telling Clara that Western Slope's leads come from AppFolio guest cards. Both of those switches are OFF for every org right now, including Western Slope, so nothing acts on a real card no matter what merged overnight. Everything below this line shipped dark tonight so it's ready to flip once the proof holds up — no overnight report exists, and none of tonight's proof runs have happened yet.

What ships tonight

PieceEffect in plain wordsStatus
Sync reads the card and who created itEvery new guest card gets checked for whether a person typed it in or a listing site sent it automatically.Merged and live in the sync worker (PR #9785 — fixes the key the sync job reads the card by)
Reply only to System-created cardsClara only writes back on the automatic, listing-site cards — never one a staff member typed.Merged (standing rule, shipped earlier)
Zillow relay cards skippedCards that came through Zillow's referral network, not a direct listing, are left alone.Merged (standing rule, shipped earlier)
Company live switch holds every emailUntil you flip Western Slope's switch, no email goes out, no matter what else is working.Merged (standing rule, shipped earlier) — currently OFF for every org
Lead-source settingTells Clara "Western Slope's leads come from AppFolio guest cards" so she knows which pipe to trust.Merged (standing rule, shipped earlier) — currently unset for Western Slope
Composer writes the exact draft that later gets sentThe reply Clara would send is written out in full now, not a placeholder filled in later.Merged (PRs #9633, #9705, #9635, #9484, #9737, #9425)
Draft composed before hold so held drafts are reviewableEven while the switch is off, you can read the exact email Clara would have sent.NOT merged — PR #9721 has a green review; one red test is under triage
Real composer wired into the sync workerThe live card-reading job now calls the real reply-writer, not a test stand-in.Merged and live in production, web and the sync worker (PR #9750)
First-touch gate on the mailboxClara only auto-replies to a prospect's very first email, never everything that lands in the mailbox.Merged and live in production (PR #9761)
Booking-promise guardClara is blocked from telling a prospect a tour is booked unless a tool actually booked it.Merged (standing rule, shipped earlier)

What still blocks real cards

Proof

What was testedWhereResultEvidence
Replaying real Western Slope guest cards end to end through the reply logic (Willows email proof)Test mirror org only — never the real Western Slope databaseNot run. Pending — blocked on PR #9721 landing and a test inbox.
Sending the rental application after a tour (Willows application send)Willows test propertyNot run. Pending. The mode setting (PR #9779) merged this morning; the post-tour wiring (PR #9783) is being moved onto main; the drain step (PR #9784, session 003) has not merged. The robot's send route itself is live and was proven on a bench card — see the "SMS/Email Rental Application Sent" entry by Clara PropflowAI in that card's Actions Log.
Temporal tour-scheduling workflow, live run evidenceWillows test propertyNot run. Pending.

Deploy: production web is serving commit 30a22927, confirmed via the health endpoint at 2026-09-19T13:38:57Z UTC (7:38 AM MT) — that commit includes PR #9750 and PR #9761. The sync worker Lambda (propflow-appfolio-sync-prod) is running main@c7eb90d9, deployed 2026-09-19T12:58:00Z UTC (6:58 AM MT) per its function Description — that build includes PR #9750, #9761, #9785, and #9809. PR #9721 has not merged.

Duplicate prospects

The root-cause fix (PR #9809) is merged and live in the sync worker as of the deploy above. The last 3 remaining Western Slope duplicate pairs were collapsed at 7:33 AM MT today, with backups kept; zero duplicate prospect groups remain in Western Slope's data. One thing this can't touch: AppFolio can still create two guest cards for the same person on its own side — that's a duplicate inside AppFolio itself, not something fixable from ours. The replay harness that catches this class of bug going forward (PR #9771) is open, but its own review check is currently failing (red) — not yet mergeable.

Follow-up issues: #9807 (9750 follow-up, backfill terminal no-inquiry-text holds); #9812#9816 (9809 follow-ups, duplicate-fix defects); #9817#9824 and #9855 (9771 follow-ups, replay-harness test defects).

What is NOT included tonight

How to turn on in the morning

  1. Read the proof results above once they land — all three are still pending as of this report.
  2. Set Western Slope's lead-source setting to "AppFolio guest cards."
  3. Flip Western Slope's company live switch on.

How to turn off: flip the live switch off. The effect is immediate — no new replies go out from that moment on.

Known limits

PropFlow Docs