Customer phone-system recordings into PropFlow

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.

The flow

Crexendorecords, uploads
→
S3 bucket, oursone per customer
→
new file eventLambda → queue
→
parsepath + tag → caller, staff, time, call id
→
transcribeexisting Scribe wrapper
→
match personexisting phone lookup
→
timeline rowlike a staff-answered call
→
engineswork orders, leads, brain

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 works today (proven 2026-10-28)

What works today (proven 2026-10-28)

Before

Calls Situs staff take live only inside Crexendo, invisible to Clara, deleted after 90 days.

After tonight

Every recorded staff call is copied to a private Situs-only bucket we own, about two minutes after hang-up, with the caller's number inside the file. Transcribed in 30 seconds.
What arrivedValue from the test call
PathSitusGroup/2026/10/29/aud-<UTC start>-<call id>-8656.wav
Audiomono, 8 kHz GSM WAV, 17 s, 28 KB
Tag inside the fileaor0=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 includedNo transcript, no dialed number, no missed/answered flag. Voicemails are not call recordings and never reach the bucket.
Pieces and the seam each reuses

Pieces and the seam each reuses

PieceWhat it doesReuses
TriggerBucket "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 mappingA 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 adapterReads 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.
TranscriptionMono audio, so speaker split comes from diarization, not channel split.Existing Scribe wrapper, with a diarize-on variant.
Timeline rowOne 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 callThe 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.
Switchvoice.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).
The one decision: which engines run on a staff call

The one decision: which engines run on a staff call

OptionWhat happens after each callVerdict
A. Capture and actTranscript 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 onlyTranscript and audio on the timeline, nothing else.Decided, Fede 2026-10-28
C. Full Clara post-callEverything 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
Privacy gate (Fede, 2026-10-28 night)

Privacy gate (Fede, 2026-10-28 night)

What two office days of calls look like against PropFlow (Oct 27–28, read-only)

What two office days of calls look like against PropFlow (Oct 27–28, read-only)

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 outInbound, office hoursInbound, after hours (to Clara)
Calls29598
Distinct outside numbers20383
Known to PropFlow16 (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
Unknown4220

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.

StaffOutbound callsMinutesOver 10 seconds
One staff member (ext 2007)164610
Assistant VA1 (2014)775
Three others653

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.

Practice days (Oct 29–31): the whole pipeline runs, nothing is written to PropFlow

Practice days (Oct 29–31): the whole pipeline runs, nothing is written to PropFlow

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.

DayCallsKeepDeleteGate would have dropped a real customerNotes
Wed Oct 29 (full office day) · call-by-call report13 distinct calls (17 recordings: 4 transferred calls produced two files each) + 1 test5 calls (6 recordings)8 calls (11 recordings)1Injectable 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.

Quality check of both office days (Oct 29–30), graded against the transcripts on Nov 1

Quality check of both office days (Oct 29–30), graded against the transcripts on Nov 1

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.

DayCalls (files)Keep rightKeep wrongDelete rightDelete wrong (a customer dropped)
Wed Oct 2915 (20)8070
Thu Oct 3017 (22)10043
Both days32 (42) + 1 team test call180 (the test call is kept and must be excluded)113

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.

Option A practice run: what Clara's engines would have done (Oct 29-30)

Option A practice run: what Clara's engines would have done (Oct 29-30)

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.

Today (option B)

The transcript sits on the timeline. Nothing else changes.

With option A on these 18 calls

0 work orders, 0 new leads, 9 existing cards updated, 2 showings added to the calendar that PropFlow did not have, about 16 durable policy facts for the brain.
WhatResultVerdict
Work orders0 of 18. The one maintenance call was a pest-spray question. A planted leak call did file one.Right
Leads0 new. Every caller was already on file; 9 cards would get notes, bedrooms or move-in filledMostly right; 2 wrong fills
Showings2 booked on the call, neither on PropFlow's calendar. A tour that was already booked was not booked againReal gain
Brain notes38 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 fastUseful, needs a filter
Follow-up tasks37, nearly all things staff already promised on the call. 3 voicemails staff left were read as "call them back"Would double up
Sensitive content7 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 listBlocker

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.

Nov 1: attached and live at Situs

Nov 1: attached and live at Situs

Before

Recordings piled up in the bucket since Oct 28. Nothing in PropFlow. Staff logged 3 of 18 real calls by hand.

After

Every kept office call is a conversation on the caller's timeline, with audio and a per-speaker transcript. New calls land about 3 seconds after the phone system uploads them. Staff log nothing.

Backfill of everything in the bucket (47 files)

OutcomeFiles
Kept → 18 conversations (transfers joined into one entry)24
Deleted by the privacy gate18
Empty files, skipped2
Team test call, excluded1
Willows proof files, already recorded2

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".

Situs Group admin settings in production with the Office phone recordings switch on
Situs admin page in production, Nov 1.

What is set

PieceState
Company switch at SitusOn
MappingBucket → Situs folder
Forwarder and its ruleus-west-2, Terraform infra/pbx-recordings-trigger
Bucket notificationsOn
Processor settingsSet 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

Call notes on the guest card (live at Situs since Nov 1; no separate setting)

Call notes on the guest card (live at Situs since Nov 1; no separate setting)

office call recorded
→
kept by the privacy gate
→
an entry in the person's own conversationheading, recording, per-speaker turns; no per-call page
→
Clara reads the lead's guest cardstaff already logged this call? then nothing is written
→
one "log a call" entry15 min after the call, through the sensitive-content filter

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 showsIts own page, named after the phone system's call idInside the person's existing conversation, in order with their texts and emails
Lead calls on the guest card3 of 14 (staff, by hand)Every kept lead call; staff's own entries are respected, never duplicated
Time on the entryHand-typedThe call's real time
SwitchesTwo (recordings, call notes)One (recordings)

Proven at Willows, Nov 1 (twice)

A test prospect's conversation in PropFlow showing an office call with recording and per-speaker turns between her earlier messages
Second test call (43 s): it landed in the prospect's existing conversation 6 s after upload, and one guest-card entry followed 36 min later. The planted sensitive sentence is absent from the entry.
Test guest card in AppFolio showing one Call entry written by Clara
First test call (55 s): one Call entry on the test guest card 20 min later; the planted health sentence is absent.

Situs backfill, Nov 1

Open

To verify before the rule is final (one data point each so far)

Timeline QA, Nov 4: why office calls look out of order on the person page

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.

DefectWhat a reader seesCauseProof caseFix shape
1. Outbound calls land ~1.5 min lateA 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 cardOne "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 legsThe 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 threadThreads 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.

Nov 5: what changed (before → after)

DefectBeforeAfterProof
1. Outbound call timesOutbound 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 cardAlyssa'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 swappedPer-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 silentlyTranscription 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.

To verify before the rule is final (one data point each so far)

Proof ladder and PR cut

Proof ladder and PR cut

  1. Mapping + switch. Merged and live 2026-10-29 (the "Office phone recordings" row on the customer admin page, off).
  2. Parser + trigger. Merged 2026-10-28.
  3. Practice days (above): the full pipeline in log-only mode, Oct 29–31.
  4. Transcription + timeline row. Proven on Willows: a staff-shaped recording lands on a test person's timeline with a Play button.
  5. Situs supervised week: one Situs staff user with recording on, switch on for Situs only, Fede's call. Engines are a later decision.

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.

PropFlow Docs