Voice merge, drawn: two agents today, one agent after

The current state of Camellia's agents and the Western Slope prototype, the surgery that joins them, what the phone does at the start of a call to pick the single-property script or the centralized one, and how that lines up with Gera's architecture.

2026-09-09 · Proposed, pending Fede · the written plan, the decisions and the evidence are in section 11 of the voice decision page; this page is the picture

0 · Picture it: how a call works today, and what the merge changes

shared by every customerprototype onlyElevenLabs owns it
Camellia's numberone real apartment building Willows & bench test numbersfor testing only, nobody real calls in Western Slope's 970 numbertheir line forwards here today Production agent ringsthe live customer fleet Sandbox agent ringsa separate copy, same wording Prototype agent ringsa one-off, written by hand same wording Our server answers first: "which property is this, and what does it need to know?" looks up the property behind the dialled number decides: one building, or a group of homes — then builds a one-call "fact sheet" Fact sheet: property facts, hours, the homes for rent the "which homes do you want?" script is EMPTY for one building, FILLED for many homes ElevenLabs speaks: its stored script + the fact sheetproduction & sandbox read the SAME script; the prototype reads its OWN script The front door greets the caller, then hands off to Leasingthe prototype has no hand-off — it is one agent doing the whole call Tools: book, move, or cancel the tourwrites the database row, the calendar hold, and the property manager's email Call ends → a confirmation text to the prospect

Three numbers, one server. Camellia and the Willows/bench numbers ring separate copies of the same script; the Western Slope prototype rings a hand-written one-off that shares the same tools but none of the wording. Key files: src/app/api/voice/personalization/route.ts (the lookup + fact sheet) and agents/clara/lib/voice-agents/leasing.ts (the shared script).

NumberPropertyLine hangs onAgent it ringsWhere the words come from
Camellia's numberCamellia Apartments, one real buildingOne buildingProduction front door → LeasingThe shared script
Willows test numberPropFlow's own test buildingOne building (bench for the Camellia script)Sandbox front door → LeasingThe shared script, byte-for-byte the same text as Camellia's
Bench test number (+1 720-807-1724)A test "many scattered homes" recordCompany-wide (many-homes bench)Sandbox front door (same agent as Willows) → LeasingThe shared script, but fed a many-homes fact sheet
Western Slope's 970 numberA placeholder record standing in for their 16 homesCompany-wide, today on the prototype agentThe prototype agent, on its ownA separate script written by hand for this one client

0.1 · The proposal: two scripts, one front door

shared, unchangedfrom the prototypethe new fork
Any of the numbers aboveCamellia, the test lines, and later the 970 line One front door, one script, for everyonesame fact sheet as today feeds in here Today: the placeholder buildingtype "scattered homes" (tagged, Phase 0) After Gera's Phase 3the number's own address-head row (org + scope) The line's scope from the resolver: company-wide, or one buildingthis is a fact the server already knows, not an instruction the model has to obey One building → today's Leasing scriptCamellia's script, byte-for-byte unchangedthis is the file nobody touches Company-wide → the prototype's scriptthe qualify-first, many-homes script, proven on Western Slopeits own separate file Tools: book, move, or cancel the tourthe same database, calendar, and PM-email path either way — unchanged at the re-key

The fork reads one derived value — the line's scope from Gera's resolver, company-wide or one building — never wording the model has to choose to obey. Today that answer comes from the placeholder building's type ("scattered homes"), tagged in Phase 0; after his Phase 3 address head (the ADDR#…/HEAD nameplate row) lands, it comes from the number's own row, carrying the org and the scope directly. The re-key at Phase 6 only swaps that source: the fork, the two scripts, the shared tools, the fact sheet and the prompt do not change. Camellia's script file is never edited. Both fleets still deploy from a single merge, the same as today. The 970 line moves onto this shared front door later, as its own explicit cutover — from that day its calls simply take the company-wide branch. Key files: agents/clara/lib/voice-agents/leasing.ts (unchanged, single-building script) and agents/clara/lib/voice-agents/prototypes/western-slope-portfolio-triage.ts (source of the many-homes script). See Gera's resolver and the phase tracker.

0.2 · End state on Gera's model

