Follow-up engine investigation — 2026-09-20

What the follow-up (nudge) engine does today, what it ignores that we already know, and the shape of the work a future session would build — with the decisions that need Fede first.

Investigation only · 2026-09-20 · code read at ~/.claude/propflowai-gc-replay3 (main @ 43002444db, read-only) · numbers from live reads of DynamoDB propflow-prod (us-east-1) · every behaviour claim cites file:line

Contents

  1. Context-aware follow-ups: vision and roadmap (Proposed, 2026-10-08)
  2. How the engine works today
  3. What it ignores that we already know
  4. Camellia numbers (prod)
  5. Do-not-re-engage scenarios, and how to detect each
  6. Lead fit — the thing Camellia never needed
  7. What the team actually does today (full count, 20 Sep 2026)
  8. The dynamic-follow-up opportunity
  9. Rough shape of the work (for the hand-off, NOT underway)
  10. Context-aware follow-ups (Proposed, 2026-10-07)

Context-aware follow-ups: vision and roadmap — Decided for the tour check-in (2026-10-14); prospect follow-ups still proposed · 2026-10-08

Decided 2026-10-14 (Fede): a scheduled touch never sends blind from a timer. The timer wakes Clara's loop for that conversation with no text; the loop reads the thread and decides whether to send, skip, or close. First instance: the staff tour check-in (origin: Kat gave Natalie Haynes's tour notes at 12:48 PM and the 2:00 PM "how was the tour?" check-in fired anyway). Prospect follow-ups move to the same pattern next. PR: pending.

In this section

  1. The problem
  2. Vision
  3. What would have caught the bad texts
  4. Test results
  5. Roadmap
  6. Bugs found along the way
  7. Decision

1. The problem

Greg was handed to Kat on a call, toured, and was applying. Clara still texted him “Would love to have you come by.” Six different senders each decide on their own, and none sees the whole picture.

Call handed to KatKat promises to follow up Toured, thenstarted applying A follow-up sender runsand sees none of it “Would love to haveyou come by.”
1Call handed to Kat. Kat promises to follow up.
2Toured, then started applying.
3A follow-up sender runs and sees none of it.
4“Would love to have you come by.”

2. Vision: one check before every automatic message

EVERYTHING WE KNOW ABOUT THE LEAD Clara’s texts, emails and callsall threads, not just the newest Staff calls after a hand-off Staff takeovers Tour bookings AppFolio card status Later: AppFolio activity notesand staff texts One check beforeany automatic message,any channel STOPHandled: applied, leased, not interested,or staff took over. WAITSkip this one. The next scheduledfollow-up reads everything again. SENDWrite it from the conversation.
Everything we know about the leadClara’s texts, emails and calls (all threads, not just the newest) · staff calls after a hand-off · staff takeovers · tour bookings · AppFolio card status · later: AppFolio activity notes and staff texts.
One check before any automatic message, any channel
StopHandled: applied, leased, not interested, or staff took over.
WaitSkip this one. The next scheduled follow-up reads everything again.
SendWrite it from the conversation.

Every sender joins the same check: no-reply follow-ups, follow-ups after a reply, first replies to listing inquiries, tour messages, texts after a call, escalation callbacks.

No new lists or timers: a staff promise like “I’ll resend the application” is context the check reads, never a reminder.

Shares one reader with the platform vision; the shared reader and staff-alert model live there. Any text written from tour or staff notes goes through the same staff-only filter.

3. What would have caught the real bad texts

THE 16 REAL BAD TEXTS IN OUR TEST SET Clara’s own conversations (all, incl. staff calls and takeovers) 16 of 16 caught Add tour bookings, AppFolio card status, AppFolio notes, staff texts 0 more 58 WESTERN SLOPE LEADS LABELED “SHOULD SEND” AppFolio’s activity log shows staff had already acted before the text 17 of 58 (9 strong) Strong = staff-scheduled showings, card set Inactive.
Clara’s own conversationsWould have caught 16 of 16 real bad texts, including staff calls and takeovers.
Tour bookings, card status, notes, staff textsAdded 0 more on these cases.
Western Slope, 58 “should send” leadsAppFolio’s log shows staff had already acted on 17 before the text (9 strong).

So AppFolio notes matter; we just haven’t labeled with them yet.

4. Test results, honest

VersionBad texts caughtGood leads wrongly skipped
Today2 of 310 of 195
“Send unless something says no” check, reading what we already have31 of 31 (3 runs, identical)19 of 195 (13–14 of the 23 tricky ones)

The 19 is not trustworthy yet. The test cut long messages at 500 characters (7 cases), and the “should send” labels were set without AppFolio notes, so some “wrong skips” may be right. Fixing the test comes first.

Weak spot: JP&Co tricky leads (7–8 of 16 wrongly skipped).

