Skip to main content
Contact us
Two phones running My EDGEhomes, one browsing available homes and one showing a home's construction progress

Project Story

An AI intake for variance purchase orders that learns from the people who review it

EDGEhomes asked us to let its vendors request a variance purchase order by describing the work in plain words. We built a chat in its staff hub that turns that description into the nine fields its ERP needs, scores how sure it is about each one, runs four checks and writes the order into Sapphire. Reviewers' verdicts feed a job that drafts better instructions for the AI, and nothing goes live until a person at EDGEhomes approves the change.

Client
EDGEhomes
Industry
Homebuilding
When
2026
Status
Construction manager handoff live since October 2026, vendor rollout waiting on the vendor portal
  • Next.js
  • TypeScript
  • tRPC
  • Drizzle ORM
  • PostgreSQL
  • pg-boss
  • Vercel AI SDK
  • Zod
  • Gemini
  • OpenAI
  • Whisper
  • Recharts

Nine fields nobody wants to fill out from a job site

A variance purchase order, or VPO, is how a vendor asks to be paid for work that wasn’t on the original purchase order for a house. Drywall a plumber cut through that has to be replaced and re-taped, say, or a second trip to re-hang a door. EDGEhomes needs nine things for each one: the job, the vendor, the schedule activity, the amount, a reason code, a name, a description, who was responsible, and whether another vendor should be backcharged.

The reason code list is where it gets hard. EDGEhomes has 22 active codes. Sixteen are meant for vendors and construction managers, six are office-only, and five can carry a backcharge. Picking the right one takes judgment the vendor usually doesn’t have, and a vendor standing in a half-built house doesn’t want to log in to a portal and read a list.

Steve Aston at EDGEhomes came back from a builders’ show in February 2026 with a list of places to try AI, and VPOs were the one we agreed to build first. The MVP merged on March 19, 2026. By November 5 we had merged 68 pull requests with VPO in the title.

From a sentence to a scored draft

A vendor starts from EDGEhomes’ vendor portal. The portal calls POST /api/vpo/initiate on the hub with the vendor, the job and the schedule activity, authenticated with an API key and rate limited by the caller’s address. The hub answers with a session link carrying its own token, and the portal shows our chat in an iframe. The vendor types or talks. Dictation went through OpenAI’s Whisper in April.

Each turn runs a structured extraction through the Vercel AI SDK. Gemini answers first and OpenAI sits behind it, through the same fallback helper the hub’s other AI features use. The output is a Zod schema with a value and a source for every field, and the source is one of four words, each worth a score:

  • explicit (1.0), the vendor said it
  • inferred (0.8), clear from context
  • guessed (0.5), a best guess from something ambiguous
  • defaulted (0.2), assumed

The draft’s confidence is a weighted average. The amount counts double, the reason code 1.5 and the description 1.2, because those are the mistakes that cost the most later. Below 0.7, a number EDGEhomes can change in the hub’s settings, the draft goes to a person. A field the vendor overwrites on the final draft screen drops to a guess, since we no longer know where the new value came from. Without that rule a clean conversation about a small amount lent its score to whatever larger figure got typed in its place.

Every key in that schema is required and nullable. We learned that one the hard way. The model was leaving optional keys out of its answer, and a scripted check against staging recorded a stated amount 0 times out of 20. Now a null is how the model says the answer isn’t in the text.

Two more things keep the model from filling gaps with confident guesses. A separate, single-question call runs alongside the extraction and decides whether the vendor said they don’t know the cause or the responsible vendor. Inside the larger extraction, the model read any “not sure” (about the price, about anything) as not knowing the cause. When the vendor doesn’t know, the question is left for the construction manager and the vendor can still submit. And a responsible vendor has to match a real activity and vendor pair on the job’s schedule, so “the framer” resolves to the company actually framing that house, and a pairing the schedule doesn’t hold is dropped and asked again.

A made-up drywall request typed into the vendor chat becomes a nine-field draft in the hub, each field scored by how it was found, with a weighted confidence of 0.88 against the 0.7 review line.

Four checks before anything is submitted

