The Purchase-Order Pivot — vendor coordination strategy, evidence & ADR-0116

Phases 1–3 shipped · Camellia not yet flipped ADR merged: #4720 follow-up #4773 held for review updated 2026-07-28 companion: pivot brief

This page is the whole story: what the evidence says about how Camellia actually coordinates vendors, how each PropFlow channel behaves today and where it breaks, what turnovers need, the decision (ADR-0116), and the phased plan. The full technical specification lives in the appendix at the bottom and in the canonical ADR (docs/adr/0116-vendor-facing-job-reference-per-property.md, merged in PR #4720).

Status, 2026-07-28. The ADR is merged and phases 1–3 are on main — read model, binder + PM confirm lane, and vendorFacingRef() at every vendor-facing chokepoint. The live pilot is The Willows (sandbox bench property); Camellia has not been flipped, and that call is Fede's. Separately, Camellia's PM confirmed the PO-first sequence directly (§3) — which turns the key design assumption from an inference into a fact and opens a new outbound constraint (§8). Ship log: §8.

1. The strategic problem, in one paragraph

Every vendor-facing thing PropFlow does — dispatch emails, outbound calls, SMS, reply attribution, turnover scheduling — is built on the assumption that the work order is the number vendors live by. At our only real production property, that assumption is false. jpco runs Camellia's vendors on AppFolio purchase orders: the PO is the number given at scheduling, the number vendors put on invoices, and the number they chase when it's missing. The work order is an internal artifact vendors never see — and worse, PropFlow is minting AppFolio work orders no human at jpco uses, then waiting on WO-status changes that structurally can never come. The pivot is not "add POs"; it is make the vendor-facing job reference a per-property fact instead of a hardcoded one, and re-anchor every channel — voice, email, SMS, attribution, turnovers — on that fact.

2. The evidence base

919
POs in the jpco export
0
of them linked to a WO in AppFolio
0
vendor calls citing a WO id, 105 days
25 / 66
golden-set calls flip under the pivot (38%)
0 / 1,783
emails naming both a WO and a PO
24 / 25
Camellia turnovers wedged today

Everything on this page traces to one of these research assets (all produced from production data, 2026-07-27):

AssetWhat it isWhere
PO corpus919 AppFolio PO rows (2022→now), all line items, creators, approvalspo-training-data/jpco-purchase-orders.json
Golden call dataset66 production vendor interactions, hand-labeled WO-era vs PO-era handling + flip list; every PO citation re-verified against the exportpo-training-data/golden-vendor-calls.jsonl
The "Joanna spec"The human PO workflow reconstructed from transcripts + PO metadata, every claim citedpo-training-data/joanna-po-workflow-spec.md
PM confirmation primaryJoanna Weiser (on-site PM, jpco) confirming the PO-first sequence by text, 2026-07-28 — the only non-mined source here, and it settles the ordering question the corpus could not (§3)Joanna spec §10
Mailbox mining1,783 Camellia emails with bodies; PO/WO/invoice reference extraction; invoice-PDF sampling (44 attachments read)po-training-data/vendor-emails/
Corpus anomaly studyRegime changes, lag distributions, seasonality, spend — 27 CSVspo-training-data/anomaly-study/
Binder feasibilityCollision-rate measurement for vendor+unit+date matching on real datapo-training-data/binder-study/
Industry doctrineWO-vs-PO across AppFolio/Yardi/Buildium/Entrata/MRI + procurement literature, honesty-labeledpm-domain-knowledge/work-orders-vs-purchase-orders.md
Turnover inspectionWhy 24/25 Camellia turnovers are stuck; the minted-WO dead endpivot brief + this page §5

3. How the humans actually do it (the Joanna spec)

The workflow Clara has to slot into, as evidenced — not as imagined:

  1. The PO is cut first, and it is what confirms the job — see the unit, put the PO in AppFolio, then call the vendor. What lags is transmission, not creation: "I'll have that PO, I'll reply with the PO if that's okay, or I'll call you back." PM-confirmed 2026-07-28 — see the callout below; this page previously read the quote as evidence the PO did not yet exist, which was wrong.
  2. Erika creates the PO in AppFolio (351 of 381 modern POs) — the number comes from a strictly sequential shared counter. Approval (Kayla) is decoupled and late: median 15 days, and 4 of the 5 PO numbers spoken on real calls were still Pending when spoken.
  3. Joanna reads the number to the vendor over the phone, off a list. The PO number moves by voice: across 35,315 PropFlow messages, a PO regex hits 3 times — all voice. In email it appears only as a hand-typed line in a reply (34 instances: "The PO is 662").
  4. The vendor invoices against it — and chases when it never arrived (Miracle Method invoiced 9 days after the PO existed because nobody had transmitted it).
"Do you have that PO? I don't think I got a PO." — "It is six eighty-two." Production call, 2026-07-27. PO 682 (Metropolitan, "Clean House 7 days") was issued that same day. All five PO numbers spoken on sampled calls resolve to real PO rows with matching vendor, unit and scope.
"So we get the po first because that's how we confirm the job with the vendors they need a po before we can do anything." — and, to "so see the unit, put in PO in appfolio then call?" — "Yep!" PM-confirmed, 2026-07-28. Joanna Weiser, on-site PM at jpco, by text to Fede — primary evidence, not corpus mining. Everything else in this section is reconstructed from transcripts and PO metadata; this is the PM stating her own process. It upgrades an inference: PO-creation dates sat same-day or one-day-before the call on which the number was spoken, but AppFolio's CreatedAt is date-granular and could not order them. Accounting approves the PO later, which is why numbers get read to vendors while still Pending.

Why it matters for the design. Three consequences, all new as of this confirmation: (1) the 15-minute PO sync is comfortable by construction — a PO exists before the PM ever dials, so the read model never races a vendor callback; (2) outbound and inbound become asymmetric — a vendor calling about a job with no PO is normal, but an outbound-initiated job with no PO is off-process ("they need a po before we can do anything") and should be flagged, not worked around; (3) outbound scheduling calls should lead with the PO number, because it is what the vendor's back office keys on (17/21 sampled invoice PDFs carry it back, §7). Those become the outbound lane in §8.

Two structural facts still matter for design: the PO lags the turn (a unit routinely sits for days after move-out before any vendor work is scoped and a number cut — "no PO yet" is a normal state on a turn, not an error), and vendors have learned to route around Clara — "speak to Joanna" opens half of the PO calls in the golden set.

4. Channel by channel: today vs. what changes

ChannelTodayBreaks at Camellia because…After the pivot
Outbound vendor calls (ADR-0111)Carries no job number by design; the agent is scripted to refuse the PO question, and an eval fails it for stating any POThe first thing a vendor asks for is the PO — the golden set shows humans answering it on the lineThe confirmed PO arrives as a call-start dynamic variable (no mid-call lookup — that's the corpus's worst failure surface); the eval flips to "answer with the supplied PO, never invent one"; no PO yet → "I don't have a PO number for that yet" + job description
Vendor dispatch email (ADR-0059 lane)Requires the AppFolio WO id as a hard precondition and renders it to the vendorThat's precisely the number Camellia vendors have never used (0 calls in 105 days)Keyed on property + unit + scope; renders vendorFacingRef() — PO when confirmed, plain job description when not; the WO-id precondition is dropped
Inbound voice (ADR-0113)"For questions about purchase orders or vendor billing… let me transfer you." Signature failure: "I can only open work orders for current residents" — said to a vendor with 101 POsPO topics are the vendor's main business; transferring them is why vendors skip ClaraClara answers PO-scoped questions from the read model (listOpenPosForVendor for call prep, org-scoped number lookup for "calling about 4471"), escalates only what's genuinely human
Inbound email + reply attributionBare integers were ambiguous between the WO and PO number spaces; a wrong match binds a vendor reply to the wrong jobBoth number spaces are live at a PO propertyShipped ahead of the pivot: PRs #4710, #4716, #4718, #4721 — PO-shaped references (incl. "P.O. 644", "purchase-order 644", "P/O 644") suppress WO binding; the standing rule is no attribution beats wrong attribution
SMS / Telegram headlines, conversation labelsRender the WO idSame wrong numberRender vendorFacingRef() — one resolver, five call sites, drift-guarded
Spend view (/maintenance/costs)Dollar amounts regex-parsed from inbound email bodies, grouped by vendor email domain; no PO/WO joinEmail-domain spend is not ledger truth; commitment (PO) and actual (invoice) never meetSuccessor lane (§8 phase 4): AppFolio bills read model + invoice-PDF extraction — 17/21 sampled invoice PDFs carry a real PO inside, even when the email doesn't (§6)

The "after the pivot" column has shipped. Every row above is on main as of 2026-07-28 — the resolver and its five chokepoints in #4745, the inbound triage lane in #4760 — with one exception: the spend view (phase 4b) is unchanged. What has not happened is flipping Camellia's mode, so at Camellia these paths still resolve to work_order and behave exactly as the "today" column describes. Ship log: §8.

5. Turnovers: how they work today, and what must change

The turnover inspection produced the sharpest single finding of the whole effort. Today's turnover flow at a PMS property: PropFlow detects a departure → creates scope items → mints an AppFolio work order per work item → dispatches vendors referencing that WO → waits for the AppFolio WO status to change to mark the step complete (ADR-0030: AppFolio WO status is authoritative, polled every 60s).

At Camellia every link in that chain is broken:

Median move-out → first-unit-PO lag, days, by year (anomaly study q2; corroborated by AppFolio's PO-independent days-to-complete metric, 15–17 d → 46 d in 2025)
20222 20239.5 202417 202544 2026*23 * partial year · survives outlier-dropping (44→33) and all five match windows

What changes for turnovers:

  1. Stop gating completion on AppFolio WO status at purchase_order properties. The signal must come from what actually happens: vendor reply ("job's done"), PM confirmation, or inspection — the completed work order is the industry's closest analogue to a service-entry sheet, and at Camellia we are the only party who can author it.
  2. Make minting AppFolio WOs optional per property. Where no human uses them, they create confusion and a false integration surface. (WO minting keys off the same vendorJobReferenceMode decision — one config, coherent behavior.)
  3. Extend the scope vocabulary: a blinds trade, and whole-property work items so "Clean House N days" is expressible and bindable (the PO model handles 1:0 property-wide POs via a sentinel).
  4. Bind POs to turnover jobs so the turn board shows commitment reality: which steps have a PO, which are waiting (normal for ~2 weeks), what the committed cost is.

Un-wedging the 24 stuck turnovers is an ops action gated on the completion-signal redesign — they cannot self-heal under the current rule.

6. Vendor adoption: earning back the front door

Half of the PO calls in the golden set open with "speak to Joanna" — vendors have learned to route around Clara, because on their core business she has been a dead end (scripted refusal of the PO question, "I can only open work orders for current residents," transfer). Adoption is therefore a trust-repair problem, and the levers are ordered by what the evidence says actually retrains a caller:

  1. Competence before persuasion. Nothing works until Clara can do the job: give the PO number, answer PO/billing status, know the vendor's open POs. That's phases 1–3 — adoption is the reason for the pivot, not a separate campaign.
  2. Prove it in the first utterance. Company-name recognition (#4709) + listOpenPosForVendor means a recognized vendor gets context immediately — "Hi Mike — calling about the unit 210 fridge, PO 667?" — the capability is demonstrated before the caller can ask for Joanna. The strongest anti-routing move is the first five seconds.
  3. Never fight the handoff — convert it. On "speak to Joanna": offer the concrete thing once ("I can give you the PO number or scheduling right now — or I'll get Joanna") and transfer gracefully if insisted. Then loop back with the artifact: after the human handles it, Clara sends the confirmation SMS/email carrying the PO — so even a routed-around call ends with Clara being the system that delivered the number they keep.
  4. Outbound is the retraining channel. Every dispatch email and outbound call that carries the right number teaches vendors that Clara is where the numbers come from. Today the humans are the only source of POs; after phase 3, Clara is the fastest source — "reply to this thread or call this line and reference PO 682" makes her the path of least resistance.
  5. Retire the trust-burning failure strings. The both-direction evals plus a dedicated eval on the golden set's 14 clara_failed calls: the exact utterances that taught vendors to skip her must score zero.
  6. Measure it or it didn't happen. Baseline from the golden set (66 calls): handled by Clara 11, human-after-transfer 40, failed 14; "speak to Joanna" opens 4/8 PO calls. Standing metrics: transfer-request rate on vendor calls, Clara-resolved rate, per-vendor trend (a trust ledger per VendorCompany — A&K's trajectory matters more than the average). Reviewed against the baseline after Camellia flips.
  7. Optional accelerator (needs Fede's sign-off on copy): a one-time note to the top PO vendors — "you can now get PO numbers and scheduling instantly at the Camellia line" — outbound customer-facing comms, so drafted only on request.

Prevention long-term: the routing-around behavior returns whenever Clara's competence lags the vendors' actual business. The adoption metric belongs on the same dashboard as the sync-health strip — a rising transfer-request rate is an early-warning signal, not a UX nit.

7. What the industry + the paper trail say

Doctrine (research-verified, honesty-labeled)

The invoice-PDF finding (new, flips an earlier conclusion)

Sampling 44 invoice attachments from the mailbox: 17 of 21 PO fields inside the PDFs are populated with a real Camellia PO — including five vendors whose emails never mention one (MTech, Home Depot, HD Supply, Bomar, Metropolitan). Genuinely PO-free invoices: Anchor Pest (keys to our WO number — the one vendor that uses it), American Garage Door, TK Elevator, Western States. Miracle Method and A&K never email invoices at all. Caveat that shapes the design: ~12% of printed POs are wrong (HD Painting stamped a Metropolitan PO on its invoice) — invoice-extracted POs must validate against the PO read model, never be trusted raw.

8. The decision (ADR-0116) and the phased plan

The decision, in five lines (full text in PR #4720, merged):

  1. The vendor-facing job reference is per-property configurationProperty.vendorJobReferenceMode: 'work_order' | 'purchase_order'; absent = today's behavior; set at onboarding with an advisory auto-suggestion; no third mode until observed.
  2. One resolver, vendorFacingRef(), consumed by all five existing chokepoints; drift-guarded; returns "no reference yet" gracefully.
  3. A read-only, PMS-agnostic PurchaseOrder read model (AppFolio proven; 15-min sync; 60-min discovery / 24-h render staleness budgets).
  4. PropFlow owns the PO↔job binding — heuristic proposal (vendor+unit+date; 0.8% collision rate at a 7-day window), PM-confirmed before anything vendor-facing renders it; no auto-confirm in v1 (~1 card/workday).
  5. PropFlow never writes or mints a PO. Ever. ADR-0030's principle extended; enforced by drift guards and both-direction evals (a WO property never emits a PO; a PO property never emits a WO number; no PO → never an invented one).

Shipped — the ship log as of 2026-07-28

PRWhat landedState
#4720The ADR itself — vendorJobReferenceMode, one resolver, read-only PO model, PropFlow never mints a POmerged
#4733Phase 1PurchaseOrder read model + AppFolio adapter, vendorJobReferenceMode with save-time validation, 15-minute sync + watermark, read-only drift guardmerged
#4740Phase 2 — PO↔job binder + PM confirm lane. Measured on the hand-labeled Camellia book: high-tier precision 1.00 (4/4), floor 0.98; coverage 0.93 (14/15), floor 0.85 — test output, not a doc claimmerged
#4745Phase 3vendorFacingRef() wired at five chokepoints, voice behaviors, UI touchpointsmerged
#4754 / #4755Regression fix — the job-reference section was reading the {{var}} name instead of the substituted value. Caught by the subscription eval, zero production exposuremerged
#4760The inbound lane was dark: triage never referenced the PO variables, and ElevenLabs drops variables no prompt references. Found by real robo-calls, not by reading codemerged
#4759Live inbound robo-vendor call battery — real calls into Clara, 11 deterministic assertionsopen
#4773The PM confirmation (§3) recorded in the ADR as a primary source, plus the proposed outbound posture belowheld for Fede
Live pilot: The Willows, the sandbox bench property, was flipped to purchase_order on 2026-07-28 with seeded real-numbered POs (#4742). Camellia is untouched and the mode is absent everywhere else, so production behavior is unchanged — flipping Camellia remains Fede's decision.

Phases

PhaseScopeStatus / est.
0 — Guardrails & fixes (land before any PO variable exists)Reply-attribution PO/WO collision + unification (#4710/#4716/#4718/#4721) · internal wo_ id leak fix (#4711) · company-name vendor recognition (#4709) · no-minted-PO drift guard + stripped corpus fixture + grader widening · PO capture in quote parsingmerged / 2 PRs in flight
1 — Read modelTypes, capability, AppFolio adapter, sync + watermark, vendorJobReferenceMode + save-time validation, read-only drift guard. Zero behavior change until Camellia flips.merged — #4733
2 — Binder + confirm queueScoring, tiers, PM confirm/reject/supersede, corpus test with pinned floors (precision ≥ 0.98, coverage ≥ 0.85 — as test output, not doc claim), stickiness testsmerged — #4740
3 — Surfaces + flip CamelliavendorFacingRef() wiring: voice dynamic var + eval flip, dispatch email (drop the WO precondition), SMS/labels, ops lookup_purchase_order, the four UI touchpoints, both-direction evals. Then flip Camellia's mode and supervise the confirm queue ~half a day before any vendor-facing surface reads a PO.merged — #4745, #4760. Camellia not flipped; The Willows is the pilot instead
4 — Successor lanes (each its own decision)(a) Turnover completion-signal redesign + optional WO-minting + whole-property work items + blinds trade — un-wedges the 24 stuck turns; (b) Vendor spend: PO↔invoice association — AppFolio bills read model + validated invoice-PDF extraction (Trello card); (c) Yardi/RealPage PO adapters when a property needs them.to scope

The outbound lane — proposed, not decided

All three trace to the 2026-07-28 PM exchange in §3, and all three are Fede's call on review of #4773 — nothing here has been built.

ProposalWhyDependency
Lead the call with the PO. An outbound scheduling call at a purchase_order property opens with the number and the job — "calling about PO 90760, the tub resurface at unit 101, scheduling the work"The PO is how the vendor confirms the job, and what their back office keys on — 17/21 sampled invoice PDFs carry it back. A PO mentioned only at the end reproduces the chase-at-invoice-time failure the corpus documentsNone — the resolver already supplies it
Gate the dial. No PO on file → don't dial; prompt the PM to mint one in AppFolio firstDialing without a PO is off-process per the PM ("they need a po before we can do anything"). Inbound stays the opposite: a vendor calling about a PO-less job is normal and "no PO yet" is an honest answerNone — reads the existing PO read model
Stop minting AppFolio work orders for turnover vendor work at purchase_order properties. The internal work item stays the tracking spine; completion comes from vendor reply or PM confirmation; the PM's PO is the only AppFolio artifactThose minted WOs are exactly the artifact jpco ignores, and waiting on their status is the 24/25 stuck-turnover wedge (§5). Minting is still unconditional — the dispatch path has no mode awareness; #4745 made only external dispatch mode-awareHard — cost attribution keys on minted WOs, so spend attribution must move to the PO lane (phase 4b) before minting is switched off, or spend goes dark

9. Risks & open decisions

ItemWhy it mattersOwner
Disclosure bar for self-claimed callers ("calling about 4471" with no verification — how much job detail may Clara disclose?)Trust/privacy call; the PO number is the highest-blast-radius artifact we holdFede
Binder date-curve calibration vs the 2025 regimeThe 13-day peak is the historical median, but 2025 ran at 44 days; the corpus test with pinned floors is the honest arbiter — recalibrate there, not in proseeng (phase 2)
"No PO yet" is the majority state for modern turns (coverage 26%)Sets expectations: the binder can't bind what was never issued; empty states and PM comms must treat absence as normaldesign (phase 3)
~12% of vendor-printed POs are wrongAny PO extracted from paper validates against the read model before it influences anythingeng (phase 4b)
Mode auto-suggestion thresholds; Yardi GetPurchaseOrders unvalidatedOnboarding polish; second-PMS reacheng (later)
Technical appendix — condensed spec (full detail: the ADR on main)

Data model

PurchaseOrder (derived read model — rebuildable by blind put, nothing PropFlow-authored beyond resolved* match-backs)
  PK PROP#<propertyId>  SK PO#<externalPurchaseOrderId>
  GSI1 VENDORPO#<vendorCompanyId>         → open POs per vendor (call prep)
  GSI2 PONUM#<orgId>#<purchaseOrderNumber> → org-scoped number lookup (returns ARRAY — ambiguity is the caller's problem)
  purchaseOrderNumber is a vendor-facing STRING: never parsed, keyed, or sorted; never minted.

PurchaseOrderJobBinding (canonical, PropFlow-authored)
  PK PROP#<propertyId>  SK POBIND#<poId>#<workOrderId | 'PROPERTY'>   GSI1 POBINDWO#<workOrderId>
  status proposed|confirmed|rejected|superseded · tier high|medium (low writes no row) · signals[] · bindingGroup
  Only CONFIRMED drives vendor-facing renders. Rejection sticky at unchanged hash; changed hash on confirmed → needsReconfirm (stays confirmed).

Sync & staleness

rate(15 min) lambda job · rolling 18-month window · contentHash diff (steady-state zero writes)
budgets: discovery 60 min (watermark-alerted, never silent) · vendor-facing render 24 h cache-only (past budget → suppress number, describe job)
       · reply attribution 24 h cache-only (miss → NO attribution, never a guess) · binder same-tick
failure posture: zero-row guard (never mass-remove) · vanished → removedInPms, never delete · 4 consecutive failures → alert

Binder scoring

candidates: wo.createdAt ∈ [po.createdAt − 45d, +7d]   (asymmetric — PO lags job)
score: vendor exact +50 / no-vendor +20 (different vendor → dropped) · unit +30 · date 0–15 peaked 13d
       · trade +15 · instructions token-match 0–15 · competing-PO −40
tiers: high ≥80 + margin ≥25 + unique → one-click confirm · medium 55–79 → top-3 picker · low → Unbound (manual)
floors pinned in po-binder-corpus.test.ts: high-tier precision ≥ 0.98, coverage ≥ 0.85
       measured on the hand-labeled Camellia book: precision 1.00 (4/4) · coverage 0.93 (14/15)

Guard rails

po-read-only.drift.test.ts — no PO write path exists, anywhere, ever
vendorFacingRef guard — no template reads purchaseOrderNumber directly
evals pinned both directions — work_order property never emits a PO; purchase_order property never emits a WO number;
                               no PO → "I don't have a PO number for that yet", never invented
Clara: PO arrives as call-start dynamic variable; NO new voice tool (mid-call lookup = the corpus's worst failure mode)

Prepared by Fable, 2026-07-27; rev 3, 2026-07-28 — ADR merged, phases 1–3 shipped, PM confirmation added (§3), outbound lane proposed (§8). Canonical decision text: docs/adr/0116-vendor-facing-job-reference-per-property.md on main, merged in PR #4720; the PM-confirmation follow-up is PR #4773, held for Fede's review. Evidence files on the Mac mini under ~/agents/006/po-training-data/.

PropFlow Docs