Vendor coordination · Investigation · 2026-07-27

Camellia runs vendors on purchase orders.
We built everything on work orders.

Every PropFlow vendor surface — voice, email, SMS, reply-routing — presents a work-order ID as the job reference. Mining every jpco purchase order, 105 days of call transcripts, and the full codebase shows the actual vendor-facing reference at Camellia is an AppFolio PO number, issued by the PM, required at scheduling and invoicing. This is the evidence, the blast radius, and the proposed pivot.

Sources: AppFolio Reports API v1 (purchase_order, unit_turn_detail, unit_inspection) · PropFlow prod conversations + ElevenLabs transcripts (Feb–Jul 2026) · repo audit @ main 7ecedc1. Corpus: ~/agents/006/po-training-data/. No numbers in this page are estimated — every figure traces to one of those sources.
722
distinct POs mined (2022–2026), 919 line items — 696 are Camellia
0 / 722
POs using AppFolio's PO↔work-order link. jpco never connects them
9 / 9
PO phone calls transferred to a human. Clara has never answered one
0
vendor calls ever citing a work-order ID. "Work order" is only Clara's word
~2 wks
median move-out → first unit-specific PO (window-sensitive 13–17d; 2025 ran ~3× slower)
0
lines of PO-aware code in PropFlow today (the only mention is a scripted refusal)

1 · What a PO actually is at Camellia

From the full corpus — 722 POs, verbatim from AppFolio.

The PO is a vendor + trade + batch authorization token, not a per-job ticket. It's a bare sequential integer (currently in the 600s), issued by the PM (92% by Erika Wieser since 2023), and — since the Sep 2023 cutover — carrying no dollars at all: every one of the 381 modern-era POs is $0.00, unapproved-or-approved-but-never-received, and has zero bills attached (all 137 bill-linked POs are legacy backfill). The modern PO is a pure requisition/reference token — "Erika wants a dishwasher from A&K" — and the money flows somewhere outside the PO record entirely. It's what the vendor needs to submit an invoice, which is why they chase it; but nothing in AppFolio closes the loop.

POs per year
Read the 2022 bar carefully: the 5-digit legacy series (90188–91048) was bulk-entered on four days in Oct 2022 (298 POs on 10/31 alone) covering work back to Jan 2021 — a backfill event, not ordering activity. On the real work axis, throughput has been flat ~10–11 POs/month across both eras. The sequential series 46→685 starts Sep 2023 (a clean 3-day baton pass from the old process) and is the one vendors live in.
2022 (legacy 9xxxx)340
202332
2024144
2025120
2026 (through Jul 27)86
Who gets POs — the turnover trade stack
Top vendors by PO count, all years. Service trades get per-unit or batch POs; ~40% of all POs are supply runs.
Home Depot (supplies)123
Miracle Method (tub/tile)80
Metropolitan Bldg (cleaning)78
A & K Appliance66
Bomar Painting55
HD Supply (supplies)49
Royal Textile (blinds)49
HD Painting41
MTech (plumbing)37
TradePO shapeExample (verbatim)
Cleaning / turnover laborWhole-property, by duration"Clean House 7 days" — Metropolitan, PO 682
Tub & tile refinishingPer-unit (78 of 80 unit-specific)"Resurface Tub" unit 308 — Miracle Method, PO 671
PaintingMulti-unit batches"Paint units/patch work" units 115, 120, 224 — HD Painting, PO 681
AppliancesPer-unit"New Dishwasher" unit 302 — A & K, PO 670
BlindsPer-unit"New Blinds" unit 308 — Royal Textile, PO 684
SuppliesProperty-level runs"Maintenance Supplies" — Home Depot, PO 685

Lifecycle is barely maintained — 618 Approved / 104 Pending; 71% never marked Received or Completed; the Instructions field holds the responsible person's first name ("Erika", "Joanna") and sometimes a check number. It's a paper process wearing software: on calls, the PM reads POs off a physical list. Since 2024, zero POs have bills recorded against them in AppFolio — invoice reconciliation happens outside the system too.

