0060 — Stale-lead call list eligibility is the human phone layer, not Clara's bot gates
- Status: Accepted
- Date: 2026-06-18
- Deciders: Fede (+ Sean, Gera — 2026-06-17 leasing standup)
- Trello: "Weekly stale-lead digest for PM manual callback"
Context
PropFlow ships a weekly stale-lead call list — a prioritized email to a property's
leasing contact telling a human PM who to phone (src/lib/domain/leasing/stale-lead-digest/).
It ships disabled behind three independent gates: no vercel.json cron entry, a global
env arm (LEASING_DIGEST_AUTONOMOUS_SENDING=armed), and a per-property flag
(Property.autonomousLeasingDigestEnabled). The requirement came out of the 2026-06-15
cofounder standup: PMs assume "the AI did its job" and stop making the closing calls, and
occupancy is trending down — so the human still needs a ranked, contextual call sheet for
the leads Clara's automation can't close on its own.
This list sits downstream of Clara's automated cadence, and that boundary is the whole
point. Clara's only automated touch for a prospect is the first-reply follow-up
(first-reply-followup.ts): she replies on inquiry (claraEngagedAt), and ~48h later — if
the lead hasn't answered and is still at INQUIRY — sends one more nudge. That path is
gated to online-listing sources (WEBSITE/ZILLOW/APARTMENTS_COM) and deduped by
firstReplyFollowUpSentAt. After that single nudge, automated outreach stops. The call
list's 7-day recency floor is deliberately the handoff line: days 0–7 are Clara's
automated window; days 7–30 are the human's.
Three things are unresolved:
- The eligibility principle is only an inline comment. On 2026-06-17 we decided the
human list is source-agnostic and must not inherit Clara's automated-texting gates
(
isStageEligible,firstReplyFollowUpSentAt). Those gates answer "when may a bot text?" — a different question from "who should a human call?". That decision lives only as a code comment inbuild-call-list.ts(Cohort C). A future contributor can silently re-couple them. - No phone-required gate.
buildStaleLeadCallListincludes leads with no resolvable phone; the email renders a "No phone on file" row with no call icon. On a call list, a lead you cannot dial is noise. - No consent posture is written down. A lead who texted STOP (
SmsConsentRecord.revokedAt) still appears with atel:link, and there is nodoNotCall/doNotContactconcept anywhere in the codebase — the only opt-out signal that exists is SMS-keyed.
Decision
The stale-lead call list is the human phone layer. Its eligibility is its own — defined by cohort + recency + callability, and deliberately decoupled from the gates that govern when Clara's automation may text a lead.
A lead is eligible for the call list when all hold:
- Cohort membership (deduped, hottest reason wins: A → C → B):
- A — toured, not applied: a completed (past), non-cancelled tour and no application.
- C — ghosted after first reply: Clara engaged (
claraEngagedAt), no tour, not applied, then went quiet. - B — inquired, not toured: has a prospect row, no non-cancelled tour, pre-terminal stage.
- Recency window: last contact is between
minDaysSinceContact(default 7) andmaxDaysSinceContact(default 30) days ago. - Not terminal: stage is not
applied/approved/lease_signed/rejected/not_interested, and no application is detected (appliedAt/signedAt/af.rentalApplicationId). - Callable: the lead has a dialable phone number. A lead with no resolvable phone is
not eligible — you cannot call them. (They remain visible on the
/leasing/prospectspipeline; they are simply not on a phone list.) - Not suppressed: the lead is not on a genuine do-not-contact list.
Three rules a future change must not violate:
- The builder MUST NOT consult Clara's automated-outreach gates for membership. Cohort C
is source-agnostic and includes leads Clara has already auto-nudged
(
firstReplyFollowUpSentAtset). A lead who ghosted even after Clara's auto-text is the strongest call candidate, not one to hide. - An entry with no dialable phone MUST NOT appear on the call list.
- An SMS opt-out (STOP /
SmsConsentRecord.revokedAt) MUST NOT, on its own, suppress a lead from this list. A human manually dialing is not a TCPA-regulated automated/robocall, and STOP is consent to stop texts, not a do-not-call request. A genuine do-not-contact signal — distinct from SMS-STOP — will suppress a lead when one exists. The builder exposes the seam for it today (suppressPersonIds, empty by default); the runner is where a real signal will be resolved and passed in.
This ADR proposes no new persisted entities. suppressPersonIds is a function parameter, not
an entity, so the entity-classification subsection is omitted.
Consequences
- The call list contains only leads a PM can actually phone. The email's "No phone on file" branch becomes defensive-only for the cron flow (it still guards a number that passes the digit gate but fails formatting), and is retained.
- SMS-opted-out leads stay on the human list — locked by a regression test so a future "helpful" re-add of the SMS gate breaks the build instead of silently narrowing the list.
- The decoupling-from-bot-gates decision is now enforceable: a test asserts an already-nudged, non-listing Cohort-C lead still appears.
- Follow-up (tracked, not in this change): introduce a real
doNotContactsignal distinct fromSmsConsentRecord(SMS-STOP), resolve it inrun-digest.ts, and pass it viasuppressPersonIds. The seam built here is exactly where it plugs in. - No go-live: this change does not wire the
vercel.jsoncron or arm any gate. The list stays disabled; this is eligibility hardening only.
Alternatives considered
- Reuse Clara's
isStageEligible/firstReplyFollowUpSentAtas the human-call eligibility. Rejected: those encode "when may a bot text" (online-listing sources only, stop once nudged). Applied to a human call list they over-narrow it and hide the hottest leads — exactly the ones who ignored the bot and now warrant a human. - Honor SMS opt-out as a call suppressor. Rejected: a manual human call is not a TCPA automated/robocall, and a STOP is "stop texting me," not "never call me." Suppressing on it would wrongly hide recoverable leads. We will honor a real do-not-contact signal when one exists — that is the tracked follow-up.
- Keep no-phone leads with a "No phone on file" flag. Rejected for the call list: an un-dialable lead is not a callable lead, and surfacing them is noise on a phone sheet. They stay visible on the prospect pipeline page, where finding a number is the right next action.