shared, unchangedscope = one buildingorg-level, per Gera's model
Number → Twilio → ElevenLabs → our webhook RESOLVER reads the address headwhich org, which scope — company-wide or one building; unknown number REFUSED, fail-open gone Org walleverything read from here is scoped to that org Attachments resolve, nearest hook winsphone line · leasing mailbox · org calendar or the building's own calendar · PMS credential · office hours Fact sheet built for that scopeone building's units, or the org's homes across buildings — each on its real building row; guest cards land on the listing's building ONE shared agent, one tool set — same for every customer Single-building scripttoday's Camellia script, unchanged Many-homes scriptthe qualify-first script Our server books the touragent only calls 'book a tour'; server asks the resolver which calendarapplies (the building's own, else the company's) and writes the event there Conversation and prospect rows, keyed under the org Western Slope's 970 line is just another number on the shared agent — the prototype agent is retired Camellia · Yale · Willows · bench take the same path, with scope = one building

Plain English: a call rings in, the resolver reads the number's address head and says which org and which scope — an unknown number is refused, not adopted by a default. Everything downstream is read inside that org's wall. Attachments (the phone line, the leasing mailbox, a calendar, a PMS login, office hours) resolve by nearest hook — the org's own if the building has none of its own. The fact sheet is built for that scope, one shared agent and tool set handles every customer, and the only fork left is which leasing script to speak from — the rest, including which calendar our server writes the tour to and which channel a notification follows, is already scope-driven today. Western Slope's 970 line ends up exactly where Camellia, Yale, Willows and the bench already are: a number on the shared agent, differing only in scope.

What retires at each phase.

The whole idea in one line. The agents are identical for every customer. The only thing that differs between a Camellia call and a Western Slope call is what our server hands ElevenLabs in the first 300 milliseconds, and that is decided by the record behind the number that was dialled.

1 · Today: two agents, one server

shared by every customerprototype onlyElevenLabs owns it
CAMELLIA (and Yale) WESTERN SLOPE, the 970 line Camellia toll-free numberone number = one agent Yale numbersame agent Clara, front doorone prompt, {{slots}} filled per call Leasing Maintenance Residentservices Renewals silent hand-offs; the caller hears one Clara 970 number (their line forwards here)one number = one agent Portfolio triage, PROTOTYPE one agent, three branches inside the prompt: leasing · maintenance (dials their centre) · anything else own turn settings · own deploy lane · own kill switch office hours and company facts typed into the prompt no hand-offs; asks beds, area, budget first; names two homes; books the one the caller picks PropFlow server, one URL for all 26 agents call-start webhook · the four leasing tools (the same records) · post-call webhook Camellia property recordapartment building, bedroom bands Western Slope placeholder record"scattered homes", 16 homes as units

Two families of agents, one server. The prototype already books through the same tools and the same handlers as Camellia's leasing agent; what is separate is the prompt, its settings, and how it is deployed. The record behind each number is already what makes the inventory block read "one line per home" for Western Slope and "bedroom bands" for Camellia.

2 · The surgery: what moves, what is thrown away

taken from the prototypewhere it lands on the shared fleetnew, small
FROM THE PROTOTYPE TO Qualify-first script (beds, area, budget, two homes, same street) Office hours, lunch gap, holidays written in the prompt Company name, main line, maintenance centre as constants After-hours "I'll pass your message on" (transcript only) Turn, voice and transcription settings, copied by hand Its own deploy lane, 943-line script, kill switch, roster row "Tour request" text for a line that could not book, plus its flag The 970 number itself One block in the shared Leasing prompt, rendered only in portfolio mode Property record → the shared after-hours desk (gains a midday gap) Property record fields, read at ring time, fail closed when blank The existing note tool and the missed-call email The fleet's settings, under the fleet's drift guard Deleted. The one fleet writer deploys on every merge. Deleted. A tour is confirmed or it is not. Repointed to the shared front door: one config row, on merge

Only the first row is genuinely new text on the shared fleet, and it lands behind a slot that is empty for every apartment property. Everything else is either data on the property record or a duplicate that dies. Camellia's record does not change in any row.

3 · After: one front door, two lines, and where the fork actually is

sharedthe one new factElevenLabs
Camellia numberbound to the shared front door 970 numberbound to the SAME front door Clara, front door (one agent id)ElevenLabs asks our server before the first word Our server answers first: which company is this, is the line company-wide or one building, and what does it need to know? 1 · number → property record (one lookup, already exists) 2 · read the record: shape, office hours, desk number, timezone, inventory (already exists) 3 · derive the intake mode from what the line reaches and the record's shape returns ~90 variables that fill the {{slots}} in the shared prompt; ElevenLabs then renders and speaks Leasing intake: singleintake directive = (empty)inventory block = bedroom bands Leasing intake: portfoliointake directive = the qualify-first rulesinventory block = one line per home "Which unit are you interested in?"two concrete times → bookbyte-identical to today's Camellia call bedrooms → area → budget → two homescaller picks one → two concrete times → booksame-street pair = one visit, two bookings schedule_tour → same handler, same calendar path