5. Roadmap, most impactful first

0 · 1 DAYFix the testTrustworthynumbers 1 · 2–3 DAYSMinimum sliceOne check, sixfollow-up senders 2 · 2 DAYSThree sendersthat bypasseverything 3 · 3–5 DAYSWrite from theconversation 4 · 1 WEEKAppFolioactivity notes 5 · DAYSAppFoliostaff texts
#StepWhat it addsSizeGate
0Fix the test: no cutting messages, re-label with AppFolio notes, re-scoreTrustworthy numbers1 dayNone (local only)
1Minimum slice: one check in front of the six follow-up senders, reading all of the lead’s Clara conversations, tour bookings and AppFolio card status; stop / wait / sendCatches all 16 real bad texts in testing2–3 daysShips live once the fixed test shows wrongly skipped good leads near zero. No switch (a fix to live behavior).
2Route the three senders that bypass everything (texts after a call, escalation callback, tour messages) through the same checkSame coverage for every message2 daysAfter step 1
3Write the text from the conversation instead of the generic oneFixes “robotic and generic”3–5 daysQuality check passes 77% today; needs a higher bar first
4Bring AppFolio activity notes into the lead pictureStaff calls and showings logged only in AppFolio (17 of 58 Western Slope leads)1 weekCareful with AppFolio request limits
5Turn on the AppFolio staff-text feed per customerStaff texts sent from AppFolioDaysTurning it on at a customer is Fede’s call
0 · 1 day · no gateFix the test: no cutting messages, re-label with AppFolio notes, re-score. Adds trustworthy numbers.
1 · Minimum slice · 2–3 daysOne check in front of the six follow-up senders, reading all of the lead’s Clara conversations, tour bookings and AppFolio card status. Catches all 16 real bad texts in testing. Ships live once the fixed test shows wrongly skipped good leads near zero; no switch (a fix to live behavior).
2 · 2 daysRoute the three senders that bypass everything (texts after a call, escalation callback, tour messages) through the same check. Same coverage for every message.
3 · 3–5 daysWrite the text from the conversation instead of the generic one. Fixes “robotic and generic.” Quality check passes 77% today; needs a higher bar first.
4 · 1 weekBring AppFolio activity notes into the lead picture (17 of 58 Western Slope leads). Careful with AppFolio request limits.
5 · daysTurn on the AppFolio staff-text feed per customer. Turning it on at a customer is Fede’s call.

6. Bugs found along the way

Separate, small, and each needs Fede’s go.

7. Decision

Start with step 0 (fix the test), then step 1? yes / hold.