2 · The calls — vendors speak PO, Clara speaks work order

105 days of post-transfer transcript capture (Apr 13 – Jul 27), 58 vendor voice calls, all searched.

"Do you have that PO? I don't think I got a PO." → "Oh, yes. Let's see. It is six eighty-two."
Sylvia (Metropolitan Bldg Maintenance) ↔ Joanna, front desk — Jul 27, 2026. PO 682 = "Clean House 7 days", created that day. The call that surfaced this whole investigation.
"I was just getting ready to submit your guys' invoice here, and I realized I still was missing the PO." → "That is completely my fault, Anna… let me get you the PO."
Anna (Miracle Method) ↔ Joanna — May 15, 2026. PO 618 = "Resurface Tub" unit 207. The invoice is blocked without it.
"Did you want to provide a PO number for the service or do you like just…" → "Yes, please. I got one. It's, uh, 667."
Maddie (A & K Appliance) ↔ front desk — Jul 10, 2026. PO 667 = fridge service, unit 210.
16%
of vendor calls mention a PO (9 of 58) — every one handled by a human
71%
use no reference at all — unit + trade + date is the shared language
66%
of vendor calls get transferred to a human today

Every PO cited on a call exists in the mined corpus with matching vendor and job (613, 618, 644, 667, 682). The reverse is also clean: no vendor has ever cited a work-order ID in any recorded conversation. When Clara asks "what work order are you calling about?", vendors answer with a delivery, a unit, or a person's name. The mismatch is a measurable driver of the 66% transfer rate.

3 · POs in the turnover clock

231 unit turns (AppFolio unit_turn_detail) joined against the PO corpus.

~2 wks
median move-out → first unit-specific PO (13–17d by window)
18%
of POs pre-staged before the tenant leaves
26 d
median total turn duration (target: 10)

The PO lands mid-turn — issued around scheduling time, consistent with the calls. The exact medians are window-sensitive (13–17d overall depending on match window), but one year-effect is robust under every check: 2025 was a real operational slowdown — median turn duration tripled (15–17d → 46d; 44% of turns blew past 60 days against a 10-day target) on AppFolio's own turn records, independent of POs, with the same staff and PO volume. 2026 has recovered, though the figure is right-censored (25 of 38 turns still open). Why 2025 happened is a question for JP, not the data. Three honest caveats: 128 turns matched no unit-specific PO — partly attribution limits (multi-unit paint batches), partly because not all PO work is turnover work; the binder study found Metropolitan's day-count "Clean House" POs are a recurring building-janitorial contract (GL: Janitorial – Service Contract), not per-turnover cleans — they belong to a service period, never a turnover; and inspection lag is not computable — AppFolio's inspection report stores only each unit's latest inspection date, and our condition reports only reach back a few months. Third confirmation of the WO disconnect: AppFolio's own Unit Turn feature has a PurchaseOrdersFromWorkOrders column — null on all 238 rows.

4 · Blast radius — every surface that speaks "work order" to a vendor

Full-code audit. Class: what the vendor sees/says. Effort: S/M/L to carry a PO instead.