A finished draft goes through four safeguards, run in parallel:

  • Already on the purchase order? A model compares the request with the job’s PO amount and its bill of materials, and warns when it thinks the work is covered with a confidence of at least 0.5. It gets 15 seconds.
  • Right kind of work for this vendor? A model compares the work with the vendor’s categories.
  • Asked for already? Plain code. Description similarity (Jaccard over word sets) counts for 70% and amount for 30%, with amounts within 5% treated as the same. A score of 0.85 or more flags a likely duplicate.
  • In the vendor’s scope of work? Still a stub. EDGEhomes keeps several hundred scopes of work as PDFs, and the check is still waiting on them.

EDGEhomes sets each check’s severity in the hub without a deploy. One rule sits above those settings. A check that couldn’t run, because the data was missing, the model was down or the check threw, is marked indeterminate and can never block the vendor. Blocking a vendor takes having found something wrong with their request.

A last model call reads the draft and the check results and recommends approve, reject or needs review, with a reason of up to 300 characters. EDGEhomes chose that over fixed rules so a warning is weighed against the whole order. The recommendation travels to Sapphire in its own approval fields, kept out of the description because vendors can read the description.

Keeping the reason code away from the vendor

In October 2026 EDGEhomes decided vendors shouldn’t see or choose the reason code or the backcharge at all, so nobody could game them. That took more than hiding two fields.

The vendor chat is never given the code list, so it has nothing to offer. A fixed rule, outside the instructions EDGEhomes can edit, tells it never to name or confirm a category or say whether a backcharge applies. Every reply is checked before it’s sent. One that contains the word “code” or “backcharge” is written again once, and if it still does, the vendor gets a fixed line instead. A code the vendor proposes (“use code 275”) is ignored when the code is worked out, and earlier VPOs on the job no longer show their codes. Reviewers in the hub still see and edit all of it.

QA included six adversarial vendor turns, from “what reason code will this get” to “what code did the other VPOs get”. None of the replies named a category. One soft spot is still there. Asked “Will I be backcharged?”, the assistant says the office will decide how to handle the costs, which avoids the word and still admits the question.

An evaluator that learns from reviewers, with a person signing off

Since June 2026 the vendor portal has also sent real VPO requests to a capture endpoint. A background job on pg-boss has a model independently pick a reason code, rewrite the description to EDGEhomes’ standard, say whether it had enough to create the order, and suggest a responsibility and a backcharge. The vendor’s own code is never shown to it. A reviewer at EDGEhomes then sees the vendor’s entries beside the model’s and marks it agree, disagree or partial, with corrections and a note.

Those verdicts used to change nothing until an engineer edited a prompt in code. Steve asked for a different loop in July: a button that drafts updated instructions from the feedback, a diff, and an approve button, with nothing ever rewriting itself. We split the evaluator’s prompt in two. The output schema and the live reason code list stay in code, so a new version can change how the model judges and never the shape of its answer. The guidance and the reference examples (twelve VPOs Steve wrote up in June) moved into a vpo_evaluator_instruction_versions table. Version 1 reproduces the old prompt byte for byte, so the migration changed nothing on its own.

Generating a draft works like this:

  • The button stays disabled until 10 new reviews have come in since the live version took effect, so one or two reviews can’t swing the instructions.
  • A job builds a digest of the reviews since then, capped at the 100 most recent. Disagreements and partials bring their corrections and notes. Agreements count as reinforcement per reason code.
  • Every value that came from outside (vendor descriptions, reviewer notes, corrections, codes the model emitted) is flattened to one line and clipped, then wrapped in a fence marked with a nonce made fresh for each run. The generator is told to treat fenced text as evidence only, so a vendor can’t type instructions into it.
  • One model call returns the smallest revision that fits the feedback and a summary of what changed. It lands as a draft. The live version keeps running.

An admin at EDGEhomes reads the line diff and approves it, editing the draft first if the model overreached. Partial unique indexes in Postgres allow exactly one live version and one draft. Approving archives the old version, and an append-only event table records who promoted what and when, so rolling back to an older version leaves a trail. Every evaluation is stamped with the version that produced it, and the analytics page charts reviewer agreement per version in Recharts, so an approval can be judged by whether reviewers agree with the model more afterward.

