Applied AI · field sales · built solo

A rep talks into their phone in a parking lot, and onboarding gets a record they can work from.

The notes a salesperson types after a shop visit are all the onboarding team gets, and the one question that decides the go-live date is usually the one nobody asked. This tool makes that question visible. Two minutes to read, the full write-up is one click away.

Field discovery is a handoff problem before it is an AI problem. A rep finishes a good conversation, writes four lines into the CRM that afternoon, and three weeks later an onboarding consultant opens the record and finds out that the one requirement that decides the go-live date was never asked about. This is a tool that closes that gap, and this page is the reasoning behind how it is built.

Everything below runs. The merchant data is synthetic, written to carry the pattern rather than borrowed from anyone, and the Salesforce writes were wired against a real Developer Edition org.

Read this as
The short version. About a two-minute read. The full write-up: architecture, the schema argument, evals, field design, and what broke.
The guided walkthrough Six scenes, from the parking lot to the onboarding consultant's screen. Start here if you have ten minutes. Open → The working app Eight seeded deals. Capture a visit, work the checklist, send the handoff. Try it → The eval results The golden set, the paired stability arms, and the deterministic layers. Read the numbers →

00 · In one picture

What the rep has time to type, next to what onboarding actually needs.

The same visit as a free-text field and as a coverage record.

Same visit, same hardware store. Left is what a rep has time to type. Right is what the tool makes from the rep talking for a minute in the car, and the rows at the bottom are the questions nobody asked, which a notes field can never show you.

Same transcript, same merchant. The left is the CRM's free-text field, which accepts four lines and forty and cannot distinguish silence from a resolved requirement. The right is coverage against the vertical's card stack, with three confidence states and the never-discussed cards printed rather than omitted.

Today · the CRM notes field

Kestrel. Met Ted, the manager. On Epicor, wants better reporting, hates the card fees. Two lanes, wants a third. Follow up next week.

Every line here is true and the record looks complete. Nothing on it says the rental counter runs on a second system, or that four questions were never asked.

With the tool · what onboarding opens

Kestrel Hardware & Rental19 questions · hardware store with rentals
Equipment rental contracts“they rent equipment out the back, trailers and a scissor lift”
Current POS · Epicor Eagle“they’re on an epi core system”
~Decision makerMet someone who seemed in charge, never confirmed. Stays a guess until a person checks, and it is labelled as one everywhere it travels.
!Restricted chemical licence checkNever came up
!BudgetNever came up
!TimelineNever came up

✓ the merchant said it, and the quote is attached · ~ the system thinks so but a person has to agree · ! the question was never asked

How it gets from one to the other, in four steps

The path from capture to CRM

1 · Talk
Kestrel · capture0:58

Sixty seconds, one hand, in the car

Talk, paste or type, after the conversation, never during it.

Voice, paste or typed text all enter at one point and take the same path. Transcription is bought, not built. Entries are append-only and current state is derived across all of them.

2 · Match
hardware_rental19 cards
incumbent_systemconfirmed
rental_contractsconfirmed
decision_makerinferred
chemical_licencenot_discussed
budgetnot_discussed

Checked against this kind of shop

The AI checks the words against a fixed list of questions for this kind of shop. It cannot add questions of its own.

One LLM call receives the vertical's card ids as a closed enum and returns a status per card. A local keyword pre-pass runs first and instantly, capped at inferred. The model's response is validated against the contract and rejected on failure.

3 · Confirm

Rental counter on a second system

confirm before onboarding

Decision maker · Ted?
Never came up: budget, timeline, chemical licence

The rep sees only the gaps

The phone shows only what is missing. The red card is the one that could sink the deal. One tap confirms a guess.

Progressive disclosure, confidence by weight. Viability at 27px on red, awaiting-confirm at 18px on near-black, never-discussed at 15px on stone. Inferred never auto-writes. Every open gap carries an owner.

4 · Hand off
Opportunity006Ax0001
Discovery_StatusOpen gaps
Viability_Flagrental_contracts
Golive_Shift+3 wks
Discovery_Open3 items
Chatterreceipt posted

Written to the record they already use

Plain fields on the Salesforce record onboarding already uses, with the merchant's words under every answer and the open questions under their own heading.

REST upsert on an external id, six plain custom fields, raw captures attached as ContentNotes, a Chatter receipt, and one review Task on first sync. No managed package, no Lightning component.

01 · The problem

