What Clara does when a leasing agent tells her to move a prospect's booked tour, cancel it, or give it to someone else: who she asks, what the tour holds while she waits, and what the host hears back.
2026-11-01 · Proposed — pending Fede's review · local POC built, nothing merged or on · related: Situs tours: current and new state
Today, when an agent moves a tour, Clara tells the prospect "your tour is confirmed for the new time" and never asks if it works. Proposal: when the agent moved it because they can't make it, Clara asks the prospect "does Tue 4 PM work? If not, reply with a time that does", keeps the tour at the new time while she waits, books any counter-offer against the host's real calendar (drive time included), and tells the agent how it landed. When the prospect asked for the move, Clara just confirms, as today. "Give Rosa's tour to Miriam" becomes a staff command too: same time, new host, prospect not bothered. Situs staff already do the ask by hand: one new time, by text, wait for the yes; prospects answer in a median 6 minutes. It is rare on Clara today: 1 staff move through Clara in 60 days (77 tours), so this is built because Situs is about to route tours through her, not because of a pile of failures.
| Staff says | Clara does | Prospect gets | Agent hears |
|---|---|---|---|
| "I can't make Rosa's 2 PM, move her to Tue 4" | Moves the tour, marks it waiting for Rosa | The ask: does Tue 4 PM work, or reply with a time that does | "Moved. I've asked Rosa to confirm." Then: confirmed / new time / declined |
| "Rosa called, she wants Tue 4 instead" | Moves it, confirmed | Today's confirmation, no ask | "Moved and Rosa has the new time." |
| "Cancel Rosa's tour" | Cancels (today) | Today's cancellation, plus "want another time?" | "Cancelled and Rosa knows." |
| "Give Rosa's tour to Miriam" / "can someone else take it" | Same time, new host: calendar, AppFolio showing and host field move | Nothing (time unchanged) | Both agents told; if nobody is free it becomes case 1 |
Moves through Clara are rare, and the one real staff move told the prospect nothing.
| Since Sep 2 | Tours booked | Tours moved | Staff moves via Clara |
|---|---|---|---|
| Western Slope | 49 | 4 (5 moves) | 1 |
| Camellia | 26 | 2 | 0 |
| Situs | 2 | 0 | 0 |
| Who moved the 7 | Moves | Prospect replied |
|---|---|---|
| Prospect asked, Clara moved it | 2 | 1 "Thank you!", 1 silent |
| Prospect running late, staff moved it on Clara's line | 1 | told nothing (no text permission, email-only lead); attended |
| Our team fixing a Clara mistake by hand | 3 | 1 clarified, 2 silent |
| Second visit saved as a "move" | 1 | silent |
| Clara's running-late path | 0 | it tried twice for the late prospect and failed |
| Changed in AppFolio and synced in | 0 | impossible today: the showing sync never reads changes back |
Lead time: 3 of the 7 moves happened under 2 hours before the tour (one 1 minute before), so same-day handling is the common case, not the edge. The best hand-written move we sent (Oct 2) already had the shape this design proposes: "it's at 3:45 PM, not 4:00… If 3:45 doesn't work, just reply here."
Seen along the way, not fixed (suspicion, needs its own reproduction): the late prospect's own "running behind, 9:15" failed in Clara's reschedule with "no prospect found for this phone number" (email-only Zillow relay lead), and staff were told a different reason. History before Oct 5 doesn't record who moved a tour; attendance exists from Oct 3. Each case was read in the raw conversation.
Situs already does propose-and-confirm, by hand: one time, by text, and they wait for the yes. Prospects answer in minutes. The gap is the other half: about half of staff moves are never asked; staff change the time in AppFolio and its automatic "your showing has been updated" text is the only notice.
| Staff-initiated moves (Jan–Oct 2026) | Count |
|---|---|
| Asks found (texts, mail, a call) | 9 |
| offered exactly one new time | 7 of 9 |
| offered two or more times | 0 of 9 |
| waited for the yes before changing it (where order is visible) | 3 of 4 |
| Moves never asked, prospect told only by AppFolio's text | 4 of 8 + 1 by email |
| Median prospect reply to a staff-proposed time | 6 min (11 pairs) |
| Median staff reply to a prospect's own move request | ~54 min (8 pairs) |
Also seen: when nobody confirms, Situs staff cancel and don't drive out ("makes sense to not go if they don't confirm", Jul 28 email). After a no, they counter with one new time, a virtual tour or a lockbox. Sources: 884 AppFolio guest cards in our robot dump (54 showing time changes, hand-classified), the leasing mailbox May–Oct (read-only), 44 recorded calls. Emails on guest cards show no body and most calls are unrecorded, so 19 time changes have no visible message.
| Case | Staff's words that decide it | Default when unclear |
|---|---|---|
| A. Staff can't make it | "I can't make it", "something came up", "I need to move", "I'm stuck", "can we push her" | Treated as A: Clara asks the prospect. Asking someone who requested the time costs her one "yes"; presuming costs a no-show. Staff are never asked "did she ask for this?" |
| B. Prospect asked (relayed) | "she called/texted/emailed and wants…", "per her request", "she asked to move" | |
| C. Cancel | "cancel", "she's not coming", "she rented elsewhere"; "don't tell her" keeps today's silent cancel | Never inferred from "move" |
| D. Reassign | "give it to Miriam", "Miriam will take it", "can someone else take it", "I can't make it, can anyone cover" | "I can't make it" with no new time and no name: try D first (same time, someone free), fall back to A |
Clara asks staff only what she cannot get anywhere else: which tour (two match), or the new time when staff gave none and nobody else is free. That is today's resolver behaviour; nothing new.
| Option | Message | For | Against |
|---|---|---|---|
| One-way notice (today) | "Your tour is confirmed for Tue 4 PM." | Zero friction | Presumes. A prospect who can't make 4 PM either no-shows or drops silently; staff never learn why. |
| Propose and confirm (recommended) | "The team needs to move your tour from Mon 2 PM to Tue 4 PM. Does that work? If not, reply with a time that does." | One question, one-word answer; staff's chosen time is usually fine; counters use the same booking rules as any request | Some never answer (handled by the timers below) |
| Offer 2–3 alternatives up front | "Can we move you? Tue 4, Wed 10 or Wed 5?" | More choice | Longer message, more back-and-forth, and staff already chose a time; slots offered must be held or they go stale |
After a no, Clara offers 2 open times from the host's calendar using the trip-aware pick (drive time between homes applies, same engine as every booking). A counter-offer ("Thursday morning?") goes through the ordinary prospect reschedule path against the same rules.
The tour lives at the new time only. The old time is dead (the agent can't make it), so holding both just blocks a slot nobody will use. A moveAcceptance record on the tour says "waiting for the prospect".
| Thing | While waiting |
|---|---|
| Calendar event | At the new time on the host's calendar; title unchanged, one line in the body: "Waiting for Rosa to confirm this time." |
| PMS showing | Moved to the new time through the adapter, as today. Not moved again until the prospect lands somewhere else. |
| Reminders | Re-armed for the new time (today's behaviour). The day-before reminder carries the question if still unanswered; the 1-hour reminder is the normal one. |
| Virtual tour | A moved virtual tour gets a new video room today (the old one can't be moved); the new link rides in the ask. When the move resolves to another slot or to a cancel, the abandoned room is closed. |
| Household | Ask goes to everyone today's confirmation reaches. First answer from any of them settles it. |
| Answer on another channel | Settles it: the record is on the tour, not the thread. |
| Situs (Supervised) | The ask is staff-started, so it goes out as today's confirmation does (no review). Clara's follow-ups to a no go through the review queue like any reply. |
Through the coworker template: email at Situs, text at Western Slope (each agent's own setting). One line each, only when something changed.
| Moment | Host hears |
|---|---|
| Ask sent | "Moved Rosa to Tue 4 PM and asked her to confirm. I'll tell you when she answers." |
| Yes | "Rosa confirmed Tue 4 PM." |
| Counter booked | "Rosa couldn't do Tue 4. She's now Wed 10 AM, on your calendar." |
| Declined, nothing worked | "Rosa couldn't make any time I offered, so I cancelled her tour. I'll keep following up for a new day." |
| No answer by 2 h before | "Rosa hasn't confirmed the 4 PM move. The tour is still on; she may not come." |
| Reassigned | Old host: "Rosa's Tue 4 PM is now Miriam's; off your calendar." New host: "You have Rosa, Tue 4 PM at Ember Estates, taken over from Lee." |
Precise enough to code against with a local harness. Names are proposals; the shapes and rules are the contract.
| Field | Shape | Rule |
|---|---|---|
hostPersonId | string | undefined | NEW. The person hosting. Written at booking from the pool pick, on every host change, and from the PMS "assigned to" at the sync boundary. tourHostPersonId() reads it first, then falls back to calendarNode (legacy rows). |
moveAcceptance | object, below | NEW. Present only after a staff move that needs the prospect's yes. One per tour; a newer staff move overwrites it (old one recorded as superseded in history). |
prospectConfirmed | existing {at, answered, forDate, forStartTime} | REUSED. A yes to the ask stamps it for the new time; "confirmed for this slot" = forDate/forStartTime equal the tour's. |
history[] | existing TourEvent | Staff move keeps writing rescheduled with actor:'staff' + movedFrom, plus NEW moveCase ('staff_cannot'|'prospect_asked'|'reassign_fallback'). Host change keeps host_changed, plus NEW fromHostPersonId, toHostPersonId, source ('staff_text'|'staff_email'|'manager_page'|'pms_sync'). |
moveAcceptance: {
id: string // move id, also the dedup key for messages
state: 'awaiting_prospect' | 'countering' | 'accepted' | 'declined' | 'unanswered' | 'superseded'
case: 'staff_cannot' | 'reassign_fallback' // 'prospect_asked' never creates one
from: { date, startTime, hostPersonId? }
to: { date, startTime, hostPersonId? }
staffPersonId: string; staffWords: string // verbatim, for the audit
askedAt: ISO; askedVia: ('sms'|'email')[]
nudgedAt?: ISO // at most one nudge
hostToldUnansweredAt?: ISO // the T-2h line, once
offered?: { date, startTime, hostPersonId }[] // max 2, only after a no
resolvedAt?: ISO; resolvedByPersonId?: string; resolvedChannel?: 'sms'|'email'|'voice'
}
Where the case lands (new inputs). reschedule_showing_for and the desk's reschedule_tour each gain move_reason: 'staff_cannot' | 'prospect_asked' | 'unclear' with staff_words_for_reason (a verbatim quote, same shape as caller_words_for_time), and notify_prospect (today only cancel_tour has it). A new desk tool change_tour_host takes tour_id, new_host_name (optional; absent = "someone else") and staff_words. moveAcceptance is written inside handleRescheduleTourImpl when opts.staffBooking is set and the reason is not prospect_asked, in the same row write as the reschedule; notifyProspectOfStaffMoveOrCancel then sends M2 instead of the confirmation.
| Staff words | New time given? | Action | Prospect message |
|---|---|---|---|
| Relayed request (B) | yes | reschedule, confirmed, no moveAcceptance | M1 confirm (today's) |
| Can't make it (A) or unclear | yes | reschedule + moveAcceptance awaiting | M2 ask |
| Can't make it, no time, no name | no | try reassign (D) at same time; if no giver free → nearest slot any giver is free → as A with case:'reassign_fallback' | none (D) / M2 (fallback) |
| Give to named agent / "someone else" (D) | n/a | host change; refusal host_busy/not_eligible → readback names why + nearest free giver; never moves the time without staff's "can't make it" | none |
| Cancel (C) | n/a | cancel (today) | M5 cancel + rebook offer |
| any + "don't tell her" | as above, no prospect message, no moveAcceptance | none | |
| M2 cannot go out (no permission to text, no email, company's tour-confirmation path Off/Preview, send failed) | move stands, NO moveAcceptance created; staff hear "not told" (today's prospect_notified:false); never an ask that wasn't sent | none |
awaiting_prospect or countering| Reply means | State → | Tour | Prospect | Host |
|---|---|---|---|---|
| Yes / thanks / "see you then" | accepted | stamp prospectConfirmed for new slot | Clara's normal short ack | H2 |
| Names another time | countering → accepted | ordinary prospect reschedule against host calendar + drive rule; if not open, offer the 2 nearest open (offered) | Clara's reply (booked / 2 options) | H3 when it lands |
| No, without a time | countering | stays at new time | 2 open times, trip-aware, host first then any sharing giver | none yet |
| No to the offered times too, or "never mind" | declined | cancelled | Clara's reply; normal follow-up cadence resumes | H4 |
| Anything else (question) | unchanged | unchanged | answered normally | none |
Any party on the tour (household) and any channel (text, email, call) can settle it. Classification runs in the existing tour reply decider; it gains one input: "this tour has a pending move to X".
| When | Condition | Effect |
|---|---|---|
| Ask + 4 h (daytime) | still awaiting, new slot more than 24 h out | one nudge M3, stamp nudgedAt |
| Day-before reminder | still awaiting | reminder uses M4 wording (asks again); no separate nudge that day |
| New slot − 2 h | still awaiting | state unanswered, host gets H5 once; tour stays on |
| Move made inside 2 h of the old or new slot | ask goes by text if allowed (else email), no nudge, host gets H5 immediately if still awaiting at slot − 30 min | |
| Second staff move | any open state | old record superseded; one new M2 ("one more change"); timers restart |
M-messages go to the prospect (fixed family, translated, same sender and channel rule as today's confirmation; email variants answer by reply). H-messages go to the host through ONE sender: the host notice path (host-confirm-notice.ts, plus the Nov 1 host-email route that sends tour-day updates to the host, #24230): email by default, text when the agent opted in.
eligibleTourPersonCalendars + free for the window; "someone else" → same filter over all givers, nearest-first (trip-aware pick). Today's free check (readHostFreeForTourWindow) reads the busy set only, with no drive pads: the POC adds padsAround from the drive engine.applyChangeHost (claim → create new invite → write → delete old). Stamp hostPersonId in the same write.source:'pms_sync', no prospect message, both hosts get H6. The POC stubs this input.| # | Case | Expect |
|---|---|---|
| 1 | "I can't make Rosa's 2, move her to Tue 4" | M2 sent, awaiting, H1 |
| 2 | "Rosa called, wants Tue 4" | M1 only, no record |
| 3 | "move Rosa to 4" (unclear) | as 1 |
| 4 | prospect "yes" by email after a text ask | accepted, H2 |
| 5 | prospect "can't, Thursday 10?" host free | moved to Thu 10, accepted, H3 |
| 6 | prospect "no" then "none of those" | cancelled, H4 |
| 7 | silence | one nudge, M4, H5 at −2 h, tour kept |
| 8 | second staff move before reply | superseded, one new M2 |
| 9 | "give Rosa's tour to Miriam", Miriam free | host changed, no prospect message, H6 ×2 |
| 10 | "can someone else take it", nobody free | fallback move, M2, H7 |
| 11 | PMS "assigned to" changed | host changed silently, H6 |
| 12 | "move her, don't tell her" | moved, no message, no record |
| 13 | planted defect: ask sent with the OLD time | harness goes red |
| PR | What | Kind |
|---|---|---|
| 1. State | hostPersonId, moveAcceptance, history fields, readers; no behaviour change (byte-identical replay) | storage |
| 2. Ask and answers | case split in the staff tools, M/H messages, decider input, timers in the tour workflow | improvement to live behaviour: no switch |
| 3. Reassign | staff tool on text/email/voice, "someone else" pick, adapter write, inbound "assigned to" | new staff command; no prospect-facing effect, no switch |
Ladder (testing playbook): read-only replay of every real staff move in the last 60 days (what would Clara have sent, byte-identical otherwise) → Willows live run of harness cases 1, 4, 5, 7, 9 with a real text, email and calendar → live everywhere on merge. Proof is the real thread ids, never unit tests.
Wording check: Opus 5.5 grades every rendered message. Staff-text reading: the same model and tool definitions the staff desk uses in production (Sonnet 5.5). Both ran on the subscription.
| Case | Today | With the prototype |
|---|---|---|
| Agent can't make it and moves the tour | Prospect told "confirmed", never asked | Asked "does Tue 4 PM work?"; the host hears Clara is waiting on her |
| Prospect asked for the move (through staff) | Plain confirmation | Same already right |
| Agent moves it with no reason | Told "confirmed" | Asked (unclear means ask) |
| Asked by text, she says yes by email | Never asked | Settled; host told |
| She texts "yes" | Never asked | Settled; host told |
| She offers another time and the host is free | Never asked | Tour moves to her time; host told |
| She says no, then "none of those" | Never asked | Clara offers two times, then cancels; host told |
| She never answers | One plain reminder | One nudge 4 h later, the day-before reminder asks again, host warned 2 h before; tour kept |
| Agent moves it a second time | Two "confirmed" texts | First ask replaced; one new ask that says "one more change" |
| "Give Rosa's 2 PM to Noor" | No such command | Host changes, time stays, prospect not bothered, both agents told, the PMS showing follows |
| "Can someone else take it?" and nobody is free at 2 | No such command | Nearest free agent and time (3:00, Noor) becomes a move she is asked about; both agents told |
| Showing reassigned directly in the PMS | Clara never notices | Host follows the PMS, both agents told, nothing echoed back |
| "Move her, don't tell her" | She gets "confirmed" | Moved, nothing sent, nothing waiting |
| Household: the partner answers | Both get "confirmed" | Both asked; the partner's yes settles it |
| Virtual tour | Confirmed | The ask carries the video room for the new time |
| Same-day move (the real Western Slope shape) | Confirmed | Asked; no nudge (under 24 h out); host warned 2 h before |
| Move inside 2 hours | Confirmed | Asked; host warned 30 min before |
| Supervised company | Confirmed | Ask goes (a person started it); host notice and nudge wait for review |
| Spanish speaker | Confirmed in Spanish | Asked in Spanish |
| Email-only prospect | Confirmed by email | Asked by email; an email yes settles it |
| "Give it to Leah" and Leah is busy | No such command | Refused, names the nearest free teammate; nothing changes |
| The ask cannot go out (no text permission, no email) | Nothing sent | Same; nothing waits on her; staff hear "not told" already right |
| Planted bug: ask quotes the OLD time | Told "confirmed" | Asks with the new time; planting the old time turns the test red |
| Rung (playbook) | Status |
|---|---|
| Made-up cases, red today and green on the branch, planted bugs caught | done |
| Replay of the real 60-day staff moves, byte-identical otherwise | not run the cases are shaped from real tour rows, but no real thread was replayed |
| Willows live run with a real text, email and calendar | owed per PR blocked tonight by the no-send rule |
| Point | Prototype | Proposal |
|---|---|---|
| Note to the old host when "someone else" lands on a new time | Reworded: the contract line failed the wording check | Use the new line |
| New host in that same case | Also hears "it's yours now" | Add it to the contract |
| Names of the hand-off tool and its fields | Its own names | PRs adopt the contract's names |
| Nudge to an email-only prospect | None (the nudge is text only) | Decide: email nudge or none |
| "She asked to move, the agent picked the time" | Not covered by the contract; the one missed staff text is this shape | Ask her |
| Host notices | Go through the existing team-notice sender | One sender, as the contract says |
Not built yet: decision 3 (a bare "I can't make it" tries another agent first), drive pads in the free check, closing the old video room, the real PMS "assigned to" read (stubbed). Open bug in the prototype: when "someone else" lands on a new time but the ask could not go out, the old host is still told "asked her to confirm".
| PR | What | Lines | Kind |
|---|---|---|---|
| 1. State | tour fields and readers | ~340 | no behaviour change |
| 2. Ask and answers | the ask, replies on any channel, host notices, "ask could not go out leaves no record" | ~700 | improvement to live behaviour, no switch |
| 3. Timers | nudge, day-before re-ask, host warning | ~150 | improvement, no switch |
| 4. Reassign | staff command, PMS showing follows, PMS "assigned to" flows back | ~480 | new staff command, no switch |
Four PRs, not section 8's three: the timers come out of PR 2 to keep it near the size limit. The test harness rides with PR 2. Build the PRs from the branch's final files, never by copying its commits one by one (three early commits carry real staff first names). This page's design text also uses real staff first names in examples (Miriam, Kat, Lee); the prototype uses made-up ones.
Evidence: branch fede/poc-staff-tour-move, folder scripts/coworker-harness/staff-tour-move/results/ (before and after files with the attestation, the planted-bug run, both model runs). Run it: npx vitest run --config scripts/coworker-harness/staff-tour-move/vitest.config.ts.
staff-booking-confirm-prospect.ts; staff tools staff-leasing-desk.ts, staff-desk-writes.ts; reschedule handler and notifyProspectOfStaffMoveOrCancel in tools-leasing.ts; host change tour/change-host.ts (manager page only today); host read tour-host-person.ts; reconfirm stamp staff-tour-reconfirm-notice.ts; reminders temporal/activities/tour.ts.history[].action/actor/movedFrom, bookedByStaffPersonId, attendance) at the three companies, each move read in its raw prospect and staff threads. The "staff" stamp marks the code path, not who typed: three hand fixes by our team carry Kat's id.