One team-notification engine

Right now, 72 different places in the code decide, on their own, whether to email or text a client's team. We're replacing that with one door everything goes through.

Proposed — pending review. 2026-10-03 · design + local replay · numbers independently re-graded; judge noise is ±7 rows

00 · Readiness, Oct 4

Not at the bar yet. The three hard rules held on every replayed case. The overall pass rate is 88–94% against a 98% bar. One defect is left (below). Turning it on is safe but partial: only some notice types go through the engine so far.

CheckResult
Real situations replayed (30 days, JP&Co + Western Slope)2,786
Planted controls (known-bad must fail, known-good must pass)12 / 12
No message → no team email100% (with fixes; 85–90% on main)
Never an error email to the client100%
Automated senders (AppFolio, do-not-reply) get nothing100%
Summary model fails → no email, one engineering alert32 / 32 (with fixes; main sent two alerts on 16 of 39)
Overall pass rate, graded engine notices88–94% (bar 98%)

The one defect left: the short topic the AI writes for a callback subject is checked for shape, not meaning. It can contradict the message: "unlock issue" when the caller asked for a unit to be locked, or "question about rent" when the caller only asked to talk. Fix options are a decision for Fede: use the existing subject (name + home) for callbacks, or add a second AI check.

What the switch covers today: voice messages and handoffs, missed transfers, tour-day updates. Renter questions forwarded to the team, held first emails, person-asks, after-hours notes, application failures and the action reminders still go out the old way, even with the switch on.

Willows, in production (our test property)

SituationHowResult
Voice messageReal callOne email, delivered (SendGrid); old email did not send
Missed call, no messageReal callNothing sent ✓
Renter questionHarness, prod sendAsk email, delivered
Held first emailHarness, prod sendAsk email, delivered
AI summary failsHarnessNo email, one engineering alert ✓
Old staff email vs new engine email for a voice message
Voice message, old | new. Same plain-note look. The new email is one sentence, then the callback number.
Old vs new renter question email
Renter question, old | new. Sent before the subject fix; the subject now reads "question about 6-month lease".
Old vs new held first email notice
Held first email, old | new.

Fixes from tonight: subject topic #16434 (merged), #16491, #16512; engine #16481–#16485; harness #16500.

01 · The answer in three lines

Today, 72 separate code paths each decide on their own whether to email or text a client's team — which is why a fix in one place never reaches the other 71, and why the team gets emailed about things it never should.

We're building one door: every notice is one of three outcomes (handled, ask, or handoff), written through one composer, with one record of what was sent. On the 79 real email cases that needed a notice in the last 90 days, today's scattered code passes 0%. The new engine passes 68% (and 78% of the cases that are actually its job to decide). This is graded honestly now — the same strict grader scored before and after, and an independent check confirms it.

It ships off by default. We prove it on hundreds of thousands of real past situations, then on one real client (Willows), and only then does Fede turn it on, client by client. The old code stays live until he does.

02 · Today vs. new

Email only for now. Texting and phone routing come later, on client request — see "Not now."

TODAY: 72 DOORS Clara …72 send paths, each with its own wording, its own "who gets this", its own on/off switch NEW: ONE DOOR Clara One engine one record, one place to fix Team
Today: 72 places can decide to email or text the team, with ~31 different email wordings and no shared record. New: every notice passes through one engine, which decides the outcome, writes the exact words sent, and sends it. Source: engineering audit, main at commit 10d8146e35.
03 · Architecture

One path, start to finish. Every stage is shared across clients and channels except the two marked "per channel."

Something happens call ends · email/text arrives · tour event · Clara asks Ingest one typed TeamIntent: what happened, who, their own words, callback number Decide — shared, all channels no notice · ask the team (Clara continues) · hand off (Clara stops) Resolve recipient interface; today ONE implementation = the client's team email. Per-area routing: future. Write — one writer per channel Email writer (built). Text writer (later) — same facts in, its own rules. Summary sentence: model-written, then checked against the source; falls back to a quote. Send + record one TeamNotice record per notice: status, reply matching Staff reply by email matched back to the case Clara continues with the renter + saves the answer to the knowledge base Engineering alert ONLY send failed · robot refused · can't resolve Failures never reach a client or renter — the client-facing code cannot import the alert module.
Every box up to and including “Send + record” is the same code for every client and channel. Only the two “Write” writers (email, text) differ. A failure anywhere peels off into a separate engineering-only alert path that the client-facing code physically cannot reach into.

Shared vs. per-channel

Rule
Shareddecision, recipient, facts, reply matching — one sentence of what the person wants, each fact stated once, no internal details, failures go to engineering only
EmailText
Lengthone sentence + “Callback number: …”one short line
Subjectyesno
Link to the conversationyesusually no
Reply“Reply here”plain words, no menus
Timing—quiet hours

Off by default

One setting, teamNotifyEngine: property, then company, then off. The old 72 paths stay live until Fede turns the new engine on, client by client.

Rollout: local replays (5,736 real past situations) → Willows test property, proven live → Fede turns it on per customer → the old paths and the setting get deleted.

Built so far