The thing that delays a go-live is almost never something the rep forgot. It is something nobody knew to ask.

The gap is almost always vertical-specific, and it is almost never the thing a rep forgot to write down.

A rep writes down who decides, when they want to switch, what they can spend. What never gets written down is the requirement only this kind of shop has, because nobody knew to ask. A hardware store with a rental counter runs two systems. A grocery with a butcher counter prices by weight at the scale. A shop taking food-assistance benefits has a six-week certification ahead of it whether anyone mentioned it or not.

An empty field and a settled question look identical in a CRM, and the whole tool exists to refuse to let them.

A rep running a retail territory has a good discovery conversation and captures it honestly. They write down the decision maker, the timeline, the budget, the reason the merchant is looking. What they do not write down is the requirement that only this kind of merchant has, because it did not come up, and it did not come up because nobody asked. A hardware store with a rental counter is running two systems rather than one. A grocery with a butcher counter prices by weight at the scale. A specialty shop taking EBT is looking at a certification window that runs six to eight weeks whether or not anyone mentioned it.

None of that is a note-taking failure. It is a coverage failure, and the reason it survives is that the CRM has no opinion about what a complete discovery looks like for this merchant. A free-text field accepts four lines and accepts forty, and it accepts silence on the one card that matters.

Silence is the failure mode. An empty field and a resolved requirement look identical in a CRM, and the entire design follows from refusing to let them.

So the tool's job is narrow. It reads what the rep actually said, maps it onto a checklist that the vertical defines, and is explicit about the difference between something the merchant confirmed, something the system inferred, and something that never came up at all. The last category is the product.

02 · How it works

Two kinds of work, and the part that matters most never touches the AI.

Two tiers, and the one that matters most never calls a model.

A rep with one bar of signal needs an answer right now, so everything that has to be right runs on the phone with no network, and the AI does exactly one job.

The first decision was which parts of this are allowed to be probabilistic. A rep standing in a parking lot with one bar of signal needs an answer immediately, and a requirements checklist that only works when the network does is a checklist that gets skipped. So the work splits along a line drawn by what has to be right rather than by what is easy to build.

On the phone · instant, works offline

The checklist, the vendor matching, the record

  • “epi core” is understood as Epicor Eagle, a real point-of-sale system
  • Two answers that disagree are both kept, so a person picks
  • A keyword scan gives a first checklist in under a second, labelled as guesses

In the cloud · the AI's one job

Read the words, fill in the checklist

  • Waits in a queue if the phone is offline, sends itself when signal returns
  • Every answer is checked against a rulebook, and a bad one is thrown out and asked for again
  • It can never mark a question answered without quoting the merchant

Tier one · deterministic

Runs locally, offline, instantly

The requirements library, alias matching on incumbent systems, conflict detection, the pre-call brief, and the handoff artifact all live here, and none of them need a network, a model, or a wait.

  • Fuzzy vendor matching by edit distance, so epi core reaches Epicor Eagle
  • Conflict rules that surface two answers rather than overwriting one
  • A keyword pre-pass that can only ever produce inferred
  • The artifact that gets written to the CRM

Tier two · probabilistic

Reads the transcript, queues when offline

One LLM call through a broker, extracting requirement coverage from the raw capture. Its output is validated against the contract before anything touches state.

  • Queued in IndexedDB and replayed when signal returns
  • Every response validated, and rejected responses are retried
  • Supersedes the keyword pass, because confirmed outranks inferred
  • Can never write a status the contract does not allow

The list of questions is the real asset. Every kind of shop has its own set of cards in one file, and adding a new kind of shop means editing that file from an admin screen, no engineer needed.

The keyword pre-pass is the part I would defend hardest. It is deliberately weaker than the model and the ceiling is enforced in code rather than trusted: it can only ever produce inferred, and it can never produce not_needed. When it sees "we don't rent equipment anymore" it declines to flag the rental card, and it still refuses to rule the card out, because that call needs a reader and a keyword sitting near a negation word is not a merchant statement. A rep in a dead zone gets a real checklist in under a second, and every line on it is honestly labelled as a guess until the reader has run.

The requirements library is the asset

Everything keys off one JSON file. Each vertical carries its own cards, each card has a type, a blocking flag, a support status, the question a rep would actually ask, the keyword signals for the pre-pass, and a note explaining why it exists. Adding a vertical is editing that file, and the admin screen in the app edits it in place. Nothing about that path needs engineering, which matters because verticals change quarterly and anything that needs a developer to stay current goes stale inside one.