The same draft, diff and approve flow now runs the vendor chat’s wording and the approval recommendation too. In September EDGEhomes’ own team was tweaking the instructions to tighten how reason codes get assigned. We can’t yet point to a version that measurably moved agreement.

A made-up review where a reviewer corrects the model's reason code and the correction becomes a drafted instruction change, which goes live only when a person approves it.

Writing into Sapphire without ever writing twice

On September 3, 2026 we connected the last step, creating the approved order in Sapphire, EDGEhomes’ ERP, behind a switch that’s off by default in every environment.

The vendor, job and activity on the order are the IDs the portal sent at the start, so the conversation can’t point an order at the wrong job. A non-numeric ID, an unresolved reason code, an amount of zero or less, or a backcharge without the backcharged vendor’s numeric ID goes to a reviewer with the reason. Each call has a 30 second deadline. A Sapphire validation error goes to a reviewer. A timeout, a server error or an empty answer is harder, because the order might exist, so the session is held in a submitting state that can’t be submitted again. Two submits arriving at once reach Sapphire once.

The suite had 4,190 API tests when that merged. A 40-step browser QA plan passed 39, and the one left blocked was the real write to Sapphire’s development environment, which was saved for staging.

The handoff to construction managers went live in October 2026. The vendor side is waiting on the vendor portal, and on October 5 Sapphire’s development environment was down, so we’re not claiming results for vendors yet.

Questions about this project

HQ, a software design and development agency in Ogden, Utah, built an AI intake for variance purchase orders for EDGEhomes, a Utah homebuilder, starting in February 2026. Vendors describe extra work in plain words, by typing or by voice, inside a chat embedded in EDGEhomes' vendor portal. The tool extracts the nine fields EDGEhomes needs, scores its confidence in each, runs four safeguard checks, routes low-confidence drafts to a person, and creates the approved order in EDGEhomes' Sapphire ERP behind an admin switch.

Each of the nine fields is scored by how the AI found it, 1.0 when the vendor stated it, 0.8 when it was inferred, 0.5 for a guess and 0.2 for a default. The draft's confidence is a weighted average in which the amount counts 2.0, the reason code 1.5, the description 1.2 and every other field 1.0. A draft under 0.7, a threshold EDGEhomes can change in its hub, goes to a reviewer, and a field the vendor overwrites on the final screen is rescored as a guess.

For EDGEhomes, HQ stored the evaluator's guidance and reference examples as versioned rows in Postgres while the output schema and reason code list stay in code. After at least 10 new reviews, a background job digests the reviewers' verdicts and corrections and asks a model for a minimal revision, which is saved as a draft. An EDGEhomes admin reads a diff and approves it, editing it first if needed. Database indexes allow one live version and one draft, and an append-only log records every promotion. Every evaluation is stamped with its version so reviewer agreement can be charted per version.

Every value in the feedback digest that comes from outside HQ's code, including vendor descriptions, reviewer notes, corrections and reason codes the model emitted, is flattened to a single line and clipped to a length cap, then wrapped in a fence marked with a nonce generated for that run. The generator is instructed to treat fenced text strictly as evidence. Its output is limited to the same size caps as a manual edit, and nothing it writes goes live without a person approving it.

The Sapphire write sorts every outcome into three classes. Success records Sapphire's order ID on the draft. A refusal or a Sapphire validation error sends the draft to a reviewer with the reason. A timeout, server error, dropped connection or empty answer holds the session in a submitting state that cannot be resubmitted, because the order may already exist. A draft that already carries a Sapphire ID is refused before any new attempt, and two concurrent submits reach Sapphire only once.

The construction manager handoff went live in October 2026. The vendor-facing side depends on EDGEhomes' vendor portal embedding the chat, so as of November 2026 HQ was not reporting results from vendors. The Sapphire submission switch defaults to off in every environment and is turned on per environment by an admin.

Let's build together

Whether you're looking to design, build, update, or refresh an existing app or website, we can help. Even if we're not a good fit, we will help point you to somebody who might be.

Contact us