Approved by Fede 2026-09-18
Brief standalone version of the design in section 5 of the Western Slope go-live page.
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
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.
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."
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.
| Piece | State |
|---|---|
| Feed that copies every new card into PropFlow every minute | Built, running |
| Robot read endpoint the feed calls | Built for other work; not yet wired to this feed |
| Recording who actually created each card | Not built |
| The switch that starts a reply when a new card lands | Exists in code, deliberately turned off |
| The message Clara sends as the first reply | Built, currently paused by Fede |
| The company-wide on/off switch for live replies | Built |
| Graded test run against real past cards | In review |
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.
| Question | Answer |
|---|---|
| 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. |
Once Clara's first reply goes out, two things happen back in AppFolio.
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.
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.
| Piece | Owner |
|---|---|
| Reply path — intake, composer, replay | Session 000 |
| AppFolio write path — note and assign intents, drain, robot | Session 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.
| Step | State |
|---|---|
| Red main | Fixed (#9632) |
| Robot actor field | Merged, armed (#387) |
| createdBy stamp | Armed (#9633) |
| Lead-source setting | Fixing tests (#9484) |
| Composer | Armed (#9425) |
| Intake + seam | PR opening |
| Replay driver | PR opening |
| Note + assign write path (session 003) | PR opening |
| Aliases | Armed (#9643) |
| Rent-sweep | Armed (#9634) |
| Lock-in promise | Armed (#9635) |
| Weekend date | Armed (#9642); #9475 landing |
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.
The evidence, in order:
rows=9 on every tick from 12 Sep through 18 Sep 22:45:08 UTC. Then rows=10, created=1 at 18 Sep 22:46:10 UTC. From that tick on it reports unchanged=1.Received value and is never set to our own clock. It reads 18 Sep 22:45 UTC, which is 4:45 PM local, about a minute before we first saw the card. So AppFolio's report said the card was received on the 18th while the card's own activity log says the 12th. Those are two different AppFolio fields and they disagree by six days.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.
| # | What breaks | Where | Fix in one line |
|---|---|---|---|
| P0-1 | The 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:4224 | Record 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-2 | A 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:711 | Add our own "first observed" timestamp instead of overloading one field with two facts. |
| P1-1 | The 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:605 | Let the caller opt out of the automatic stamp, or drop the claim from the writer. |
| P1-2 | The "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 throw | Save the marker only after the repairs report success, or move the roll-back into the catch. |
| P1-3 | A 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-scoped | Look the card up across the client's properties before creating, and retire the record on the property it left. |
| P1-4 | Nothing 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:296 | Carry AppFolio's creator field on the synced card, the way the application sync already does. |
| P1-5 | The 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:537 | Use the metadata-returning fetch and fail the tick when the answer is truncated. |
| P1-6 | A 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:716 | Confirm what each client's status column actually contains, then match a known list instead of substrings. |
| P1-7 | A 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.ts | Compare each run's card numbers against what we hold for the property and flag the ones that vanished. |
| P2-1 | The 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, :1667 | Bump the version after the roll-back save, like every other save site. |
| P2-2 | When 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-821 | Log the error before swallowing it. |
| P2-3 | AppFolio 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:1917 | Carry the notes field through, read-only. |
| P2-4 | If 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-17 | Detect the collision and log it rather than letting last-one-wins decide. |
| P2-5 | The 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.ts | Delete the dead cadence line and correct the comments. |
| P2-6 | Cards 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:951 | Sort dateless cards last. |
P0 means it can reach a customer. P1 means data goes wrong quietly. P2 is cleanup.
Read-only audit against main at 456eddbb, plus production records and sync logs. No prospect names appear on this page.
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.
| Metric | Count |
|---|---|
| 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 grading | 12 |
| Failed grading | 2 |
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.
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.
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.
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."
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.
Full detail (evidence, file references, options considered) → Western Slope go-live, section 5.
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.
| Piece | Effect in plain words | Status |
|---|---|---|
| Sync reads the card and who created it | Every 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 cards | Clara only writes back on the automatic, listing-site cards — never one a staff member typed. | Merged (standing rule, shipped earlier) |
| Zillow relay cards skipped | Cards that came through Zillow's referral network, not a direct listing, are left alone. | Merged (standing rule, shipped earlier) |
| Company live switch holds every email | Until 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 setting | Tells 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 sent | The 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 reviewable | Even 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 worker | The 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 mailbox | Clara 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 guard | Clara is blocked from telling a prospect a tour is booked unless a tool actually booked it. | Merged (standing rule, shipped earlier) |
| What was tested | Where | Result | Evidence |
|---|---|---|---|
| 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 database | Not run. Pending — blocked on PR #9721 landing and a test inbox. | — |
| Sending the rental application after a tour (Willows application send) | Willows test property | Not 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 evidence | Willows test property | Not 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.
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).
How to turn off: flip the live switch off. The effect is immediate — no new replies go out from that moment on.