PieceWhat it isStatus
Scoring tool13 automated checks that grade a notice against the real case, with planted-bad-notice controlsIn review
Corethe typed intent, the three outcomes, the quote fallback, the general-team recipientMerged
Engine + record + settingdecides the outcome, writes the TeamNotice record, the off-by-default setting, matches staff email replies backIn review
Summary sentencethe model-written sentence plus its fact-by-fact check against the sourceIn review
Replay runner, part 1runs real past cases through the decision stepIn review
Replay runner, part 2grades every case and produces the before/after numbers belowIn review
Wording + stricter graderfails filler words; closed the gap between the old and new engine's scoresIn review
Email-only fallbackwhen there's no phone on file, shows Email: … instead of a callback numberIn review

Next

  • Wire missed-call/callback handoffs and email asks behind the setting.
  • Prove it live at Willows.
  • Fede's turn-on, client by client.
  • Then staff texts move into the engine too — today there are about 6 separate text senders (tour arrived, check-in, lockbox, message taken, handoff question); the Text writer replaces all of them.
04 · The three outcomes

Every situation Clara can't finish alone sorts into exactly one of these. Nothing else.

1 · HANDLED Clara finished it herself → nothing sent e.g. tour booked, question answered 2 · ASK “Jordan asked: ‘Can I bring a dog?’ Reply here and I'll get back to Jordan.” → team replies, Clara continues + saves the answer 3 · HANDOFF “Callback number: (555) 010-0142” → Clara stops, a person takes it from here
One engine, one of three outcomes every time — never a fourth kind of notice, never a guess at wording. Source: core.md, Fede's 10-03 ruling (email only, one team address per client).
05 · Before / after, in numbers

We made the grader stricter (filler words now fail), and the engine caught up.

Pass / 79
Old engine, old grader54
Old engine, stricter grader12
New engine, stricter grader51

Of the 28 misses: 14 the judge wanted more detail than the brevity rule allows, 4 small wording misses, 10 nothing was sent (being investigated).

5,621 / 5,623
stayed silent correctly, when nothing was owed
9 / 9
engineering failures kept off the client
06 · Four real examples, before and after

First names and addresses below are placeholders, not real customers. The "after" side is shown as the actual rendered email: subject in bold, then the body.

Before

Amy D. — left a message at 123 Example Street

Amy said: “Uh, something else. Application. Speak to a representative. Set up a callback.”

After

Amy D. — question about an application

Amy D. wants to talk to someone about an application at 123 Example Street.

Callback number: (555) 010-0142

Before — Natasha called about renting out her house

Property: not identified yet
Natasha called. The office was closed
(after hours). Caller named Natasha is
interested in renting out her house
and having the property management
company manage it for her. She is
calling from the number ending in
0142. Office was closed when she
called.
Called at 9:45 AM.
Can someone call Natasha back?
(555) 010-0142

Clara

After

Natasha — wants to rent out a house

Natasha wants to have a house managed as a rental.

Callback number: (555) 010-0177

Before — prospect arrives for a tour

Prospect has arrived — Example
Townhomes

Jed Hammond has arrived for his
9:00 AM tour of Unit 405.

Prospect: Jed Hammond
Phone:    (555) 010-0142
Tour: Thu, Oct 1 at 9:00–9:30 AM
Unit:     405

Prospect detail: [link]

— Clara by PropFlowAI

After

Taylor P. — here for a tour at 12 Test Street

Taylor is here for the 3:15 PM tour.

Before — renter's question Clara couldn't answer

PROPFLOW · FORWARDED BY CLARA

From: Unknown
Status: Prospect
Property: Example Apartments
Intent: Lease info
Why forwarded: Prospect asked about
co-signer/guarantor acceptance — not
covered in property data.

Summary: Prospect interested in a
studio wants to know if co-signers
or guarantors are accepted.

Message:
Tomorrow, anytime, works for me.
What would be the estimated move-in
costs with everything included on
studio?

After

Steven F. — question about 12 Test Street

Steven asked: “What would be the estimated move-in costs with everything included on the studio?”

Reply here and I'll get back to Steven.

Each notice can go by email or text. Text versions come with the text channel, next after the email side merges.

07 · What staff replies look like today

The "ask" outcome only matters if a real staff reply makes it back to the renter. Today it mostly doesn't.

5 / 6
text replies from real staff got matched and used
0 / 16
email asks got a reply from an actual staff person (the 4 that resolved were typed in by Fede)

Texting already closes the loop because it's one thread. Email doesn't yet — this is exactly what the new engine's reply-ingest step fixes before it goes live anywhere.

08 · Rollout
Local replays done Small pieces merged live in code, switched off Now building the real pieces, switched off Willows one test property, proven live Fede turns it on per customer, his call Old paths deleted
Ships off by default, behind a setting. The old 72 paths stay live and untouched until Fede turns the new engine on for a given client; they're deleted only after that.
09 · Not now (nice to have, if a client asks)
Texting the team — email only for v1. Smarter phone routing — voice stays as-is. Per-area owners — one team address per client for now, no cascade.
10 · One open question
  1. When a renter writes back into a conversation the team already took over by hand, should Clara send the team a one-line "Jordan replied" email?

    Recommendation: yes. Today that renter reply sits silently in the thread and the team can miss it; one short email ("Jordan replied — [link]") costs nothing and nobody has complained about getting too many of these. No other change to how takeover works.

PropFlow Docs