The screens

What the rep sees on the phone, and what onboarding sees at a desk.

Two surfaces, one store. The design effort went into the gap panel.

The rep gets a phone that shows only what is missing. The onboarding consultant gets a desk view with every answer, its quote, and the recording next to it, so they can check the tool's work.

Three screens matter: capture, the gap panel, and the handoff artifact. Confidence reads by weight rather than border style, because dashed borders were the first thing to vanish in sunlight. Required action sits in the bottom third, where a thumb can reach it on a large phone.

9:41synced

Kestrel Hardware & Rental

What’s missing

4h ago

Rental counter runs on a second system

“they run that on a point a rental thing so that’d need to tie in”

Two systems to tie together. Confirm this can be done before the deal moves.

integration · unverified · go-live +3 wks

Decision maker

“Teddy? Ted?” · authority unclear

Never came up: budget, timeline, restricted chemical licence, loyalty programme.

Three lanes and multi-register reporting are a strong fit. Say so.

Send handoffAsk next visit · 4
1

The loudest thing on the screen

The one thing that decides whether the deal should have closed, in the merchant's own words, with what it does to the date.

Viability, 27px on red, with the verbatim evidence and the expected go-live shift. The five-second test: show the panel, take it away, ask what mattered most. If the answer is not this card, the weight is wrong.

2

A guess that needs a person

The system thinks the manager decides but nobody confirmed it, so it stays a guess until the rep taps.

Inferred, 18px on near-black. Inferred never auto-writes. The one-tap confirm is the human step that the eval numbers below argue for, since the schema constrains vocabulary rather than judgment.

3

What never came up

One quiet sentence, carried over as the questions for the next visit.

Not-discussed, 15px on stone. Only gaps are shown, never the full checklist. These become the pre-call brief for the next visit, and a confirmed card is never re-asked.

4

Good news too

If the shop is a strong fit somewhere, the rep hears that too.

Platform strengths collapse into one green bar. Three full-size cards pushed the decisions off the screen.

ReviewAdmin
library v3 · 38 cards · 5 verticals
Kestrel Hardware & Rentalhardware_rental · 1 viability flag · 3 open
Sable & Rye Specialtyspecialty_food · sent 4d ago · 2 unmapped
Loma Verde Grocerygrocery · conflict: 3 vs 4 locations
Hollis Supply Co.hardware_rental · thin capture · keyword pass only
Ninth Street Marketgrocery · scale integration inferred

Kestrel Hardware & Rental

Opportunity 006Ax0001 · 1 capture · voice · 4h ago

viability open
Equipment rental contracts“they rent equipment out the back, trailers and a scissor lift” · Point of Rental · integration unverifiedviabilityconfirmed
Current POS“they’re on an epi core system” · Epicor Eagle, matched by aliasconfirmed
Reason for switching“wants better reporting, hates the current payment processing fees”confirmed
Decision maker“Talked to the manager, Teddy? Ted?” · authority unclearinferred
Unmapped“Cards are like three percent” · no card for current processing rateunmapped
Kestrel, this was Tuesday I think. Parking was a nightmare, took me twenty minutes to find the place. Talked to the manager, Teddy? Ted? He walked me through the register situation, they're on an epi core system, wants better reporting, hates the current payment processing fees. Cards are like three percent he said. We went through the counter setup, two lanes, he wants a third. Grabbed lunch after at the taco place next door which was honestly great. Oh and they rent equipment out the back, trailers and a scissor lift, they run that on a point a rental thing so that'd need to tie in.

The desk view. Every answer links back to the words it came from. The last sentence, after the tacos, is the one that moves the date.

The handoff artifact on the desk surface. Extraction rows with evidence, the unmapped phrase preserved verbatim, and the transcript with confirmed and viability spans marked. The buried lede is the golden-set fixture this deal exists to carry.

03 · The argument

The AI is handed a deck of cards and told to pick from it. It is never allowed to invent a card.

A closed enum beats a good prompt, and the difference is measurable.

Ask an AI to summarise a recording and it comes back with different categories every time, so nothing downstream can count on it. Here the AI is handed the nineteen question cards for this kind of shop and can only mark each one confirmed, guessed, or never came up. Anything the merchant said that has no card is kept aside in their own words, and that pile is how an admin learns a new card is needed.