The fork is not in ElevenLabs and not in the tools. It is one derived value in the webhook, delivered as two variables: a labelled fact line the prompt can read ("Leasing intake: portfolio") and a directive block that carries the rules when the mode is portfolio and is empty otherwise. The prompt branches on the literal word, never on a variable's name, because ElevenLabs substitutes values before the model reads anything.

Alignment with Gera's portfolio design: today the answer comes from the placeholder building record (type "scattered homes"); after his Phase 3 address head lands, it comes from the number's own row (company + scope), and after Phase 6 the placeholder is archived. The fork reads that one answer; the scripts, fact sheet, and tools do not change. See https://docs.propflowai.co/a/portfolio-architecture-phases.

4 · In ElevenLabs terms

MechanismUse it?Why
Phone number → agent bindingyes, alreadyOne number, one agent id. Camellia and the 970 line end up on the same front door id. The 970 line's row is the cutover and the rollback.
Personalization webhook → dynamic variablesyes, alreadyThis is the fork. We already send about ninety variables per call; the merge adds one fact line and one directive block. Same shape as the closed-office directive and the hand-off policy today.
First-message override from the webhookyes, alreadyThe greeting is already a per-property variable, so "Thanks for calling Western Slope, leasing, maintenance or something else?" needs no new mechanism.
Hand-off to specialists (transfer to agent)yes, alreadyUnchanged. Front door to Leasing as today; the intake variables ride along because the child agent gets the same call-start payload.
Full prompt override per call from the webhooknoDocumented and possible, but every override field is off by default and the webhook is one URL for all 26 agents. A mistake there reaches Camellia. Dynamic variables into one shared prompt are the safe subset of the same feature.
Branches and traffic splits on an agentnot for thisA branch is reachable only by a number pointed straight at it: hand-offs land on the child's live main, the simulator ignores branches, and the branch id our webhook can return is a documented no-op for us. Fine for iterating one agent's wording later, wrong as the way two numbers differ.
Workflows (node graphs, subagent nodes)not nowNever used on any of our 26 agents. Whether node settings quietly override the webhook's per-call variables is an open question to the vendor (Q5 on the decision page). Not a dependency for this merge.
Duplicate agent per customernoThat is the second agent family; the prototype already is one, and the plan deletes it. Copies made with inline tools minted 15 duplicate tools in July and rebound seven live agents.

5 · Does it match the new architecture?

This page saysGera's HOW page saysSame thing?
Intake mode: single or portfolio, derived at ring timeScope of the nameplate: building, group, or company; minted once at the edgeyes — single is a building nameplate; portfolio is a company nameplate, or a building whose record holds many homes
Inventory block: bedroom bands or one line per homeavailable_homes grouped by area, built in code, not by the modelyes — the one-line-per-home block is already shipped and shared
Phase one: the property is known at pickup (one record)home_state: knownyes
Phase two: 14 records behind one line, ask, then stamp oncehome_state: unknown → known, bindHome writes the stamp once, tools carry a home referenceyes — not built in this plan; the qualify-first script is the same text in both phases
Never widen the one-property resolver in placeA sibling "reach" resolver beside it; Camellia stays HEAD → its buildingyes
Cutover by repointing the number's row on merge"The claim writes the platform ids back onto the head; config files become dumps"same direction, different stage — today the row is hand-edited config synced on merge; in the target it is generated from the nameplate table. Phase one uses the file that exists.

One rule keeps the two aligned while phase two is built: the intake mode is derived from what the line reaches and the record's shape, never from the record's shape alone. That way, the day a number reaches fourteen records, the mode reads "portfolio" without another rule, and the only new work is the binding, not the script.

6 · Deployment strategy: Camellia intact, the client's number on the merged agent, then everyone

The trick that makes it safe: we already have a second, separately-deployed copy of every shared prompt. The Willows test fleet has its own agent ids, receives every merge automatically, and is bound to the bench number. That fleet is our branch. A merge changes both fleets identically; Camellia's copy renders byte-identical because its record has no portfolio shape; the Willows copy is where we call in and break things.

