Flow Through Logic/Published work/FlowSpec
FlowSpecLive
Voice-to-quote software for Australian trades
Speech capture to a structured quote to Xero, priced against a proprietary Australian trade dataset.
- Speech to structured quote
- Xero integration
- Live subscriptions
The problem
A quote gets written twice. Once in the ute at the end of a job, scribbled on a docket or held in your head, and again at nine that night on the laptop, where every line has to be looked up and typed into the accounting software. The pricing comes off memory or a supplier PDF, so the margin drifts. The quote goes out two days later, by which time the customer has already rung someone else.
What it does
A tradesperson talks through the job the way they would describe it to an apprentice, and a structured quote comes out the far side. The audio becomes a transcript. That transcript becomes job detail naming the client, the site, the job description and one entry per material, and each of those entries becomes a real catalogue line carrying a code, a unit price, an account code and a tax type. Australian trade shorthand is handled before matching runs: a dictionary held in the repository translates spoken abbreviations and colloquialisms into the full product names a supplier would print.
The prices are the account’s own Australian rates rather than a generic catalogue. The Xero item list syncs in, and every item is embedded for semantic search and given generated colloquial aliases, so the phrase someone actually says finds the row a supplier actually wrote. Matching runs a vector search and a full-text search in parallel and hands the merged candidates to a smaller model to adjudicate. Where a tradesperson resolves an ambiguous line and asks for it to be remembered, that phrase resolves to that item on later quotes, priced off the live catalogue rather than a frozen number. Saved formula rules add the derived lines a trade always carries, such as disposal volume scaled off a poured area.
The quote lands in the tradesperson’s hands as a draft, with anything uncertain flagged and named. A deliberate send pushes it into Xero as a real quote against a real contact, carrying the item code, the line description, the account code and the tax type on every line. Signup runs self-serve through Xero’s own OAuth, and Stripe carries the subscription. A paying customer outside the company runs on it.
How it works
One request path carries a job from spoken words to a quote sitting in an accounting system, and each stage hands the next a more structured object than it received. Typed input joins at the extraction stage and follows the rest of the path identically.
- CaptureThe browser records the spoken job description and posts the audio to the server, where a transcription model returns text. Uploads are size-capped and format-checked at the boundary before any paid inference runs.
- ExtractionA language model reads the transcript alongside the Australian trade slang dictionary and returns structured job detail: the client and site, plus one entry per material with a quantity and a unit. Quote-level instructions the tradesperson spoke, such as a discount across the whole job, are extracted separately from the line items.
- Catalogue matchEach material runs a vector search and a full-text search over the account price list at the same time, the two result sets merge and de-duplicate, and a smaller model picks the best candidate from what survives. Pack sizes parse deterministically by regular expression across the supplier formats that occur in practice, so a catalogue line reading "carton of 50" resolves without asking a model anything.
- AssemblyMarkup applies across catalogue-priced lines and leaves any price the tradesperson stated aloud exactly as spoken, since a stated price is their final sell price. Repeated mentions of the same item consolidate by code into one line, and saved formula rules inject their derived lines. GST then computes per line against each item’s own tax type, so exempt and free-rated items stay correct.
- ReviewThe quote stores as a draft, or as flagged when the engine has something to raise, most often a line that arrived without a confident match. The tradesperson reads the flags, edits by voice or by typing, and matches the client against their live Xero contacts from the review screen.
- Push to XeroA send builds the Xero payload through a shared port module that both the live path and the sandbox use, resolves or creates the contact, and posts the quote under an idempotency key derived from the local quote id. Xero’s own QuoteID and quote number come back and are written onto the local record.
- Account and billingTrial state and subscription state live on the account profile, with billing resolved to the account owner so an invited team member is covered by the owner’s plan. Stripe carries checkout and the subscription lifecycle through a webhook handler that records each event before acting on it.
What was hard
A price list that could disappear halfway through a sync
Pulling a Xero catalogue into the search index is multi-step work: fetch the items, generate embeddings in batches, write the rows, then swap the new set in for the old. The original shape deleted the existing rows and then inserted the new ones, which left a window where the account held zero items, and any mid-sync failure made that window permanent. Rows now build under a staging supplier value that live lookups ignore, one batch of a hundred at a time. A failure deletes only the staging rows and the previous list stays whole. Once every batch has landed, the old rows are removed and staging is renamed to the live value, so the worst case became brief duplicate visibility instead of a missing catalogue. A second failure sat on top of that one: a large catalogue takes tens of seconds while the front end gives up at ninety, so a retry raced the rename and could still empty the list. A per-account in-process lock returns a busy response to the second run, and the code says plainly that a multi-instance deployment wants a Postgres advisory lock in its place.
Counting one send exactly once
A push to Xero is both an external write into someone’s accounting system and the meter for the free trial, so a double tap or a lost response could create a second draft in Xero, spend a second trial credit, or both. Four mechanisms hold it. The POST to Xero carries an idempotency key derived from the local quote id, so a retry inside the provider dedup window returns the original quote. The write-back is a compare-and-swap that sets the Xero id only where it is currently null and reports how many rows it touched. The trial increment then runs through an atomic database function, and only for the single request that won that race. A partial unique index over the Xero id, added in the pre-launch integrity migration, allows nulls for drafts while making a duplicated push a database error rather than a quiet second record. The comment beside the compare-and-swap is honest about what remains: if both requests genuinely reach Xero, a duplicate draft can exist there, and the trial counter stays correct regardless.
Safe to leave running
The engine writes into someone’s accounting system when a person presses send, and at no other moment. Everything before that stays inside the product as a draft the tradesperson owns and can edit or discard. Autonomy runs the whole distance from speech to a priced, contact-matched quote, and a human carries it across the last step.
Uncertainty is surfaced rather than resolved quietly. A line that finds no confident catalogue match prices at zero and carries a flag naming the reason, which puts the quote in a review state instead of a ready one. On the create path a spoken quote-level instruction such as a discount across the job appears as a flag to apply by hand, because applying a discount to the wrong subset of lines is worse than leaving the totals alone. A voice edit on an existing quote does apply a discount that names both its percentage and its scope, and keeps the flag for the ones that name neither, such as "mate’s rates". A pack size with no exact match prices as individual units and says so in the flag. The lookup also reports whether the search backend itself failed, separately from an account catalogue that simply held nothing, so an outage on our side reads as an outage rather than as the user’s price list being wrong.
The money paths are idempotent by construction. Every Stripe delivery claims a row in a webhook events table before the handler runs, and a duplicate delivery is answered from that row: completed means acknowledge and stop, incomplete means a prior attempt died mid-handler and the work replays. Credit operations against Stripe carry idempotency keys derived from the event id. The comment records what this replaced, which was an inversion where a thrown handler returned an error, the provider retried, the duplicate check reported success, and the event was lost for good.
Spend has ceilings at the boundary where strangers can reach it. The public demo runs the real engine for anyone with no signup, so anonymous traffic burns paid transcription and inference: uploads cap at three megabytes, writes are rate limited per address with the worst-case cost arithmetic written into the comment beside the limit, every query is pinned server-side to one seeded demo account so no real account is reachable through it, and the demo quotes are swept after six hours so the table stays small.
The contact list is read at the moment it is needed rather than mirrored. Contact search queries Xero live with the typed term and returns the handful of matches, so there is no local copy of the address book to drift or leak, and the client detail the product does hold is what the tradesperson spoke onto a quote. Archived contacts are filtered out, and results are name-filtered again on our side so an over-broad query still returns only real matches.
The process fails towards a stop. Production boot halts when the CORS origin list is missing rather than falling back to a permissive default. An uncaught exception drains in-flight requests through the same shutdown path as a normal signal and exits non-zero so the platform restarts it cleanly. The model and accounting calls on the quote path carry explicit timeouts, written in after the provider defaults of ten minutes and two retries compounded into a degraded upstream holding a quote request open for as long as thirty minutes.
Fourteen of the forty-nine migrations verify themselves. Each of those closes with a block that re-reads the database and raises an exception when what it just created is missing, so a half-applied migration in that set announces itself at apply time instead of sitting quiet until something depends on it. Two pure functions carry runnable self-checks committed beside them: one asserts that a null, undefined, empty or non-numeric catalogue price becomes zero rather than a NaN leaking into a quote total, and the other asserts that a large result set drains every page rather than stopping at the default first thousand rows.
Evidence
- Speech to structured quoteEach quote row stores the words it came from, the transcript for a spoken job and the typed text for a written one, alongside the structured line items, anything the engine flagged and the milliseconds the pipeline took, all written by the quote engine at the moment of insert. The same engine backs the public demo, so the path is checkable end to end by speaking a job into it and reading what comes back.
- Xero integrationA successful push writes Xero’s own QuoteID and quote number back onto the local quote row, so the link is a recorded value per quote rather than an assertion. The pre-launch integrity migration adds a unique index over that column for pushed quotes, which turns a duplicated push into a database error at the point it happens.
- Live subscriptionsEvery Stripe delivery is written to a webhook events table keyed on the provider event id, with the time the handler completed, so the billing history is a table someone can read rather than a claim. That same subscription state gates the paid routes, and a paying customer outside the company runs against it.
See it running
The public demo at flowspec.com.au/demo runs the real engine on a seeded price list, so speaking or typing a job returns a priced quote without a signup or a Xero connection.