
Role: Business Systems Analyst & Digital Strategist
Company: Ehret Plumbing Heating & Cooling Belleville, IL
Build Time: 9 Months through July 2026
Scope: Solo build. 24 screens, 46 services, about 38,000 lines. Node, Express, SQLite, vanilla JS, OpenAI. Sits between ServiceTitan and LeadConnector for a 15-technician plumbing and HVAC contractor.
Result: Started as one automation and became the office's daily operations layer. Customer service reps entered information once and it landed everywhere it needed to go, and the calls, jobs, and equipment records the company already owned finally told somebody what to do next. Ran under $25 a month against the $700 stack it replaced.
I wanted a review request to fire when a job closed in ServiceTitan. That was the whole ask.
Ehret has been in business since 1903 and is run by the fifth generation of the family. They had bought good software and hadn't finished absorbing it, which describes most of the trades. ServiceTitan was in place and used mostly for billing. Technicians worked off paper. There was no job-level cost data, so nobody could say what any single job earned.
Inside my first thirty days I got the technicians working in ServiceTitan directly. Nothing about that went smoothly, because it was teaching old dogs new tricks. The younger techs came around first and helped pull the rest along.
The review trigger should have taken an afternoon. It didn't.
ServiceTitan's API returns objects that point at other objects. Pull a job number and you may not get the address or the technician's name. Pull an invoice number and the address comes back in a format nothing downstream can read. One review request meant chaining three or four calls and reshaping the payload before GoHighLevel would take it. Zapier billed a task for every hop and truncated the nested data on the way through. I was paying more per job to get less usable output, then building workarounds to recover what it dropped.
I knew what I wanted the thing to do. I was limited by the dropdown options Zapier had available in tasks and triggers.
Once I could reach the data, the review trigger stopped being the interesting problem. ServiceTitan tracks whether a call got booked, but only if a human labels it, and nobody was labeling calls, so the booking rate wasn't measuring anything. Completed jobs sat unbilled because the invoice never got generated in the field and neither system noticed. Warranty registrations got skipped because it meant typing the same equipment into ServiceTitan and then again into each manufacturer's portal in a different format. Registration gaps don't surface until someone needs the warranty.
I brought in outside developers to build the bridge properly. After a few conversations it was clear the scope I needed wasn't something they could quote. So I built it.
I read the API, got stuck pulling call audio, worked the problem through Claude, and shipped a Node service on Railway with the repo on GitHub. That ended the Zapier dependency, which was the original point.
Then it kept growing, but not because I went looking for things to build. I'd use a screen for a few weeks and find the place where somebody was still retyping, or still checking a thing the system already knew. Each addition came out of a redundancy that surfaced in use. Twenty-four screens is what two years of that looks like.
Every inbound call transcribes and classifies into fourteen categories with confidence, sentiment, spam flag, and summary bullets, matched to a customer and a likely job. The office reviews a queue, corrects anything wrong, and posts a clean note back to ServiceTitan.

American Standard imports from the warranty PDF. Bradford White reads a photo of the data plate through vision OCR. Rinnai exports a ProPortal CSV. Serial numbers decode arithmetically from each manufacturer's date encoding and get cross-checked against the OCR, because two weak signals that agree beat one strong signal you can't verify.

Monthly and fiscal-year profitability, per-customer cost versus benefit, supplier invoice to purchase order parsing, pricebook deduplication and AI renaming, address geocoding and correction, timesheets with comp-time balances, a county-format backflow report, contract diffing, fleet reporting, membership and review sync to LeadConnector, and job walkthrough video upload.
The first version of Open Jobs flagged every membership maintenance visit as lost revenue. Those bill zero on purpose, covered by dues. After the third false alarm people stopped opening the screen.
The fix wasn't better filtering. It was telling people what I filtered. The current version excludes membership visits and prints on screen what it hid and why. Being open about the exclusion is what made the remaining number believable.
Two more like it. The membership push to GoHighLevel POSTed unconditionally and created a duplicate contact on every poll, so I moved it to an upsert. And mid-month, ServiceTitan gives you revenue with no cost attached, which computes to a triumphant and completely false 100% margin. Now margin shows a dash until the cost data lands.

Call review shipped as analysis only. It transcribed a call, scored the sentiment, and stopped, which left someone to go find the customer and the job by hand. Attaching it to both was the obvious fix. The one I didn't see coming was that the first version treated every inbound call as a customer. Employees call in. Suppliers call in. Running sentiment on a parts vendor confirming a delivery filled the queue with noise, and a queue people stop believing is a queue people stop opening. Caller classification cleaned it up.
Automation spend dropped from roughly $700 a month to under $25, while the system did far more than the stack it replaced.
Call review used to mean listening to fifteen or twenty calls a day at two to three minutes each. That became a queue you read.
One customer scheduled a plumbing quote and never heard back. The technician hadn't told anyone he was working on it. The customer called in upset and reached the answering machine. Call review caught it and it got resolved days sooner than it would have.
The change I'd point to isn't in the numbers. The office started counting on the review being on the job. When a customer called back referencing an earlier conversation, the notes were already there, and nobody had to remember who took the first call.
My position was eliminated in July 2026. Call review and fleet reporting were in daily use. Several modules were built and piloted without reaching the full team.

The model proposes. A deterministic check or a human disposes
A confident wrong answer costs more than a missing one
If a deterministic rule can answer it, don't ask the model
Every write to an external system is logged or reversible
Config over code whenever the business changes its mind
I never instrumented the thing I set out to build. Nine months in, I couldn't tell you whether the review workflow moved anything, because I wasn't measuring it. I built the reporting layer for the company's operations and skipped it for my own work.
Second one: I left too much human in the loop on the safe half. Call reviews waited on my approval even when the classifier was confident on a routine category. I'd auto-post the boring ones and save review for the ambiguous ones.
Built for a plumbing contractor. The pattern holds anywhere the software got bought and the workflow never caught up: find where people are retyping, and close the gap.
Ryan Wisnasky • Creative Marketing Strategist • Collinsville, IL • [email protected]