SurfaceTodayUnder the PO realityEffort
Outbound vendor voice (ADR-0111)Carries no job number by design; scripted to refuse the PO question (vendor-outbound.ts:208) and an eval fails it for stating any POAdd po_number dynamic var + prompt section; flip the eval to "answer with the supplied PO, never invent one". The single highest-value changeS–M
Vendor dispatch email"Work order" row renders displayId; a code comment already calls the AppFolio PO # "the ideal future state"One vendorFacingRef() resolver: PO when present, canonical WO format otherwiseM
Quote flowQR-<displayId> is the live reply key; invoice parser discards the PO printed on vendor invoicesKeep the QR key (quotes precede POs); capture poNumber in parsing — free binding evidenceS
SMS / Telegram dispatch"NEW WORK ORDER - CAM-604" headlinesReference swap in headlines (mind the SMS truncation cap)S–M
Inbound reply attributionA bare number in vendor text binds as a work-order match at the strongest tier, high confidenceLive bug: 8 of 13 sampled CAM-xxx WO numbers collide with real PO numbers. "PO 598, when can we get in?" binds to WO CAM-598 and can relay to the wrong tenant. Fix ships now, pivot or notS · ships first
Vendor chase emailFalls back to raw internal wo_… id in subject lines when displayId is missingLive bug: internal ID leak to vendor inboxes; also breaks its own reply threadingS · ships first
Inbound voice ("which job?")Phone-keyed session match — already reference-agnostic; triage phrase list only anticipates "work order X"Add "calling about a PO / purchase order" as a vendor routing phrase + eval row (real corpus utterance)S
Conversation labels / PM UIregardingLabel = displayId everywhereAppend, don't replace: "CAM-604 · PO 512"M
The structural finding: AppFolio can't tell us which WO a PO belongs to. PropFlow has to own that binding.
The PO report's WorkOrders field is null on all 919 rows, and no PO since 2024 has bills recorded against it — both join paths are empty. The good news: (vendor, unit, created-date) is 96%-unique across recent POs, so a heuristic binder with PM confirmation is viable. That binder is the one genuinely novel build; everything else is reference plumbing.

5 · The design principle — per-property reference, two modes, no framework

Camellia proves POs; another property may genuinely run on work orders — AppFolio supports both and operators differ. The pivot is therefore not "switch WO→PO". It's:

The vendor-facing job reference is a per-property concept, resolved by one function at the existing chokepoints (dispatch email, voice dynamic vars, SMS headline, reply matching, conversation label). Two modes, because two have been observed in the wild:

The over-engineering guard: the mode is data (a property-config field, inferable from observed PMS usage — jpco classifies itself: 722 POs, zero PO↔WO links), the flexibility lives in config, and no third mode is built until a third pattern is observed. Yardi has PO APIs too (GetPurchaseOrders), so the read model is PMS-agnostic in naming from day one, per the existing adapter rules.

Dependency-ordered backlog (from the code audit)