With the deck · 19 cards for a hardware store with rentals · Kestrel's read

rental_contractsEquipment rental contracts incumbent_systemCurrent POS switch_reasonWhy they're changing hardware_scopeHardware needs decision_makerDecision maker budgetBudget owner and range timelineTarget go-live location_countLocations business_modelOwnership buying_processApproval process top_priorityTop priority site_readinessInstall readiness restricted_chemicalsLicence check propane_exchangePropane restricted_reportingRestricted sales reporting paint_key_servicesPaint mixing, key cutting jobsite_deliveryJobsite delivery high_sku_countCatalog size contractor_accountsHouse charge accounts

could sink the deal, confirmed confirmed guessed never came up

Without it · what one run invents, none of it twice

age check at registerID verificationage_verificationrental POS tie-inequipment side businesssecondary system (rentals)fee sensitivitycard processing costregister countlane expansion

Five made-up categories per run, 99 across twenty runs. To a database those are ten things. To a person they are four.

The obvious way to build this is to hand the model a transcript and ask for a summary of what was covered. That demos beautifully, and it also produces a different set of categories every time it runs, which means nothing downstream can count on anything: you cannot report on coverage, you cannot compare two visits to the same merchant, and you cannot build a queue of open gaps, because "age check at register" and "ID verification" and "age_verification" are three different things to a database and one thing to a person.

So the model does not get to name the categories. It receives the exact card ids for this vertical as a closed enum and its only job is to decide, for each one, whether the merchant confirmed it, whether it can be inferred, or whether it never came up. Anything the transcript contains that has no card goes to a separate unmapped list with the raw phrase preserved.

Forcing a real thing into the wrong card is worse than admitting there is no card for it, because the first one is silent and the second one is a backlog item.

That last rule is the one I care about. In the golden set there is a merchant who mentions WIC, and the tool has an EBT card. Folding WIC into EBT would look like a successful extraction and would be a data-integrity failure, because they are distinct programmes with distinct requirements and the record would now assert something nobody said. The correct output is EBT confirmed, WIC unmapped, and a vocabulary gap that an admin sees and can turn into a new card.

The contract, in full

  • Closed enum. Card ids come from the library for this vertical. Nothing else is a valid id.
  • Three confidence states. confirmed, inferred, or literal null. A percentage is rejected, because 0.8 is a number that invites a threshold and a threshold is a decision nobody made deliberately.
  • Absence is not adequacy. not_needed requires an explicit merchant statement, quoted. Silence produces not_discussed, forever.
  • Confirmed needs a quote. Every confirmed status carries verbatim evidence from the transcript, and the UI links each field back to its source.
  • Unmapped over forced. A real thing with no card goes to unmapped with the phrase intact.
  • Negation is not mention. Keyword presence is not agreement.
  • Surface conflicts, never silently overwrite. Two answers both stay on the record until a person picks.

Every one of those is enforced by a validator that runs on the response before it reaches state, and a response that violates one is rejected and retried rather than repaired. Repairing it would hide how often the model needs repairing, which is exactly the number I want to be able to see.

04 · Testing it

I wrote the right answers by hand first, then ran the same recording twenty times each way.

Eight hand-written fixtures, and one paired experiment that settles the schema argument.

Eight recordings, each built to carry one way this goes wrong, and I wrote the correct answer for each by hand before the AI ever ran. Then the same recording twenty times with the deck of cards and twenty times without it.

The golden set is eight transcripts, and the expected output for each one was written by hand before any model run. That ordering is the whole point, because expectations written after you have seen the output are a description of the output. Each fixture carries a named failure mode: a buried lede where the blocking requirement arrives in the last sentence after a story about parking and tacos, a negation case, a near-empty capture, a contradiction across two visits, a secondhand report with an unresolvable pronoun, and a control case where nothing is wrong and the correct behaviour is to stay quiet.

The primary metric is recall on viability cards, targeted at about 100 percent, and the reasoning is asymmetric: a missed viability item blows up a go-live, and a false flag costs a rep fifteen seconds. So the system is tuned for recall and over-flagging is accepted.

The paired experiment

To test whether the closed enum is doing real work, both arms run inside the same invocation on the same fixture, and the second arm has the enum stripped out of the prompt while everything else stays identical. Twenty runs per arm, all twenty on one fixture, so what is being measured is run-to-run variance rather than fixture-to-fixture difficulty.

