How calls that a customer's own staff take on their office phone system reach the right person's timeline in PropFlow. Design for Fede's decision.
2026-10-28 · Situs Group first (Crexendo VIP) · Live at Situs since 2026-11-01 (capture only; no actions)
FlowProven Oct 28PiecesDecisionPrivacy gateTwo office daysPractice daysQuality checkOption A runLive Nov 1Call notesTimeline QA Nov 4To verifyProof ladder
Decided (Fede, 2026-10-28): capture only, no actions. Each staff call lands on the person's timeline with transcript and audio. No work-order sweep, no lead capture, no grading: calls are captured on the timeline with no actions, and each call sits inside the person's existing conversation and Clara logs one entry on the lead's guest card unless staff already did (call notes). Built as a structured-event ingester behind one company switch, Office phone recordings. Live at Situs since 2026-11-01 (capture + call notes; no other actions); see Nov 1.
Blue boxes are new code. Everything else reuses what Clara's own calls already use. Nothing in core code names Crexendo: the vendor lives behind a small PbxRecordingSource interface, the same rule as the PMS adapters.
| What arrived | Value from the test call |
|---|---|
| Path | SitusGroup/2026/10/29/aud-<UTC start>-<call id>-8656.wav |
| Audio | mono, 8 kHz GSM WAV, 17 s, 28 KB |
| Tag inside the file | aor0=sip:<staff ext>wp@SitusGroup; aor1=sip:+1<caller>@…; domain0=SitusGroup (staff leg, outside leg, customer domain) |
| Transcript | "Hello? Just just testing the recording. 1 2 3. Test, test, test." Two speakers found. |
| Not included | No transcript, no dialed number, no missed/answered flag. Voicemails are not call recordings and never reach the bucket. |
| Piece | What it does | Reuses |
|---|---|---|
| Trigger | Bucket "new file" event to a Lambda, which drops a pbx_recording message on the inbound queue. | Standard Lambda layout, inbound-processor queue, same as transfer recordings. |
| Bucket → company mapping | A per-company attachment row saying which bucket and prefix is theirs. The customer name is data, never a constant. The domain inside the WAV is a cross-check only. | The mailbox and phone-line attachment rows. |
| Parser + vendor adapter | Reads the path and the tag. Rule: the leg that is a real phone number and not one of the customer's own numbers is the outside person; the other leg is the staff extension. | New, in the integrations layer, Crexendo/NetSapiens as the first implementation. |
| Transcription | Mono audio, so speaker split comes from diarization, not channel split. | Existing Scribe wrapper, with a diarize-on variant. |
| Timeline row | One voice conversation per call, id conv-pbx-<call id> so duplicate events are harmless. Audio converted once to MP3 (ffmpeg inside the Lambda, fractions of a cent) and stored next to Clara's recordings so the existing Play button works. Same conversation page, no new screens. | The staff-answered-call record, the audio route's staff leg, per-speaker transcript bubbles, person matching by phone. |
| Who took the call | The header reads like a Clara call: "Kat answered" or "Kat called Raimundo" (Fede, 2026-10-28: never a wording like "office phone call answered by extension N"). The staff extension is a new fact on the staff person's record; unmapped extension falls back to the company name, never a number. | Staff person records. |
| Switch | voice.pbxRecordingIngest, company level, default off, a row on the customer admin page. Gates PropFlow's processing only; Crexendo's upload is already on at Situs. | Same pattern as the staff welcome text switch (Oct 27). |
| Option | What happens after each call | Verdict |
|---|---|---|
| A. Capture and act | Transcript on the person's timeline plus work-order sweep, lead capture and a company-brain note per call. | Not now; revisit after the supervised week |
| B. Capture only | Transcript and audio on the timeline, nothing else. | Decided, Fede 2026-10-28 |
| C. Full Clara post-call | Everything in A plus call-outcome grading and the team alert path. | No: every human call grades as "unhandled" and pages the team about a call they took |
Situs Group's phone-system export for Oct 27–28 was matched, number by number, against PropFlow's records for Situs. A number counts as "known" only when a prospect, guest card, tenant, lease or vendor record holds that exact phone number. Carrier caller-ID names are never used as identity. Nothing was written anywhere.
| Staff dialed out | Inbound, office hours | Inbound, after hours (to Clara) | |
|---|---|---|---|
| Calls | 29 | 59 | 8 |
| Distinct outside numbers | 20 | 38 | 3 |
| Known to PropFlow | 16 (6 tenants, 3 incoming tenants, 5 prospects, 2 vendors) | 16 (7 tenants, 3 incoming tenants, 6 prospects) | 1 real tenant; the other 7 calls were our own test phones |
| Unknown | 4 | 22 | 0 |
What this says for the privacy rule. Outbound calls are almost all to people we know (16 of 20 numbers), so "keep only known counterparties" would keep most of what staff do on purpose. Inbound is the opposite: more than half of office-hours callers are unknown to PropFlow (22 of 38), and carrier names suggest businesses, vendors and cold calls. Vendor matching is weak today because only 71 of Situs's 1,282 vendor records carry a phone number, so a "vendor with a work order" rule would miss most vendor calls until vendor phones are filled in.
| Staff | Outbound calls | Minutes | Over 10 seconds |
|---|---|---|---|
| One staff member (ext 2007) | 16 | 46 | 10 |
| Assistant VA1 (2014) | 7 | 7 | 5 |
| Three others | 6 | 5 | 3 |
38% of outbound dials lasted 10 seconds or less (no answer or voicemail). One tenant was dialed 5 times in 10 minutes on Oct 28. The longest outbound time went to a waste-hauling vendor (26 minutes over 2 calls). 24 people appear both in the phone export and in a PropFlow conversation, but in all but one of those threads the PropFlow side is a mirror of staff's own AppFolio texts and emails, not Clara. Two numbers that reached Clara after hours were PropFlow team test phones; one of them left three real-looking work orders at a Situs property that should be cleaned up.
Full tables (per number, per staff member, vendors, tenants with recent maintenance) stay in the session scratch folder; they carry customer names and never enter a repo or this page.
Decided, Fede 2026-10-29 "Build the whole pipeline, transcribing to matching to the conversation, but don't write anything to PropFlow yet. A practice day or two where we log the transcript and what we would have done with it."
A local runner checks the Situs bucket every 30 minutes during office hours. For each recording it transcribes the audio ourselves (the phone vendor sends audio only; no transcript feed exists on Crexendo's side), matches the outside number to the person and the thread Clara would attach the call to, and logs the decision. Every call is transcribed during practice, even the ones the privacy gate would delete, so we can count what the gate would wrongly drop. The runner reads PropFlow, never writes it; the bucket is never touched.
| Day | Calls | Keep | Delete | Gate would have dropped a real customer | Notes |
|---|---|---|---|---|---|
| Wed Oct 29 (full office day) · call-by-call report | 13 distinct calls (17 recordings: 4 transferred calls produced two files each) + 1 test | 5 calls (6 recordings) | 8 calls (11 recordings) | 1 | Injectable today under the current rule: a VA voicemail to a new prospect; an applicant chasing his application (VA leg plus leasing-agent leg, one timeline entry); a leasing agent calling an applicant who had gone quiet; a resident asking about pest-control spraying; a caller asking prices at a property who already has a guest card. Dropped right: a bank card-activation menu; a bank survey caller; an apartments.com sales call (two legs); the public defender asking for camera footage (two legs); a 13-minute staff call with the phone vendor about SMS registration. Dropped by design: a third party calling about a resident (two legs, 5 min). Separate category: an employment-verification call to an applicant's employer is about a known applicant but the outside party is the employer, so a phone match can never catch it. Pipeline findings: one "kept" call was 4 seconds of a carrier rate-limit message (PR 3 needs a duration or two-speaker floor); two 94-byte empty files with no tag re-error every run (skip under a size floor); direction inference matched the transcript on all 13 calls. |
The outside number was inside the WAV file on every call, so no CDR join is needed. Real calls use file-name shapes the test file did not (a letter-and-number suffix, some with no timestamp); the parser was fixed on the local branch and the fix goes into PR 3. Call direction is inferred from which party sits first in the tag and is marked as such until the practice days settle it. The test call matched a test prospect record at Zephyr that carries a PropFlow team number; that record is test data, not a lead. Full rows with transcripts stay on the Mac, never in a repo.
What comes after the practice days: if the gate holds (no real customer dropped, no personal call kept), PR 3 wires the same pipeline to the person's timeline (transcript, audio, "who called" label), proven on Willows, then Situs supervised week with the switch on, Fede's call.
The practice runner stopped after Wednesday (its working folder was deleted, so Thursday never ran). On Nov 1 it was re-run over Thursday read-only, and an independent grader read every transcript of both days against the narrow "known" rule. Transferred calls are counted once.
| Day | Calls (files) | Keep right | Keep wrong | Delete right | Delete wrong (a customer dropped) |
|---|---|---|---|---|---|
| Wed Oct 29 | 15 (20) | 8 | 0 | 7 | 0 |
| Thu Oct 30 | 17 (22) | 10 | 0 | 4 | 3 |
| Both days | 32 (42) + 1 team test call | 18 | 0 (the test call is kept and must be excluded) | 11 | 3 |
The three misses. A Zillow prospect called twice about a 1-bed at a Lakewood building and booked a showing; his guest card was taken under his wife's name and phone, so his own number is unknown to PropFlow (2 calls). A caller locked out of an office suite, transferred to maintenance, looks like a commercial tenant whose phone is not on record (1 call, unconfirmed). None of these is fixable by the gate: the number was never on the record. Correctly dropped: a friend handling a tenant's account, a former tenant on a payment plan, a wage-garnishment law firm, two cold callers, a vendor call, an employer verification, a staff-to-staff call.
What the grader found wrong in the pipeline itself (all folded into PR 3):
Dry run of the attach (what the 18 kept calls would do): 17 land on a thread that already exists in PropFlow, 1 starts a new one; 10 prospects, 4 applicants, 4 tenants. Kept calls carry content staff said on the line: a co-applicant's name and number read aloud, a medical reason for breaking a lease, office side-talk about other people during a tenant's call. That is what capture-only means; it is visible to the customer's own staff only.
Grading table with masked numbers stays on the Mac. Next: PR 3 (trigger, transcription, timeline row, backfill entry point) merges dark behind the existing switch; the 18 kept calls attach through the same code after a dry run; then the Situs switch goes on. Before any real call shows at Situs: the heads-up to Situs and the Colorado consent check (Fede's two items), and the three test work orders at Dexter.
We replayed the 18 kept Situs staff calls through Clara's existing after-call engines, on this Mac, read-only (0 writes, nothing sent). Four engines ran for real: work orders, lead capture, the after-transfer summary that books tours, and the company-brain summary. Tenant facts and policy notes have no engine yet, so one model pass stood in for them.
| What | Result | Verdict |
|---|---|---|
| Work orders | 0 of 18. The one maintenance call was a pest-spray question. A planted leak call did file one. | Right |
| Leads | 0 new. Every caller was already on file; 9 cards would get notes, bedrooms or move-in filled | Mostly right; 2 wrong fills |
| Showings | 2 booked on the call, neither on PropFlow's calendar. A tour that was already booked was not booked again | Real gain |
| Brain notes | 38 candidates: pet deposit $300 + $35/month, screening 48-72 hours, a 3-bed price, a 6-week special. About a third are garbled or go stale fast | Useful, needs a filter |
| Follow-up tasks | 37, nearly all things staff already promised on the call. 3 voicemails staff left were read as "call them back" | Would double up |
| Sensitive content | 7 calls. A medical reason to break a lease went into the card notes; a co-applicant's name and phone number went into the task list | Blocker |
What a real build needs: treat staff as the person on the call, not as Clara; record what staff already did instead of acting on it; strip medical, third-party and fair-housing details before anything is saved; ignore years-old "open" work orders (tenants carry 7 to 39). Estimate: 2 to 3 weeks to a proven, default-off build, most of it reusing the after-transfer pipeline.
Backfill of everything in the bucket (47 files)
| Outcome | Files |
|---|---|
| Kept → 18 conversations (transfers joined into one entry) | 24 |
| Deleted by the privacy gate | 18 |
| Empty files, skipped | 2 |
| Team test call, excluded | 1 |
| Willows proof files, already recorded | 2 |
Dry run matched the Oct 29–30 quality check file for file: 24 keeps right, 15 deletes right, 3 deletes the check already called misses the phone-match rule cannot catch.
Live path, on a test file:
Production check, all 5 pass: an ordinary inbound call page; a transferred call shows as one entry; an outbound voicemail reads "Situs Group left a voicemail for …"; the conversations list (voice filter, page 2, a row opens); the admin row reads "Office phone recordings: ON".

What is set
| Piece | State |
|---|---|
| Company switch at Situs | On |
| Mapping | Bucket → Situs folder |
| Forwarder and its rule | us-west-2, Terraform infra/pbx-recordings-trigger |
| Bucket notifications | On |
| Processor settings | Set by the deploy lane |
Rollback: bucket notification off (one command, in that Terraform folder's README), or the admin row off.
Proof ladder climbed (testing playbook): unit-covered → read-only dry run over the real bucket against production records → Willows live run (sandbox company) → real backfill → live-path proof → production screenshots. Supervised week now running at Situs.
Follow-ups filed: transfer heading missing the second staff name; a staff greeting with a dropped name; a staff voicemail labeled automated; a raw placeholder on a prospect page.
PRs (merged): #23406 · #23403 · #24204 · #24205 · #24236 · #24256 · #24279
One switch: Office phone recordings. When recordings are on, the call goes into the conversation and the note goes on the card (Fede, Nov 1: "if we have recordings enabled this should default to write notes, and add to conversations"). Tenants and vendors: never. Showings: shadow only.
| Before (Oct 29–31) | After (Nov 1) | |
|---|---|---|
| Where a staff call shows | Its own page, named after the phone system's call id | Inside the person's existing conversation, in order with their texts and emails |
| Lead calls on the guest card | 3 of 14 (staff, by hand) | Every kept lead call; staff's own entries are respected, never duplicated |
| Time on the entry | Hand-typed | The call's real time |
| Switches | Two (recordings, call notes) | One (recordings) |
Proven at Willows, Nov 1 (twice)


17a181aa-… (the persona's own thread), card entry sent 2026-11-01T19:16:22Z, AppFolio test property 45, card 3622026-11-01T14:31:09Z, same cardSitus backfill, Nov 1
Open
Fede opened a Situs prospect's page and saw call cards and transcript lines in a strange order. Every Situs office recording uploaded Nov 2–4 (55 files) was read back and compared with what the thread rows say. The page itself orders rows by their stored time; the stored times and the grouping are what are wrong.
| Defect | What a reader sees | Cause | Proof case | Fix shape |
|---|---|---|---|---|
| 1. Outbound calls land ~1.5 min late | A staff call to a prospect appears after texts and emails that really came after it. | Inbound recordings carry the call start in their file name; outbound ones do not, so the ingest uses upload time − length. The phone system uploads 80–110 s after a call ends (3–6 min on a bad day). | Situs → Alyssa (Nov 4): stored 11:20:32 MT, really ~11:19. She says "I'm calling back, I had a missed call" in a call the thread shows as starting before the voicemail was left. | Design pick: subtract the measured lag (simple, wrong by minutes on outliers) or take the start time from the phone system's call log. Recommend the call log; the file name stamp proves the system has it. Existing rows stay misplaced until re-stamped. |
| 2. Two different calls merged into one card | One "Situs Group called Alyssa · 2:08" card whose transcript interleaves a voicemail greeting, a receptionist's hello and a staff member leaving a message; the joined audio plays both calls back to back; the inbound call is headed "Outgoing". | The transfer rule joins any second recording with the same outside number on another extension within 3 minutes. It never checks direction or overlap. | Alyssa, Nov 4 11:20: staff on ext 2014 calling her (voicemail) while she called the main line (ext 2025) at the same moment. | Fix, no switch: a leg joins only when its direction matches the host's. All 5 real transfers in the window are inbound → inbound with a 15–23 s gap; the one wrong merge is outbound + inbound overlapping 43 s. |
| 3. Speaker roles swapped on some legs | The tenant's words shown as staff and vice versa. | Per-leg guess of which diarised speaker is staff. | Shylynn, Nov 2 first leg ("I'm one of your residents" labelled staff; "let me connect you" labelled tenant); Norma, Nov 4 outbound ("sorry, I hung up by accident" labelled tenant). Second legs are right. | Fix with these two as replay cases. |
| 4. 17 recordings never reached a thread | Threads missing the front-desk leg (Ruqiya's Nov 3 call starts mid-sentence) or whole calls (Lida, Kira, Patricia on Nov 3). | All 17 fall in Nov 3 15:58–18:01 UTC, inside the Scribe key outage (152 "invalid API key" failures Nov 3 15:59 → Nov 4 17:53). The failed messages left no ingest log line and were never re-driven. | Files still sit in the Situs upload bucket. | Re-run those 17 through the production queue now that the key is fixed; add a retry path so a transcription failure is logged by file and retried, never dropped silently. |
| Defect | Before | After | Proof |
|---|---|---|---|
| 1. Outbound call times | Outbound calls stored at upload time minus length, about 1.5 min late. | Unchanged by Fede's ruling. The start time of an outbound call does not reach us: not in the file name, WAV header, S3 metadata or upload event, and the phone system has no feed we accept (no estimate, no scheduled export, no API). Inbound queue calls keep the exact stamp from the file name. | Primary-source check of all 55 files and the portal settings, Nov 4. |
| 2. Two calls merged into one card | Alyssa's Nov 4 voicemail and her call to the main line shown as one 2:08 card headed Outgoing. | A second recording joins a call only when its direction matches. Alyssa now has the voicemail alone plus a separate incoming call; Ruqiya's Nov 3 call has both legs on one card. | Read-only replay of every Situs recording Nov 2–4: 5 real transfers still joined, the one wrong merge split. 33 affected calls re-ingested. |
| 3. Speaker roles swapped | Per-leg order guess: "I'm one of your residents" labelled staff. | A model reads each leg and decides which side is staff, on the same credential lane as the rest of the call pipeline. The old order rule is only a logged fallback. | Re-ingested all 33 Situs calls Nov 5 after the lane fix: judge ran on every call, 0 fallbacks, 0 rate-limit errors. Shylynn Nov 2 ("I'm one of your residents" now resident, "let me connect the call" now staff) and Norma Nov 4 ("I hung up by accident" now staff) read correctly; 9 of 9 checked lines on the right side (one further Norma line could not be re-matched because the re-transcription changed its wording; it sits on the staff side as it should). Entry ids unchanged, no new AppFolio notes. |
| 4. 17 recordings dropped silently | Transcription failures during the Nov 3 key outage left no log line and no retry. | All 17 files re-driven through the production queue and on their threads. Every per-file failure is now logged and sent back to the queue for retry, with a dead-letter queue after that. | 17 of 17 ingested Nov 4; dead-letter queue holds only already-recorded duplicates. |
Page check, Nov 5 00:20 MT, production, Alyssa's conversation at Situs: the voicemail (1:03) and her call back (1:25) are two cards with the right headings. Still visible: the two calls overlapped in the same minute, so their transcript lines interleave by time under one "Outgoing" label; the timeline groups voice lines by time, not by call. Separate UI change, not part of this fix.
Open: Darwin's Nov 3 entry (a 3-line voicemail) now reads "called" instead of "left a voicemail" because the re-transcription no longer matches the voicemail phrase list, a string rule that should be a model judgement. Patricia Reed's thread, empty on Nov 4, now carries three Situs voicemails to her (two on Nov 3, one later), so the re-drive covered her as well. The AppFolio note already written for Alyssa's merged card cannot be edited.
Not a defect: Jessica Marsteller's two calls on Nov 4 (12:28 and 12:50 MT) are the Situs receptionist, not Clara. Both times she asked for Deiby or Miriam and was told "let me connect you"; the phone system uploaded no second leg either time, so whether the transfer rang unanswered is not visible to us. Cosmetic: the transcriber hears "Situs" as Citus, Cyrus, Isis, Playfish; a vocabulary hint would fix it.
Method: every file read from the customer's upload bucket (leg metadata, length, file-name stamp), compared with the thread rows and the ingest logs; page order confirmed from the thread component (rows sorted by stored time, voice segments labelled from the stored direction). The smoke login cannot open people pages at Situs (it holds no membership), so the page check used Fede's own screenshot.
Sources: Crexendo help center (retention, remote storage, recording), NetSapiens API docs, the repo map taken 2026-10-28. Bucket and key details are in the session notes, not here.