TierWhatNature
0 — nowFix the bare-number attribution collision · stop the wo_ ID leak in chase emails · route all vendor handlers through the one attribution function · capture PO numbers from parsed invoices/quotesBug fixes + free evidence, independent of the pivot
1 — read modelPMS-agnostic listPurchaseOrders capability + DDB read-model rows + sync job + ADR-0116 (holds for Fede's review; PR #4720)Additive, no live behavior change
2 — binderPO↔job binding on (vendor, unit, date) with PM confirmation, modeled 1:NThe novel piece — L effort
3 — displayPO in review drawer, PM notices, vendor detail, conversation labels, dispatch surfacesAdditive, cheap, gated on 1–2
4 — payoffClara answers the PO question on calls; PO history feeds preferred-vendor rankingBehavior change, gated on a disclosure decision (below)

Decisions that are Fede's, not code's

  1. Disclosure bar: may Clara read a PO number to a caller matched only by self-claimed company name (PR #4375's trust tiers), or does a PO require a phone-verified vendor? The PO is quasi-financial — current guidance forbids payment details on name-only matches.
  2. Issuance scope: read-and-answer only (Clara relays existing POs), or eventually mint POs? Minting requires the unbuilt AppFolio write path (browser L4) and touches an approval workflow — recommend explicitly out of scope for the pivot's first phase.
  3. Mode inference: auto-classify each property's reference mode from observed PMS data, or set it manually at onboarding? (Recommend: manual field, auto-suggested from data.)
  4. The 2025 lag anomaly: median PO lag was 33d in 2025 vs 7–8d in 2024/2026 — worth asking JP what changed before we encode assumptions about their cadence.

6 · The turnover pipeline, stage by stage

Full inspection of the turnover flow vs the PO-per-trade reality, verified against prod.

We are already minting AppFolio work orders that no human uses — and it wedges our own workflow.
The one Camellia turnover that ever reached work_scheduled (unit 116) minted two real AppFolio WOs on Jun 29 — CAM-1259 (paint) and CAM-1260 (cleaning). Both are still open a month later: jpco staff run on POs and never touch AppFolio WOs. Because turnover completion is driven by AppFolio WO status (polled every 60s), the completion signal can never arrive — the cleaning-last hold never releases and the turnover is permanently stuck. The other 24 of 25 Camellia turnovers sit at notice_received with zero child WOs.
StageCurrent behaviorCamellia PO realityEffort
Scope vocabulary16 task types — no blinds, no whole-property work itemRoyal Textile blinds = 4th-largest trade (49 POs); Metropolitan "Clean House N days" = whole-property, inexpressible per-unitS / M
Work-item cardinalityOne WO per scope line per unit, parallel fan-out, no batching primitiveOne PO per vendor per batch — 59 POs span multiple units; PO 82 mixes per-unit + whole-property lines on one POL
Vendor referenceDispatch email says "please reference CAM-1259… on your invoice"; the AppFolio WO id is a hard precondition for emailing a vendor at allVendors invoice against the PO; the WO number means nothing to themS–M
Completion signalAppFolio WO status is authoritative (ADR-0030), polled every 60sjpco never closes AppFolio WOs (or marks POs Received) — signal must come from vendor reply / PM confirm insteadM
Actual costReads the selected vendor quoteModern POs are $0; the true dollar lands on the bill — no bill ingestion exists anywhere in the codebaseM
GL codingNone on the WOEvery PO line carries a GL; the vendor→GL map is ~95% deterministic (Miracle Method→Plumbing Repairs, HD Painting→Painting, Metropolitan→Janitorial Contract…)S

Minimal first slice (no write path): sync the purchase_order report into a read-model → auto-link PO lines to open turnover jobs on (vendor, unit, GL-trade, ±14d) with a manual override → show a PO chip on the turnover detail and return it from Clara's job-list tool so she can answer "what's the PO?" on a call → swap the dispatch-email reference to the PO when present. Erika keeps creating POs by hand; PropFlow just stops being blind to them. What genuinely needs the (deferred) browser write path: minting PO numbers and true batch POs.

7 · The test harness — what flips, what holds

Full inventory of every vendor/turnover harness, eval, and drift guard.

The structural irony: Clara currently has no sayable job identifier at all on vendor calls. The eval jargon-ban forbids her from saying "work order" to a vendor; the PO doesn't exist in the domain model. Meanwhile the shipped prompt scripts a punt for the exact question ("Do you have a PO number?" → "you don't carry one… flag it to the PM"), and the graders pass the punt: transferring a PO question to a human is currently rated GOOD. The pivot flips nine sites (prompt clause, two battery rules, three eval assertions, the dynamic-var key set, the judge's source-of-truth, reply attribution) and re-rates two corpus rows from GOOD to INSUFFICIENT — while every fabrication guard stays: Clara must never invent a PO; she must retrieve the real one, and the guard moves from regex-matching to provenance-checking.

The mis-binding bug is proven, not theoretical. A live probe against the real attribution code: "Okay, and I have the PO 644" → binds work order CAM-644 at high confidence; "Got it, the PO is 618" → binds unit 618's job at high confidence (PO 618 is actually unit 207's tub refinish). 59 integers are simultaneously a real Camellia unit number and a real PO number. An existing unit test encodes the bare-number binding as intended behavior, and four prior wrong-ticket incidents came from this exact shape on the outbound side. The fix + regression suite is ~half a day and is shipping now, independent of the pivot.

Sequencing rule worth keeping: the "PropFlow never mints a PO number" drift guard lands before the po_number variable exists — otherwise the cheapest way to green a red test is to synthesize a plausible integer, exactly the fabrication the guards exist to prevent. Rebuild plan: ~11 engineer-days across 10 ordered steps; scenario personas come from the three real PO vendors (Sylvia/Metropolitan, Anna/Miracle Method, Maddie/A&K) plus cross-cutting ambiguity cases (the 618 unit-vs-PO collision, ASR manglings like "$6.18" = PO 618). The honest baseline: the vendor-recognition replay across 64 real calls measured identified 3% · greeted 0% · bound 0% — recognition is arguably a bigger gap than the reference system, and both are needed.

8 · What the fleet measured

Deep-dive results that refine the design. Full studies in ~/agents/006/po-training-data/.

The binder — measured, not guessed

On the modern era (2024+, the only slice with real creation dates — the 2022 series turned out to be a bulk backfill entered on four days in Oct 2022): auto-binding on (vendor, unit, ±7 days) has zero collisions across 252 unit-claims; the turnover anchor (unit + move-out window) is 99%+ unambiguous when a candidate exists. Widening to ±30d breaks real cases (same vendor, same unit, dishwasher then stove two weeks apart). The AppFolio work-order report is a dead end as a join target — 374 of 389 Camellia WOs carry no vendor and 96% are resident requests; ceiling ~5% coverage. After auto-suppressing supply runs with no unit and the Metropolitan janitorial-contract POs, the genuine PM-confirmation residue is roughly 1–2 POs per month. Honest limit: there is no ground-truth label anywhere (every PO↔WO field in AppFolio is null), so these are ambiguity measurements, not accuracy against labels.

The human workflow (the "Joanna spec")

Erika creates 99% of modern POs in AppFolio; Joanna reads them to vendors off a paper list; the Instructions field records the requester (Joanna 176 / Erika 173). Two rules the design must honor: the PO never precedes the booking (twice it didn't exist yet at booking time), and it gates the invoice, not the work — vendors start jobs without it, which is exactly why it silently rots until an invoice is blocked (PO 618 sat 9 days until the vendor chased it). The office has never once volunteered a PO unprompted in any recorded call — so proactive delivery of a fresh PO to the waiting vendor would beat human parity, not just match it. Twelve parity behaviors were specified, dependency-tagged: five buildable today (intent recognition, digit-by-digit read-back — ASR already renders PO 618 as "$6.18" — tracked promises, scheduling provenance, post-transfer silence), four blocked on the read-model, three on issuance. One corpus correction: the 05/06 call rated "fabricated unit 207 + PO promise" is partially corroborated by AppFolio (PO 618, unit 207, created that day) — re-examine before it trains anything.

Vendor roster — the gap that blocks everything vendor-facing

All 32 PO vendors resolve cleanly to prod VendorCompany rows (zero name drift — never build name matching). But 26 of 32, covering 77% of PO volume, have no phone or email on file — including Metropolitan, A&K, Home Depot, Royal Textile, Bomar. Worse: the real caller-ids of Miracle Method and A&K exist in prod as orphan Persons with no vendor membership — two disconnected halves of the same vendor. Six of Camellia's 13 preferred-vendor trades are unconfirmed email_mining entries that resolve to empty — despite the mined vendor being right 9 times out of 10 against revealed PO preference. One PM confirmation pass is the single highest-leverage unblock. Also: GL defaults must key on vendor, not trade (Miracle Method is refinishing but books 90% to Plumbing Repairs), and the trade vocabulary lacks glass, signage, doors, and any way to say "supply house, don't dispatch" — which is 31% of POs.

Cross-PMS reality check

The per-property mode is a capability, not a preference: AppFolio has POs read-only (Plus/Max API tier only); Yardi has read and write but behind a separately-licensed module (~$25K/yr) and with no PO↔WO link at all (the join only materializes on bill lines after invoicing); RealPage's AppPartner surface and Buildium have no PO API whatsoever. Consequences already reflected in the design: the mode selector needs a per-module capability probe; PropFlow must own the PO↔job binding on every PMS (it's optional-and-unused on AppFolio, impossible on Yardi); purchaseOrderNumber is a mutable, human-facing string (AppFolio allows manual renumbering; Yardi has three number fields) and storage must key on externalPurchaseOrderId. And resolution on a live call needs a PO-indexed cache, not an on-demand report scan.

The 2023 cutover, explained by the data

There was no 2023 volume dip — throughput has been flat ~10–11 POs/month since 2021. What changed in a clean 3-day handoff in Sep 2023: the PO stopped being a financial document (legacy: 100% approved, priced, 62% received, 40% billed) and became a live requisition log (modern: $0, never received, never billed, 27% still Pending approval). Ten questions the data can't answer are queued for JP — the top one: is the modern $0/no-bill pattern deliberate ("PO = notification token") or process drift? The answer decides whether PropFlow should fill those fields or ignore them.

9 · The week — end-to-end to ON

Not just phases: a Mon→Fri plan that finishes with Clara answering PO questions on real Camellia calls. Effort figures come from the code audits; the fleet's Tier-0 PRs are already in flight, so the week starts from a fixed foundation.

DayBuildExit criteria (verified, not assumed)
MonPO read-model: listPurchaseOrders capability + DDB entity + sync job (piggybacks the 60s appfolio-sync lambda) + the reference-mode property field. ADR review with Fede (30 min: disclosure bar, mode-field home, staleness budget) — unblocks everything downstream.All 5 transcript POs (613/618/644/667/682) resolve from prod via the new read path; sync survives a full Camellia backfill; ADR status agreed.
TuePO↔job binder (vendor+unit+GL-trade+±14d, PM-confirm residue in the review queue) + turnover detail PO chip + blinds trade + vendor→GL default table.Binder precision on the historical corpus ≥ the measured baseline; unit-116's real POs bind to the right jobs; PM can confirm/override in the UI.
WedClara surfaces: po_number dynamic var + vendor-outbound prompt flip (answer, never invent) + inbound "calling about a PO" triage phrase + ElevenLabs sync + dispatch-email reference swap (PO when present).Eval suite green with flipped assertions; bench call: robo-vendor asks "do you have the PO?" and Clara answers the real number with provenance in the trace.
ThuHarness rebuild: three real personas + ambiguity/ASR scenarios + golden-dataset regression set (~15 core rows) + provenance-based fabrication guard. Camellia completion-signal decouple (vendor-reply/PM-confirm drives workOrderCompleted) so the unit-116 wedge class dies.Two consecutive clean battery sweeps; the 618 unit-vs-PO collision scenario passes; unit-116 turnover unwedges in prod.
FriProof loop + arm: prod E2E (real inbound test call from a fresh number asking for a PO), Sentry sweep on the window, prod QA screenshots, then flip Property.vendorReferenceMode = purchase_order for Camellia and monitor live calls.Two clean E2E sweeps; Sentry window clean; the next real Sylvia/Anna/Maddie call gets a correct PO answer or a correct "not issued yet — I'll flag it" — measured from the live transcript.

Deliberately NOT in the week: PO minting (needs the unbuilt browser write path — Clara relays existing POs only), batch-PO turnover restructuring (the L), and AppFolio bill ingestion (empty for the current era). The scope is: PropFlow stops being blind to POs, and Clara reaches parity with Joanna's read-and-relay job.

10 · Workstreams in flight

A 19-agent fleet is extending this document. Landing here as they complete:

Corpus + intermediates: ~/agents/006/po-training-data/ (722 POs, transcripts, lag joins, golden dataset). Investigation date: 2026-07-27.

PropFlow Docs