Runs that agreed on the same set of answers, out of 20
With the fixed listWith the closed enum
9 / 20
Without itEnum stripped
3 / 20
Made-up categories across all 20 runs Ids emitted that are not in the library, total
With the fixed listWith the closed enum
0
Without itEnum stripped
99

Twenty runs per arm on one fixture, measured 2026-08-29. 99 invalid ids is roughly five per run against a vertical with nineteen real cards.

With the deck it invented nothing. Even so, only nine runs in twenty agreed exactly, which is why a person confirms every card before it reaches Salesforce. Every deal-breaker in the test set was caught, and each read costs about three cents and twenty seconds.

The denominator, because it is the first thing worth asking: 99 is the total count of invalid ids emitted across the twenty unconstrained runs, so roughly five per run, against a vertical that has nineteen real cards. Every one of those is a category that exists in exactly one run and nowhere else, which is precisely the failure that makes downstream reporting impossible. The constrained arm emitted zero.

The modal-set number is the honest half of the result. Even with the enum, only nine runs in twenty produced the same id set, which means the model's judgment about coverage genuinely varies and the schema constrains the vocabulary rather than the opinion. That is a real limit, and it is the argument for the human confirmation step rather than an argument against the design. The rep sees what was read and confirms or corrects it, and what reaches Salesforce is what a person signed off on.

The layer that runs on every commit

The deterministic tier is graded exactly, not statistically, and it needs no API key: alias matches, alias precision decoys, conflict detection, and the seven contract rules, currently 22 checks. The precision decoys matter more than they look. Several vendor names are ordinary English, so the suite asserts that "we talked about the square footage of the back room" and "she runs a light speed operation" and "rock solid guy, been there thirty years" all match nothing, while the real mentions still match. A false incumbent is worse than a missed one, because it pre-flags requirements off a system the merchant never named.

The rest of the numbers, for completeness: viability recall 2 of 2, seven of eight fixtures passing, $0.0298 and 20.9 seconds mean per extraction.

Dated numbers. The model-layer numbers above were measured on 2026-08-29. The golden set was later rewritten when the demo vertical was replaced, and the model layers were not re-run against the new fixtures, so the per-fixture rows on the eval results page still name the transcripts that were actually run. The deterministic layers were re-measured. The eval runner generates that caveat itself by comparing timestamps, and it clears itself on the next keyed run, which seemed better than trusting me to remember.

05 · What lands in Salesforce

Six ordinary text fields on the record onboarding already opens.

The design work was deciding which fields exist and how each line is worded.

Nothing to install, nothing to learn. Sending twice updates the same record. The field that matters most is the one listing what never came up, and it is the one a tidy-up would delete first.

The handoff writes to the Opportunity through the REST API, and the fields are deliberately plain. They render as text the way any Salesforce field does, and there is no managed package, no Lightning component, and nothing an admin cannot read. An onboarding consultant opens the record they already use and the discovery is on it.

Opportunity · Kestrel Hardware & Rentalupsert on Deal_Key__c · KESTREL
Discovery_Status__c
Open gaps
Viability_Flag__c
rental_contracts · Point of Rental integration, unverified
Expected_Golive_Shift__c
+3 weeks against merchant expectation
Discovery_Confirmed__c
Equipment rental contracts: trailers, lift, contract system in play
  “they rent equipment out the back, trailers and a scissor lift”
Current POS: Epicor Eagle (retail) + Point of Rental
  “they're on an epi core system”
Reason for switching: reporting, ~3% card fees
  “wants better reporting, hates the current payment processing fees”
Hardware scope: 3 lanes  (rep edited, was 2)
  “two lanes, he wants a third”
Discovery_Open__c
STILL NEEDS DISCOVERY
Decision maker: Ted, manager. Authority unclear, rep to confirm with owner.

NEVER CAME UP
Restricted chemical licence check · Budget · Timeline · Loyalty programme
Attached
ContentNote · raw capture, voice, Tue
Chatter · “Field discovery synced 4 confirmed, 1 open, 4 not discussed”
Task · Review viability flag before kickoff · High

The record after one sync. The “rep edited” note is there because a correction by a person and a reading by the AI should never look the same.

The record after one sync. Provenance prints per line: model-read and rep-confirmed, rep-edited, rep-answered without a model read, and rep-marked still open, which moves the card out of Confirmed and into Open under its own heading.