1. How the engine works today (10 lines)

  1. Trigger. One entrypoint, onProspectEngaged() — src/lib/domain/leasing/outreach-trigger.ts:319. Fired after Clara's first reply on a text/email turn (agents/clara/lib/agent/conversation-manager.ts:13587), after a leasing call ends (src/app/api/voice/call-ended/route.ts:2460), and by a nightly self-healing reconciler (outreach-reconcile.ts:609,740,825).
  2. Steps. Four SMS touches at +24h, +48h, +7d, +14d from the anchor — src/lib/domain/leasing/outreach-cadence-schedule.ts:101-106. Step 1 of the "5-step" story is Clara's first reply, sent outside the cadence.
  3. Channel. Every step is prefer_sms; falls back to email only if no texting consent, and is dropped entirely if neither exists — resolve-outreach-channel.ts:75-110.
  4. Timing guards. 20h minimum real gap between sends (outreach-cadence-schedule.ts:53), quiet hours 8am–9pm property-local (prospect-outreach-workflow.ts:349,418), 12h cooldown if anything else already texted that person (autonomous-outreach-cooldown.ts:26,125).
  5. Per-property gating: none. Removed 2026-07-27 (outreach-trigger.ts:12-17). The only operator lever is an org-level cadence editor that sets step delays/channels — FOLLOWUP#<orgId> / CADENCE#<scenarioId> (followup-cadence-overrides.ts:24), states default | custom | disabled.
  6. Stop conditions — exactly three reasons. outreach-stop-conditions.ts:47: 'applied' | 'tour_booked' | 'closed'. Applied = appliedAt, householdAppliedAt, or stage applied/approved/lease_signed (:67-71). Tour booked = stage tour_pending/tour_confirmed, or any tourId that isn't cancelled (:82-84). Closed = stage rejected or not_interested (:90). A cancelled tour deliberately restarts the cadence (:94).
  7. Plus operational skips (don't stop the cadence): no consent, no phone, no mailbox, suppression list, cooldown, merged/deleted prospect — full union at src/lib/temporal/activities/prospect-outreach.ts:156.
  8. Copy is LLM-written, not hardcoded. Haiku, 120 tokens for SMS, temp 0.7, no tools — generate-followup-message.ts:447-549, called from prospect-outreach.ts:786,936. The hardcoded strings (src/lib/i18n/templates.ts:965-1001) are safety fallbacks only.
  9. But it is told it knows nothing. The prompt explicitly states it has no inventory (generate-followup-message.ts:408-411), and anti-fabrication guards force the generic fallback if the copy names a price, a unit type, a policy (vouchers/income/evictions), or a tour time (:123-125, :214-215, :225-241, :259-264).
  10. What it actually sees per touch — prospect-outreach.ts:656-662: first name, property name, touch index, desiredBedrooms, desiredMoveIn, days since inquiry, Clara's own summary notes (aiNotes), language, and an optional grounded price drop. That's the whole context.

2. What it ignores that we already know

The guest card carries these and the follow-up never reads any of them:

We storeWherePassed to the touch?
Pets (leadFacts.pets)types.ts:18357No. The petsMentioned slot exists (generate-followup-message.ts:56) and renders into the prompt (:434) — the outreach lane just never sets it, so it is always "no".
Budget / income (leadFacts.budget, monthlyIncome)types.ts:18363, 18694No — no field on the context at all.
The home they asked about (targetUnitId, targetHomeAddress, targetListingId)types.ts:18649, 18667, 18691No. Never read by the activity or the generator.
Whether that home is still availableresolveListingCardForReply() — knowledge/listing-card-for-reply.ts:113-142No. No availability read exists anywhere in sendOutreachTouchActivity.
The actual conversationconversationTail field exists (generate-followup-message.ts:74)No — documented "reengagement only", never populated by this lane.
Lease length, credit band, free-text preferencestypes.ts:18359, 18361, 18401No.
Bedrooms, move-in datetypes.ts:18318, 18330Yes — the only two needs that reach the copy.

Two structural gaps behind this:

3. Camellia numbers (prod, propflow-prod, property 1773625953462 / org_jpco)

Population, last 60 days (inquiries with inquiryDate >= 2026-07-22, query: GSI phone-index, GSI1PK=PROPERTY#1773625953462, begins_with(GSI1SK,'INQUIRY#')):

To reach a ≥30 sample I widened to every touched prospect in the table (34, inquiries 2026-06-01 → 2026-09-18). Classification of the state each prospect was in at their first touch, from the conversation transcript (PK=CONV#<id>, SK prefix MSG#) plus the inquiry row:

State when touchedCountHow I detected it
Ordinary quiet — lead email in, Clara replied, no answer26exactly one inbound message (the portal lead email) before the first touch
Had a live back-and-forth, then went quiet6≥2 inbound messages before the first touch
Home/unit gone or unavailable0regex for "no longer available / already leased / off the market" across all 34 transcripts: zero matches
We said no to a hard need (pets, budget, bedrooms, move-in, voucher)0regex for pet/income/voucher refusals: zero real matches (the one voucher thread, aa4f20e9, ended in yes, vouchers accepted)
Already applied / approved / signed when touched0compared every sent-touch timestamp against appliedAt, approvedAt, signedAt, pmsApplicationReceivedAt
Tour already booked when touched0compared every sent-touch timestamp against Tour.createdAt for confirmed tours on the same personId
Prospect said "not interested"0regex over transcripts
Prospect never got an answer to their question0every thread has a Clara reply before the first touch

Touches that went to a plainly-dead lead or one we had already said no to: 0 of 62. The three stop conditions that exist are working. Camellia does not exercise the failure mode Fede is worried about — and that is the finding: Camellia is one building with fungible studios and 1-beds, so "the home they asked about" never goes away. Western Slope, scattered single-family homes, has no equivalent safety.

Five concrete examples (record ids are ProspectInquiry.id; key PK=PERSON#<personId>, SK=INQUIRY#<id>):

  1. c247fb86-edc5-4afd-a773-62e086ffb49e (Erling, Apartments.com, 2026-08-30). His lead said "(Unit 524/Model 1BR/1.0BA) … particularly on the 5th floor." The +24h touch said: "Thanks for reaching out about a 1-bed with us." Unit 524 and the 5th floor vanish. He booked a tour off the second touch. The cadence worked — it just threw away the one thing he told us.
  2. d2123015-fcfd-5e7e-8247-4728ccd41f35 (Crystal, Zillow, 2026-09-14). Two generic touches; she replied to the second and booked. Touch 2 said "check in and see if you had any questions about us" — no reference to the studio/1BR pricing Clara had quoted her by email a day earlier.
  3. da49aa9e-18b2-43a8-9c38-c32cb5affdac (Sumanth, 2026-08-01). Asked "how much time from Union Station". The follow-up: "we'd love to show you around in person." The distinctive thing he cared about is not referenced.
  4. aa4f20e9-2e37-4e8c-a0f8-d6b07bcc7ba6 (Daniel Brehm, 2026-08-25). The one voucher case. He was told at his tour that we don't take vouchers; Clara corrected it the same evening ("Camellia absolutely accepts housing choice vouchers"), then walked it back, then confirmed. No touches followed — the tour had already stopped the cadence. Not harm; it is the case that shows voucher status is never a stored fact (guest-card-note-classifier.ts:191 forces it internal-only as a protected class).
  5. fe11da47-dfd7-411d-8a21-b363a198d7e0 (Summer, 2026-08-31). Clara told her parking is full with a waitlist, then sent all four generic touches. The follow-ups never acknowledge the one friction point in the thread.

The real repetition defect is in the post-tour chase, not the cadence. 16 of 77 Camellia tours received ≥2 post-tour "did you apply?" chases and 13 received exactly 3 (Tour.postTourChasesSent, types.ts:11910). Those three messages are byte-identical. Two proven cases, from PK=CONV#<conversationId> / SK prefix MSG#:

No coworker sends the same sentence three times.

Scope note: cadence state exists in four prod orgs — org_jpco 87 inquiries, org_53047a06-…9149a 46, org_c42d9150-…acf38 15, org_sandbox 290 (full-table scan, 1.6M rows, filter entityType=ProspectInquiry). Western Slope's real org has no cadence traffic yet; only org_ws_mirror_test (2). So today's engine has been proven on exactly one building.

4. Do-not-re-engage scenarios, and how to detect each from data we already have

Ordered by how often they will bite Western Slope. "Buildable today" means every input already exists in prod.

#ScenarioDetect it withBuildable today?
1The home they asked about is rented or delisted, and it's the only one they named.resolveListingCardForReply(property, targetHomeAddress, today, targetListingId) returns kind:'none' — knowledge/listing-card-for-reply.ts:113-142.Yes for scattered-site orgs. No for building properties — that helper short-circuits to '' at :124, and there is no per-unit "is this unit still offerable" predicate; nearest is canTourUnit() (future-availability.ts:177).
2We already told them it's gone and offered alternatives, and they didn't answer. Re-pinging is the "are you still interested in the thing I told you doesn't exist" message.Today only by string-matching the outbound body for the mandated phrase "is no longer available" (clara-email-core.ts:393-405). Needs a stored fact.Needs a new signal (small).
3We said no to a hard requirement: pet not allowed, bedroom count we don't have, move-in date we can't meet, rent above what we have.Nothing structural exists. leadFacts.pets/budget, desiredBedrooms, desiredMoveIn are stored (types.ts:18318,18330,18357,18363) but no refusal is ever recorded.Needs a new signal.
4They told us they're out — "found a place", "not interested", "stop".Only if something writes stage not_interested/rejected (outreach-stop-conditions.ts:90). Nothing parses reply text for it in the stage-1 lane. The stage-2 lane already has the classifier: reengagement-intent.ts:80,111,145,157,177 produces lead_will_initiate | deferred_to_future | not_interested | we_owe_reply | ambiguous and it fired 8 times in 60 days at Camellia.Reuse the existing classifier — cheap.
5They asked us something we never answered. Nudging before answering is the rudest version.reengagementSuppressedSignal = 'we_owe_reply' already exists (types.ts:18917) but only in the stage-2 lane.Reuse — cheap.
6They toured and didn't come back — needs a different message than "want to take a look?", which we already showed them.A confirmed tour stops the stage-1 cadence (:82-84), so this is handled — but the post-tour chase then sends the same sentence 3×.Yes, copy fix.
7Move-in date has passed. Someone who needed a home by Aug 28 does not want a nudge on Sep 20.desiredMoveIn vs today. Trivially available, never checked.Yes, trivial.
8Nothing in the portfolio matches them today (no 3-beds, nothing under their budget, no pet-friendly home). Re-engaging with "still looking?" when we have nothing to show is a wasted text.getOrgOfferableHomes() / getCompanyLineHomes() (org-offerable-homes.ts:172, company-line-homes.ts:94) + desiredBedrooms/budget. Both return null for refusal when the roster is stale (>7 days), which must not be read as "nothing available".Yes for scattered-site.
9They're already a resident, a vendor, or staff.Already handled at enrolment — outreach-enrollment-identity-gate.ts:48-101.Done.

4b. Lead fit — the thing Camellia never needed

Camellia never needed this because "everyone can just apply to the community". Western Slope is different asset types and you have to match a person to a home, so "there has to be some basic logic of: are they even eligible, do we match them to something they want, are they a good fit… if we're not what they're looking for, we shouldn't be just following up blindly."

Fede, 2026-09-20

Ruling (Fede, 20 Sep 2026, 5 PM, after the team sample below): a short-term lease ask is answered with one sentence that it is something they can check with the leasing team, plus the usual tour invite and one note on the card. Clara does not state that 12 months is standard and does not say no; the lead is followed up normally. The broader 'a policy no filters the funnel' idea below stays provisional for other criteria (income, pets, credit). When the client's knowledge base answers a lead's ask—for example, a six-month lease at Western Slope, whose policy is 12 months standard with 14–18 month promotional terms at Ember Estates—Clara answers flat in one sentence with no deferral, leaves one plain note on the guest card ("Asked for a six-month lease; told them we don't offer it, 12 months standard"), nothing routed, no task, no alert, and the follow-up cadence does not chase that lead. Humans see the note only if they open the card. The same rule covers every objective policy or screening criterion the client gave us (income at about twice the rent, pets, credit or rental history): when a lead's message shows they don't meet it, Clara says so plainly so they don't waste time applying and getting declined. The one exception is fair housing: Clara never pre-screens on protected characteristics or source of income; a voucher or any lawful income counts as income, and on a judgment call she states the criterion and lets the lead decide. "we're helping filter the funnel to connect people only that match our policies to our open homes." (Fede, 2026-09-20) Sequenced after home matching (Lane B3).

What the team actually does today (full count, 20 Sep 2026)

Coverage: Western Slope — 1,226 guest cards read (~96% of all of 2026); 49 cards from Sep 9–19, 2026 could not be pulled (login/2FA gap, open follow-up). Situs Group — all 1,641 known cards captured, 1,096 of them in the 12-month window.

The earlier sample undercounted because it only looked for a short-lease ask written in the prospect's prose and ignored the lease length the renter picks on the Zillow / Apartments.com / Rent.com inquiry form itself — that field is the renter's own choice, not a system default, and this recount treats every value under 12 months as a real ask.

ValueWestern Slope (n=1,226 read)Situs Group (n=1,096 in-window)
1 month30
6 months10
9 months00
12 months2516
18 months84
24+ months00
Not stated (no field / non-Zillow-style source)1,1891,076
ClientCardDateSourceThe askTeam's reply (verbatim)Mentions term?Outcome
WSR.09/01/2026ZillowField: 1 mo; wanted a furnished unit tour"Hi, thank you for your interest in Northridge Apartments. What is your best time for a showing? -Bev"NoTour offered anyway, term never addressed
WST.09/03/2026ZillowField: 6 moTeam did a virtual tour of the 2-bed unit (call note 09/05)NoTour offered anyway, term never addressed
WSM.08/25/2026ZillowField: 1 moNone logged—No reply
WSB.08/14/2026ZillowField: 1 moNone logged—No reply
WSM.04/15/2026prose (existing tenant, near lease end)"I'm wondering if it would be possible to do a month to month in the off chance that we can't find anything before the lease is up.""Not a problem! The month-to-month fee will be $150 on top of your recurring fees… Congratulations on the baby! -Beverly"YesAccepted
WSW.unspecified 2026prose (underwriting)Team-proposed 6-month term as an approval condition"We would propose a 6-month lease with double security deposit… and would still offer you the rent concession equal to a half month."YesCountered (team-initiated, not a prospect ask)
WSA.12/01/2025prose"Oh that's a plus, could we do a 6 month lease""You can but then it's only a free half month."YesAccepted (reduced concession)
WSB.08/18/2026prose (existing tenant, holdover)"I was wondering about the lease situation for the one month""the month to month fee of $150 will be added automatically and yes the security deposit stays with the house."YesAccepted
WSC.unspecified 2026prose (renewal-adjacent)Team-initiated 6-month term with reduced concession"my boss said we can offer free half month rent since they're only doing a 6 month lease."YesAccepted
WSM.03/13/2026prose (existing tenant, holdover)"we may need to stay a couple months month-to-month""I appreciate you letting us know… You mentioned before that you will be staying for a couple of months on a month-to-month basis. I sent you an email with the unit price this year."YesAccepted
WSK.03/17/2026Realtor.com"6 months is ideal, but can work with more or less. We can pay 6 months up front""Congratulations on the sale! I have rented the unit… I'd only be able to let you have that for 1 1/2 months. I do have a 4 bed 2 1/2 bath house… coming by hopefully April 1st?"YesCountered (offered an alternative unit/timeline)
WSB.unspecified 2026proseTeam-initiated: "we could definitely do a 6 month lease"(reply is the offer itself)YesAccepted
WSL.09/19/2026Rent.com"Was wondering if you'd consider a 6 month lease with option to extend. Can prove funds for initial 6 months."None yet — same-day inquiry at capture time—No reply (too recent)
WSC.~08/2026Zillow/Apartments.com, 3 property inquiries"We are self employed and in need of a rental for 9 to 12 months while we build are home… We just sold our current home in a day unexpectedly."Kat Barker called and talked through the move, then sent an ordinary scheduling text; no mention of a 12-month minimum or refusal anywhereNoTour offered anyway, term never addressed; all 3 inquiries eventually closed for no response
OutcomeWestern SlopeSitus Group
Tour offered anyway, term never addressed30
Term addressed — accepted60
Term addressed — countered20
Term addressed — declined01 (not a genuine short ask — see above)
No reply31 (not a genuine short ask — asked for a longer term)

Reasons volunteered for wanting a short lease (Western Slope only — Situs Group had no genuine short-lease asks to attach a reason to):

(a) Three levels, and where each input lives today

LevelQuestionInputsWhere they liveReady?
EligibleDo our hard rules allow this person at all? Lease term, pets, income/voucher, smoking, occupancy.The client's approved knowledge base, company-level + per-property overrideswestern-slope-knowledge-gold-set.json → approvedCompanyEntries / approvedPropertyEntries (19 company, 7 property entries). Prospect side: leadFacts.pets types.ts:18357, monthlyIncome :18694, lease-term asks only as free text in the card message.Partly. The rules are written down and approved. Nothing machine-readable: they are prose entries, there is no structured predicate, and voucher is deliberately unstorable on the prospect (protected class — guest-card-note-classifier.ts:191).
MatchedIs there an open home that fits beds / budget / move-in / area?Card: desiredBedrooms :18318, desiredMoveIn :18330, leadFacts.budget :18363, targetHomeAddress/targetListingId (#10122) :18667,18691. Inventory: getOrgOfferableHomes() org-offerable-homes.ts:172, getCompanyLineHomes() company-line-homes.ts:94, resolveListingCardForReply() listing-card-for-reply.ts:113.The inventory half exists; the card half is thin. Across 908 real WS cards, maxRent is set on 16, a bed/bath preference on 69, a pet preference on 47. Most of what we know is buried in the free-text message (290 of 908 have any).
Good fitSoft: pet-friendly home for a pet owner, timing that works, home type they'd actually like.Same inventory reads + preferences[] :18401 + the conversationNo. No soft-scoring exists anywhere; preferences[] is never read at follow-up time.

Two blockers worth naming: targetHomeAddress lands on effectively zero live cards because resolveGuestCardHome answers no_roster for every currently-live property (guest-card-home.ts:94-109), and there is no store today that can answer "what is the cheapest open WS home" — the corpus carries no rents and the org listings ledger isn't populated for WS.

(b) Counts on the real Western Slope corpus

Source: ~/.claude/guest-card-corpus/option-d/western-slope/guest-cards.json — 908 AppFolio guest cards, org_western_slope, pulled 2026-09-20 (the "50" is propflowMirrorRows, the PropFlow-side mirror subset). 290 carry free text (message, notes or inquiry message). Plus the 51-card 2026 prod replay set in raw/western-slope-dataset.json.

Fit missCardsIds
Asked for a lease term other than 12 months22943 (566 Placer Street — "Was wondering if you'd consider a 6 month lease with option to extend. Can prove funds for initial 6 months."), 2456 (632 Hill Avenue — PM note: "wants a 6 month lease and move in maybe next week") — plus 2838 (2863 Florida Street) asking for "9 to 12 months while we build our home"
Budget plainly below anything WS lists1 identifiable2931 (Pomona Park Townhomes — "BEDS: 1 | BUDGET: $600"). 5 other budget-flavoured cards (2836, 2778, 2796, 2939, 2845) cannot be judged: no store we can query holds WS's open-home rents, so "below the cheapest open home" is not computable today. That is the finding, not a zero.
Has a pet, at a home where the KB says no pets073 cards show a pet (47 via the petPreference field, the rest in free text). The only "No pets" rule in the approved KB is 632 Hill Avenue, and no pet card landed there. Pet cards cluster on Northridge (12), 2863 Florida (9), 2978 Krista (7).
Bedroom count we have nothing open fornot computable69 cards state a bed preference; with no open-home roster there is nothing to compare against.
Voucher3(2827, 1365, 2715) — Not a fit miss — the KB says "Housing vouchers and other lawful income count the same as any other income."
Eligible but unmatched (nothing open for their timing)at least 1 proven, in the replay set7428dd7b-c43d-5d80-9a3b-5bd06af563d8 / card 2943, Ethan, 566 Placer Street. Clara's live reply: "Right now I don't have any homes available that match that timeline, but I'd love to help you find the right fit here." And it never answered his 6-month-lease question at all. Under today's engine this lead then enters the same +24h/+48h/+7d/+14d "still looking?" cadence — with nothing to show him.

What the WS knowledge base actually says about lease terms

Do not assume 12-month-only. The client's own approved answer (western-slope-knowledge-gold-set.json, rawAnswers rank 10007, factKey: knowledge.lease-length), verdict "Wrong" with the client's correction:

"12 months is the standard lease term. Shorter terms, like 6 or 10 months, are actively offered as an alternative depending on the applicant's situation, usually with a smaller move-in incentive than a full 12-month lease gets. Going month-to-month after your lease ends adds $150 a month on top of rent."

client correction: "this is mostly correct however at ember estates for our promotional offering we are doing longer term leases of 14 18 months."

The short approved company entry says only "Lease length :: 12 months standard.", and the one property override is Ember Estates :: "Longer leases of 14 to 18 months currently offered as a promotion."

So a six-month ask at Western Slope is not a rule violation — it is an "ask the team" case, and the condensed company entry is what would make us wrongly say no. That is an argument about the shape of the knowledge entry as much as about the engine.

(c) How fit should drive the first reply and the follow-up

5. The dynamic-follow-up opportunity

The copy generator already has slots for most of this; the outreach activity simply doesn't fill them. Five things a touch could say, and where the data is:

A touch could referenceDataWhere it lives
The specific home they asked about — "still thinking about 812 Pine?"targetHomeAddress / targetListingId / targetUnitIdtypes.ts:18649,18667,18691. Caveat: targetHomeAddress lands on ~zero live cards today because resolveGuestCardHome answers no_roster for every currently-live property (guest-card-home.ts:94-109) — that is a Western Slope prerequisite, not a nice-to-have.
Their dog — "and yes, 812 Pine is fine with a 40lb dog"leadFacts.pets (free text)types.ts:18357; slot petsMentioned already in the prompt at generate-followup-message.ts:56,434
Their bedroom count and move-in — already passed, barely useddesiredBedrooms, desiredMoveIn, moveInNotetypes.ts:18318,18330,18339; already at prospect-outreach.ts:657-658
A new home that matches them — "a 3-bed just came up on Orchard"getOrgOfferableHomes() / getCompanyLineHomes() filtered by their needsorg-offerable-homes.ts:172, company-line-homes.ts:94. This turns the 4th touch from a nag into news.
The home they wanted coming back / a price dropprice drop is already wired (resolvePriceDropForOutreach, prospect-outreach.ts:672-688); "came back" needs a listing-state transitionprice drop: done. Return-to-market: derivable from the org listings ledger, not stored as an event today.
What they actually asked us — travel time, parking, laundryconversationTail (exists, unused in this lane) or preferences[]generate-followup-message.ts:74; types.ts:18401

One constraint to design around: the anti-fabrication guards will reject any of this unless it is passed as grounded system data. Naming a unit type falls back today (:225-241); naming a price falls back unless it matches a grounded priceDrop (:164-182). So "make the copy dynamic" is really "widen the grounded-context contract", not "loosen the prompt".

6. Rough shape of the work (for the hand-off, NOT underway)

Fede, 2026-09-20: this is investigation only; no PR from this list exists or is authorized.

Contract-first, each ≤~300 lines, one concern, each dark and mergeable alone.

Lane A — signals (write the facts down)

Lane B — decision (stop the wrong touches)

Lane C — copy (make it worth reading)

Proof for every PR: replay through the existing guest-card corpus + judge (/guest-card-regression), each check shipping with a planted-violation control.

Decisions for Fede (multiple choice, recommendation in bold)

D1 — When the home they asked about is gone and they never answered, what happens?

→ Recommend (b). A scattered-site lead who wanted one house usually wants a house. Stopping loses a real lead; nudging about a dead house is the embarrassment. (b) avoids both, and (c) folds into it.

D2 — When we said no to a hard requirement (pets, bedrooms, budget, date), what happens?

→ Recommend (b). It is the only one a human would do, and the inventory read it needs is the same one D1(b) needs.

D3 — Where do these rules live?

→ Recommend (a), shipped dark then defaulted on. These are not preferences, they are correctness — no customer wants a text about a house that's gone. Per the no-engineering-flags rule, ship it merged and dark as a company setting, turn it on per customer, then retire the setting once it's proven.

D4 — How dynamic should the copy get before we trust it?

→ Recommend (b) first, (c) right behind it, both graded on the guest-card corpus before anything is turned on. (c) is where the value is, but it is the first time a follow-up asserts inventory, so it needs the corpus judge at the same bar we used for the guest-card replies (0 do-not-send, ≥90% send-as-is).

D5 — The six-month-lease class (a card asks for something the condensed rule says we don't do).

→ Recommend (c), with (b) as the wording we use while we wait. The prod data settles it: Western Slope's own approved answer says shorter terms "like 6 or 10 months, are actively offered as an alternative depending on the applicant's situation" — so (a) would have given a wrong hard no to card 2943, a lead who could prove six months of funds. Two cards in 908 asked this, which is too few to justify a rule engine and exactly the right volume for ask-once-and-remember. It also fixes the class, not the case: every "does the client allow X?" gap lands in the same place, visible and explicit the way Fede asked ("keep it very on"), instead of Clara guessing in either direction.

Two things the hand-off session must not assume

7. Context-aware follow-ups — Proposed, pending review · 2026-10-07

In this section

  1. The story
  2. Smallest version to start
  3. How it works today
  4. Signals we have
  5. How often
  6. Options
  7. Proof plan
  8. Two related bugs

Clara sometimes texts “come see the home” to people staff already talked to, toured, or who already applied. This section proposes making follow-ups aware of that.

The story: Greg, Western Slope, Oct 6–8

Oct 6Greg calls; Claratransfers him toKatKNEWA call wastransferred tostaffIGNOREDWhat Kat said:application is onits wayOct 6Greg applies, on aseparate recordKNEWApplication savedin our systemIGNOREDIt is not linked tothe caller (newphone and email)Oct 7Clara texts: comeby and take a lookKNEWNothing new to goonIGNOREDThe call and theapplicationOct 7Greg: we alreadytoured with KatKNEWClara read it andansweredIGNOREDNothing; this isthe first time itsurfacesOct 7Good reply dropped;generic closer sentKNEWA good reply waswrittenIGNOREDThe safety checkkept only theclosing lineOct 8Quiet-lead nudgerestartsKNEWGreg replied, thenwent quietIGNOREDThat his reply saidhe already toured

Smallest version to start

Step 1 (smallest). Before each “no reply” follow-up, stop the sequence if a staff member has been in touch since it started: a call transferred to staff, a completed tour, or tour notes from staff. It uses data we already store and no new AI. It would have stopped Greg, Tessa, the unknown caller and the after-tour cases: about 11 of Western Slope’s 70 follow-up texts in 30 days (estimate; "completed tour" is inferred from a confirmed tour in the past).

Step 2. Widen the “already applied” check to the whole household and to other records of the same person. This catches Tessa’s case (an application on a sibling record). It would not have caught Greg at send time: his application sat on a record with a different phone and email, and joined the household 1h35m later. Step 1 covers him.

Step 3. Give the follow-up writer the full context (thread, staff call summary, tour notes) so it writes the right message or skips.

Alongside: fix the dropped-reply bug below. These are fixes to live behavior, so they ship with no switch.

How follow-ups work today

LOOP 1: THE LEAD NEVER ANSWEREDEnrolled afterClara's firstreply or a callText+24 hoursText+48 hoursText+7 daysText+14 daysBefore every text, stop if:applied, closed, tour booked,or they repliedThe text is written from: first name, home, move-in date, short summary. Never the conversation.LOOP 2: THE LEAD REPLIED, THEN WENT QUIETThey reply(loop 1 ends)Wait 24 hoursafter the replyAI check: are theywaiting on us?Awaiting us: nudgeElse: holdNot interested or later:end the sequenceNeither loop looks at: a call transferred to staff, a completed tour, tour notes, or applications on other records.

Signals: what we have, what follow-ups use

SignalDo we have it?Follow-up uses it?Effort to wire in
Staff call after a transferYes: the transcript is saved on the thread; the structured summary is only in the activity logNo (only counts as a recent message for the 12-hour pause)Small for “a call happened”; medium for what was said
Staff tour notesYes: 7 of 8 check-ins answered at Western Slope in 30 daysNo: saved, informational onlySmall
ApplicationsYes, synced from AppFolioOnly when on the same inquirySmall to medium
Household and other records of the personPartly: households form from application groups, often after the factOnly a copy on the same inquiryMedium
Full conversation historyYesNo for the first sequence; the last few turns for the re-engagement loopSmall
AppFolio staff notesNo: the import is switched offNoMedium, and it needs a go from Fede
Staff texts and emailsYes, where messaging sync is onOnly a hold at the final sendSmall

How often (last 30 days)

Measured: the texts and their timing. Estimated: “completed tour” and “same person.” Not covered: leads with no saved thread.

Options (pick one)

A. Stop rules only. New stop reasons from the staff call, tour notes and a wider applied check. Small, predictable, cannot write a wrong message. It cannot say the right thing when the lead is still worth a text.

B. Context-aware writer. The writer sees the full thread, tour notes and staff call summary, and decides send, skip, or what to say. Most flexible, but it is new AI judgment on a customer-facing text.

C. Both, A first. Recommended. A is the minimal production step (Steps 1–2 above); B follows once the test results below look good (Step 3).

Proof plan

A test harness (being built now) replays Greg, Tessa and the other Western Slope cases with the expected outcome for each (skip, or send with the right content), and compares today’s behavior to the proposed one. Every check gets a planted-violation control.

Harness results (2026-10-07)

TodayProposed
All 18 real cases8/1818/18
Should skip (8: already applied, toured, or staff handling)0/88/8
Should send (10 quiet leads)8/1010/10

Two related bugs

PropFlow Docs