STAGE A · BENCH (weeks of Sep 14 – Oct 2) Camellia number → shared front door 970 number → prototype agent (unchanged) Harness number → Willows fleet + bench record every merge lands on both fleets; only the harness line renders portfolio mode; nobody dials Camellia STAGE B · CUTOVER (one row, Fede's go) Camellia number → shared front door 970 number → shared front door prototype agent parked 7 days, then deleted rollback = the 970 row put back (a merge, 10–15 min through the queue), drilled first on the harness line STAGE C · EVERYONE every apartment line → single mode every scattered-home line → portfolio mode Situs / 14-record Western Slope → phase two nothing to "turn on": a customer gets the right script by having their record described correctly What never changes in any stage Camellia's number-to-agent row · Camellia's property record · the four leasing tool records · the call-start webhook URL A drift test renders Camellia-shaped variables before and after every merge and fails on any byte that moved and no test ever dials Camellia's or Yale's number

Step by step

#StepWhat exactly happensWho
1Import the harness number into ElevenLabs and bind it to the Willows front doorThe number exists (bought Sep 7). Importing it is one third-party write; then one row in the phone-numbers config and one in the phone registry, merged, so the number-to-agent binding is on record and drift-checked.Fede gives the go for the import; a session does the rows
2Create the bench portfolio recordA test property: scattered-home shape, test flag on, added to the bench list so bench codes and test callers work, its homes refreshed from the same public listings page, a bench calendar of its own. Map the harness number to it. Merge.Session
3Prove the wiring before any prompt changeCall the harness line yourself. You should hear today's shared leasing script, already with one line per home (that block is shared and shipped). Book a tour into the bench calendar. Run the existing leasing robot scenario against the line. Nothing about the prompt has changed yet.Fede calls; session runs the robot
4Close the two open cross-company lookups on the voice pathPost-call handler resolves the dialled number's property before it looks the caller up; the leasing tool's prospect fallback is scoped to that property. The pinned counter drops from 20 to 18. Merge. This is the cutover gate, and it is Camellia-safe: both narrow a lookup.Session
5Ship the fork, darkThe intake-mode variable and the directive slot in the shared leasing prompt, with the directive empty for every property. The byte-identical drift test for apartment records, the render-trap fence, the emit-to-consume wiring test. Merge: both fleets get it; no caller hears anything different.Session
6Ship the script, dark for everyone but scattered-home recordsThe prototype's qualify-first rules move into the directive. The prototype's own tests are re-pointed at the shared prompt; the same-visit eval pack imports the shared prompt; every new simulated case fails on the old prompt. Real-history replay of the 50 prototype calls and Camellia's last 30 days, before and after, is the proof in the pull request. Merge.Session
7Test loop on the harness line, as many rounds as it takesYou call the harness line and try to break it; robots run the leasing-tour, two-homes-one-street, after-hours-message and in-hours-person scenarios; Sentry is swept for each call window; unknown-caller cleanup after every run. Each fix is a small pull request and a merge. The client's 970 line is untouched throughout because it is still on the prototype agent.Fede calls; session fixes and reruns
8Data instead of constantsOffice hours with a midday gap; maintenance intake numbers; company facts on Western Slope's record. The shared after-hours desk and the note tool take over message-taking. The tour-request flag and its module are deleted. Merge, then repeat step 7 once.Session
9Rollback drillRepoint the harness number's row away from the Willows front door and back, two merges. Time it. This proves the exact motion the cutover will need before the customer's line depends on it.Session; Fede reads the timing
10Cutover, weekday morning, someone watchingOne pull request: the 970 number's row moves from the prototype agent to the shared front door; the prototype is set to its disarmed state in the same merge. You call the 970 line as soon as the phone-number sync reports done. Then the first real forwarded calls, graded per turn as today.Fede's explicit go, then session merges and both watch
11Soak seven daysEvery real call graded; disagreements become corpus cases; the missed-call email is the only thing that reaches the client. Rollback stays one row away the whole week.Session reports; Fede reads
12Delete the prototypeIts module, lane, script, roster row and agent are removed. The removal trigger written into it on Sep 5 is executed.Session
13"Everyone" is already done; Situs is phase twoEvery apartment line has been in single mode since step 5 with nothing changed. Any future scattered-home customer gets portfolio mode by having their record described that way at onboarding. A line that must reach several property records (Situs, and Western Slope on its 14 real records) needs the company binding from Gera's program, which reuses this script unchanged.Gera's program
What you personally do, in order: give the go to import the harness number (step 1) · call the harness line after step 3, and again during step 7 as often as you like · read the rollback timing (step 9) · give the go for the cutover and call the 970 line right after (step 10) · read the soak report (step 11). Everything else is sessions and merges, under the standing grant, with every pull request dark for Camellia by construction and proven so by a test.

7 · What Fede decides

Six choices, each with a recommendation, on the decision page, section 11.5: what "one agent" means, the bench, cutover and rollback, how the mode is chosen, the cutover gate, and whether the umbrella record goes onto the shared fleet now or waits for the 14-record binding. Turning anything on at a real line stays a separate, explicit go.

PropFlow Docs