FieldWhat it carries, and why
Deal_Key__cExternal id. Every sync is an upsert on this, so re-sending updates the record in place and never creates a duplicate. This is the field that makes the integration safe to run twice.
Discovery_Status__cA picklist an admin can sort a list view on, so the queue is built from a saved view rather than from a new tool.
Discovery_Confirmed__cLong text. One block per requirement: the label, the value, and the quote it came from.
Discovery_Open__cWhat never came up, printed under its own heading. This field is the product and it is the one that would get deleted first by someone optimizing the page layout.
Viability_Flag__cThe one blocking requirement, above the fold, in the card's own id so it is greppable.
Expected_Golive_Shift__cThe implementation impact in weeks against what the merchant expects, which is the number that makes the flag actionable rather than informational.

Alongside the fields, each sync attaches the raw captures as ContentNotes, posts a Chatter receipt naming what was written, and creates one review Task on first sync. The Chatter post is there so that the record shows who wrote what and when, without anyone having to trust the integration.

Provenance is a field-design problem

The version I demoed wrote every confirmed requirement identically whether the model read it or the rep typed it, and that turned out to be the most consequential thing I got wrong. The record has four genuinely different states and they now print differently: the model read it and the rep confirmed it, the rep changed the value from what the model read, the rep answered a card the model never got, and the rep says the card is still open. That last one moves the requirement out of the confirmed field and into the open field, prints under its own heading, and comes back on the next pre-call brief. Before that change, a rep flagging "I need to talk to the GM about this" was being recorded as a resolved requirement.

06 · What broke

Three of the six things I got wrong.

Six things that were wrong, and what changed because of them.

All mine rather than the AI's, and most found by using the thing rather than testing it. The technical version has all six.

These are all mine rather than the model's, and most of them were found by using the thing rather than by testing it, which is its own lesson.

  • Confirming a card saved its name as the answer, so Salesforce got Restricted chemicals: Restricted chemicals. Now a card with no value prints as a heading with a quote under it.
  • A rep's correction looked identical to the AI's reading. Now the record prints four states differently, including “still needs discovery”, which used to be recorded as settled.
  • Widening the vendor matcher made “a light speed operation” match a POS vendor. The test built for exactly this caught it in the same run.

Confirming a card wrote the label into the value

The confirm handler stored the card's label as its value when the model had not extracted one, so Salesforce received lines that read Restricted chemicals: Restricted chemicals, which looked enough like data that I did not catch it until I read a record properly. The fix was to store null explicitly and to dedupe the value against the label when rendering, so a card with no value prints as a label with a quote under it and nothing pretends to be an answer.

A rep's edit reached the CRM indistinguishable from a model read

I edited a card during a run-through to say the owner was interested but that I had only spoken to the GM, and it posted as confirmed with no indication a human had touched it. A reader had no way to tell a correction from an extraction. The fix was the provenance model in the section above, which cost more design thought than code: the hard part was deciding that "still needs discovery" is a fourth state rather than a variant of the other three.

The status message said "you typed nothing" when I had typed something

A small one, and worth listing because it is the kind of thing that destroys trust in a tool faster than a wrong number does. The edit sheet checked the wrong field for emptiness. The fix was the check, and then rewriting the message, because telling someone they did nothing when they did something is a bad default even when the check is right.

Leaving the side-by-side view never actually left it

The renderer added a class for the combined phone-and-desktop view and removed only some of the alternatives, so once you clicked into it every later mode kept showing both surfaces. The fix was removing all four classes on every render, and then adding a regression check named "leaving Both really leaves it", because the bug was invisible until you went back.

An em dash in an HTTP header threw a TypeError

I wrote a nicely typeset string into a WWW-Authenticate realm, and header values are ByteStrings, so the Response constructor refused it and the whole route failed. The fix was ASCII, and the reason I am listing it is that my own unit test caught it before it shipped, which is the entire argument for testing the boring parts.

Fixing the vendor matcher for multi-word names broke its precision

Some vendor names are genuinely several words on the box, so I widened the matcher's scanning window from two tokens to three. That immediately produced a false match: a three-token window reaches a two-word vendor name by padding it with a short leading word, so "she runs a light speed operation" matched a POS vendor at a cost of one deletion, and the context gate that exists to catch exactly this never saw it because it was checking a string that no longer looked like the vendor name. The fix was to reject the shape before grading it, by matching a window against the vendor name's own word count, with one deliberate exception for a transcriber splitting a joined name in two. The precision decoy in the eval suite is what caught it, in the same run that the new name started matching.

