Changelog · 2026-05-02 — pull-labor-onto-invoice, 3-tier proposals, and the math is finally exact
A bigger-than-usual changelog. We shipped four major features this week — all driven by what HVAC, plumbing, and home-service contractors told us the incumbent platforms get wrong. Here's what's new, why it matters, and how to use it.
The product is six days old. We've been reading every public complaint thread about ServiceTitan, Jobber, Housecall Pro, Workiz, and FieldEdge we can find — r/HVAC, G2 reviews, Capterra reviews, Reddit DMs forwarded by early users — and turning each pattern of pain into a design constraint baked into the codebase.
This week's changelog is what fell out of that work.
TL;DR — what shipped
- Pull labor from past visits onto invoices. One click on the invoice composer adds tracked time entries for this customer — grouped by appointment, with weighted-average suggested rates — without re-typing hours.
- Good / Better / Best 3-tier proposals. Send three side-by-side options instead of a single take-it-or-leave-it quote. Housecall Pro's published case studies report ~50% average-ticket lift on the same job.
- Time-math regression test. A repeatable script (
scripts/audit-time-math.mjs) that asserts your tech-app hours, your reports, and your customer invoices all agree to the millisecond. No more phantom hours. - Reports audit script. A second script (
scripts/audit-reports.mjs) that catches "the report drifted from the database" the moment it happens, so you don't find out from a customer. - Behind the scenes: forms now auto-save as you type, the tech app keeps working when the phone loses signal, every customer-facing claim on this site has been fact-checked against the production code, and we updated the FAQ + KB to cover the eight features we'd shipped since the last refresh.
Let's walk through each one.
Feature 1 — Pull labor from past visits onto an invoice
What it does
Open any invoice composer with a customer pre-selected. There's a new button: "Pull labor from past visits."
Click it, and Plyrium shows you every unbilled time entry for that customer, grouped by appointment, with the weighted-average suggested rate already calculated. Tick the visits you want to bill. Click "Add to invoice." You're done.
The tech entered the hours from their phone weeks ago using the start/stop timer in /portal/tech. The accountant can now bill those hours from a desktop without ever picking up the phone to ask "wait, how many hours did Mike spend at the Johnson place?"
Why it matters
A theme in every Housecall Pro thread we read: "the tracked hours don't auto-populate the invoice." The tech logs the time on their app. The office sees the time on a report. But the invoice composer is a blank slate — somebody has to manually copy the hours over.
Most shops eat this as one missed billing per week per tech. A 5-tech shop loses ~250 billable hours per year just from "we forgot to put that on the invoice." At $85/hr that's $21k of revenue you earned and never collected.
How to use it
- Open the invoice composer (
/portal/invoices/new) with the customer set. - Click "Pull labor from past visits."
- The picker shows each appointment's total hours + suggested rate. Tick the ones you want to bill.
- Override the rate or description if needed.
- Click "Add to invoice." A labor line item lands per selected appointment; the underlying time entries get marked billed so they won't reappear next time you open the picker.
Tips
Skip flat-rate jobs. If you priced the water-heater install at $1,200 total, the labor is already bundled in. Pulling the time entries on top double-bills the customer. The picker is for time-and-materials work.
Skip contract-included visits. A customer on your $19/mo Care Club already paid for their two annual tune-ups via the membership fee. Adding labor on top of that breaks the perk. Add a $0 line if you want the visit on the invoice for record-keeping.
Run the time-math audit. New script: node scripts/audit-time-math.mjs. Asserts the math is exact for your workspace. Safe to run in production — it deletes the test rows when done.
Feature 2 — Good / Better / Best 3-tier proposals
What it does
In the quote composer, there's a new toggle: "Make this a 3-tier proposal."
Flip it, and the composer becomes a 3-tab editor: Good / Better / Best. Build different line items per tab. Each tier can have a completely different scope — repair vs. repair-with-upgrade vs. full replacement. Save and send.
The customer sees three side-by-side cards on their public link instead of one line-item table. They pick the option that fits their budget. The middle tier is highlighted with a "★ Most chosen" label and a softly glowing border — that's intentional anchoring math at work.
When they accept, Plyrium copies the chosen tier's line items into the regular invoice flow. From that point on it behaves exactly like an accepted single-quote — invoice draft auto-builds, customer record auto-creates, no special handling downstream.
Why it matters
Single-quote framing makes the customer compare your price to $0 (do nothing). Anything looks expensive next to free.
3-tier framing makes them compare Good to Better to Best. Suddenly the middle option is the default reasonable choice. Customers who would have walked at the original price find a cheaper Good they can say yes to. Customers who can afford it self-anchor on Best, and even picking Better feels like a discount.
Housecall Pro's published case studies show water-heater jobs going from a $1,200 average ticket to $1,850 — same job, same shop, just a different quote format. We're matching the feature.
How to price each tier
| Tier | Price spread | What's in it |
|---|---|---|
| Good | 30-40% UNDER your normal price | Cheapest viable solution. Covers the immediate problem. No frills. |
| Better | Your normal single-quote price | What you'd recommend if asked. Highlighted as "Most chosen." |
| Best | 30-40% OVER your normal price | Premium option — better materials, longer warranty, faster timeline, free annual tune-up bundled in. |
Tips
Use it for jobs over $500. Below $500 the price spread between Good and Best is too small to anchor anything — you're shaving $30 off a $200 job, which doesn't move buyer psychology. Above $500 (water heaters, HVAC repairs, drain mains, panel upgrades) is where anchoring pays. Below that, send a single quote.
Differentiate scope, not just price. Tiers that differ ONLY in dollars feel like manipulation ("why is the same job $800 vs $1,200?"). Tiers that differ in scope feel like real choices: Good has a 6-month warranty + standard parts; Better has a 2-year warranty + brand-name parts; Best has a 10-year warranty + premium parts + a free annual tune-up. Customers respect math they can see.
Don't build a Good tier you'd be embarrassed to do. Your reputation rides on every job, even the floor-priced ones. Good should still be solid work — it just leaves out the upgrades. Customers pick up on bait-and-switch tiers fast.
Skip 3-tier on emergency calls, diagnostic-only visits, and recurring-contract customers. Emergencies want a fix, not a menu. Diagnostics aren't tier-able. Recurring customers trust your judgment — sending them a 3-tier proposal feels like overselling.
Feature 3 — Time-math regression test
What it does
scripts/audit-time-math.mjs is a 5-test-case script that asserts your tech-app hours, your reports, and your customer invoices all agree exactly. It runs against a live workspace, creates a handful of test rows with known inputs, checks the generated duration_minutes and cost_cents columns produce the right values, and deletes the test rows when done.
Run it on demand or wire it into CI. Safe in production.
Why it matters
Jobber's own customers report ~90 minutes of phantom hours per week — the math their tech app shows doesn't match the math their reports show, because somewhere along the way someone added or rounded a millisecond. At $85/hr that's $128/week of either over-billing or under-billing per tech.
Plyrium's time_entries.duration_minutes is a Postgres GENERATED ALWAYS AS column computed server-side from the millisecond started_at and ended_at timestamps. No client clock skew, no JS floating-point drift. The script proves it stays that way.
Tips
Run it after any migration that touches time_entries. If a future migration changes how durations get computed, this script will catch it on the next run instead of you finding out from a billing dispute three weeks later.
Wire it into CI when you're ready. It exits non-zero on failure, so it's a drop-in pre-deploy gate.
Feature 4 — Reports audit script
What it does
scripts/audit-reports.mjs runs seven internal-consistency checks against your reports + your underlying tables:
- Every invoice status in the database is one we know how to render (catches drift when a migration adds a new status).
- P&L revenue (sum of
total_centson paid invoices) matches the sum ofamount_paid_centsfor the same period. - Aging buckets (current / 0-30 / 30-60 / 60-90 / 90+) sum to total receivables — no rows lost in bucketing.
- Sales tax collected is non-negative.
- Completed-appointment count is a non-negative integer.
- Time-entry rollup totals are non-negative integers (catches inverted timestamps).
- Every paid invoice has a non-zero
amount_paid_cents— catches webhook drift where Stripe marked something paid but the amount field didn't update.
Why it matters
Another theme from the ServiceTitan threads: "Reporting breaks silently. Updates ship without testing. We find out the report drifted when a customer asks why their numbers don't match."
A 100-engineer team can ship features faster than they can coordinate manual QA. The fix is automation — make the report run a self-check every time, not just when somebody remembers to look.
Tips
Run it weekly, even when nothing changed. Drift can come from a bad webhook payload that landed at 3am and never threw an error. The script will tell you within hours.
Run it against any workspace, not just demo. Pass --client=<uuid> to point it at a specific shop.
What else changed this week (the smaller stuff)
- Forms auto-save as you type. Open the quote composer, type for ten minutes, close the tab by accident, reopen — your draft is right where you left it. ServiceTitan's most-quoted complaint is "it erases a form I'm working on at least once a day." That doesn't happen here.
- Offline action queue in the tech app. Tech in a basement, tech in a crawlspace, tech in rural areas — the app keeps working when signal drops. Status updates, time logs, parts logs all queue locally and sync the moment the connection comes back.
- Idempotency keys on every mutating API route. A bad-network retry won't double-charge a customer or double-create an appointment. The first request wins; the second returns the same response without re-executing.
- Two new KB articles covering the features above, plus FAQ refresh on the marketing site.
- Comparison page at /why-not-servicetitan walking through every named complaint we've heard with our specific design response.
Why this matters as a whole
When we started building Plyrium six days ago, we didn't have to invent a product roadmap. The home-service contractor community already gave us one — they've been writing it as complaint threads about the incumbent tools for the last decade.
Every feature in this changelog maps directly to a specific complaint we read in a public thread:
- "Tracked hours don't auto-populate the invoice" → pull-labor-onto-invoice button
- "I want a Good/Better/Best option flow" → 3-tier proposals
- "The math drifts between the app and the report" → time-math regression test
- "The reports break silently" → reports audit script
- "It erases the form I'm working on" → form auto-save
- "It wipes my page in bad service areas" → PWA offline queue
- "A bad-network retry double-charged my customer" → idempotency keys
We don't have to invent. We just have to listen, and ship.
If you're an HVAC, plumbing, electrical, or home-service contractor and any of this lines up with what you've been frustrated about — start a free trial or book a 15-minute walkthrough. We're under 30 days old, the founder reads every email, and the roadmap is whatever you tell us is missing.
Try Plyrium yourself
Hear our AI receptionist live
Call our public demo line — same system that runs Plyrium customers' phones.
(928) 666-4329