What runs it

Seven pieces, no framework, no app store.

The stack, and what each piece is responsible for.

Anthropic API

The language model that reads what the rep said. One call per capture, about three cents.

One extraction call per capture through the worker. Prompt is versioned; a prompt change is a code change.

Reads the words

JSON Schema + validator

The rulebook every AI answer is checked against before it can change anything.

Structural schema, then core/validate.js for the library-dependent rules. Reject and retry, never repair.

Checks every answer

Cloudflare Workers

A small server that holds the passwords so the phone never has to.

Brokers the model and Salesforce. Origin check, shared-secret header, rate limit. Keys never reach the browser.

Holds the keys

Salesforce REST

Writes six plain fields to the record onboarding already uses. Safe to send twice.

Client Credentials flow, External Client App, API pinned at v66.0, idempotent upsert on Deal_Key__c.

Writes the record

IndexedDB + service worker

Lets the app work with no signal and send the capture later, with the pending count always visible.

App shell and library cached. Queue flushes on open, on reconnect, and on tap. No Background Sync, because Safari.

Works offline

Progressive web app

Opens from the home screen like an app, with no app store and no IT enrolment, so fixes ship mid-pilot.

Web first to validate the motion. Native only if background audio, geofencing, or true push become requirements.

No app store

Vanilla JS, one JSON file

No framework and no build step, and every question for every kind of shop lives in one editable file.

ES modules, core/ shared by browser, worker, and eval runner. 38 cards across 5 verticals in requirements-library.json.

The asset

08 · Where it came from

A take-home case, a panel, and a build that stayed mine.

I built this for a final-round hiring case, a take-home with a time box of a few days and a few focused hours of building expected, followed by a live session where I walked a cross-functional panel through the build and took their questions. The brief described how field reps run discovery in person and summarise it into a single CRM field, gave a handful of sample deals where thin discovery had turned into slipped go-lives and reps being pulled back into onboarding, and asked for a point of view on the whole discovery-to-handoff motion, one piece of it built well enough that a real person could open and use it, what I chose not to build, build-versus-buy thinking, and how I would know it was working.

The role went to another candidate. The build and the thinking behind it are mine, so they live here, with the company, the sample deals and the case materials kept out on purpose.

What it is not

This is a single-call extraction pipeline with a retry loop. It does not use tool calling, retrieval, MCP, or an agent loop, and the structure is enforced by my own validator in code. A second version, in progress, adds tool use with a human confirmation gate in front of every write to Salesforce, plus an eval layer for tool choice.

07 · Limits

What is wired, what is not, and what I would build next.

  • Working: the Salesforce fields, the safe-to-repeat write, the notes, the review task, the offline queue, the rulebook, the tests.
  • Not built on purpose: list-view sorting and per-gap tasks, which belong to whoever owns the Salesforce account, and two-way sync, which needs a story for disagreements first.
  • The public demo is canned: no live AI connection and no key anywhere in it.
  • Next: one outcome number, how often onboarding has to re-interview a merchant before kickoff, with the gap-closure rate and the unmatched-phrase rate underneath it.

Wired and verified: the custom fields, the idempotent upsert on the external id, the note attachments, the review Task, and the Chatter receipt, all against a Developer Edition org through the REST API. The deterministic tier, the extraction contract and its validator, the offline queue, and the eval suite all run.

Not wired: the list view sort, the path component, and the per-gap Task mapping are admin configuration rather than build, and I would set them up with whoever owns the org rather than shipping an opinion about someone else's page layout. Two-way sync is deliberately absent, because a field that writes in both directions needs a conflict story before it needs an implementation.

The published demo runs canned. There is no LLM broker wired into this deployment and no key anywhere in it, so the extraction you see in the walkthrough is a hand-written record of the quality the model produces on that transcript, and the app's other deals fall through to the deterministic keyword pass, which is exactly what a rep sees before the reader has run.

What I would build next is the measurement, and it is one outcome number with leading indicators underneath it. The outcome is the rate at which onboarding has to re-interview a merchant before kickoff, because that is the step that costs a merchant a month and a rep their credibility. The leading indicators are gap-closure rate between the first visit and the handoff, the share of handoffs that arrive with an open viability flag, and how often an unmapped phrase becomes a new card, because that last one tells you whether the library is keeping up with what reps are actually hearing.

Switch version