Two jobs, same invoice, very different money
True job cost: burden, drive time, allocation
Every dispatch board treats a $480 diagnostic and a $480 equipment swap as the same job. One carries most of its ticket as profit, the other barely covers the hours it consumed. Here is the method for telling them apart, and the two details that decide the answer.
One measure: burdened labor including drive time, parts at cost, and overhead allocated per billable hour. Below: the rate table, the cost measure, why allocation basis reorders the whole report, and the grain trap that hides losing work inside a healthy average.
01 Revenue per job is known, profit per job is not
Field service software is built to dispatch and invoice, and it does both well. What it does not do is net out what the job consumed: the technician's fully loaded hours, the travel, the parts pulled off the truck, and the share of running the company that those hours absorbed.
So the business is managed on ticket averages. Busy months feel good, revenue grows year over year, and the money left at the end stays flat. Usually there is no single big leak. There are two or three job types quietly losing money on every call, and nothing in the reporting is capable of saying which.
02 Three components, two of them usually missing
True cost is labor plus parts plus overhead. The arithmetic is trivial; the definitions are where the money is.
- Labor at a burdened rate. Not the number on the pay stub. Wages plus employer taxes, benefits, insurance, the vehicle, the fuel and the phone. Someone at $28 an hour on payroll typically costs closer to $50 by the time they pull into a driveway.
- Drive time as labor. It is paid, it is tracked, and it is almost never costed. A 45 minute round trip on a 30 minute repair is often the whole difference between a profitable call and a break even one.
- Parts at cost. What was consumed at purchase price, not the marked up line on the invoice.
- Overhead per billable hour. Not spread evenly across job count. This is the single choice that reorders the ranking, and the next section is about why.
03 The allocation choice changes the answer
Spread overhead evenly per job and a 19 hour installation absorbs the same overhead as a 1.2 hour maintenance visit, which flatters long work and punishes quick high ticket work. Allocate it per hour consumed and the table below reorders itself. Re-sort it and watch which job types move:
| Job type | Jobs | Avg ticket | Hours incl. drive | True cost | Profit / job | Margin | Total GP |
|---|
Sort by margin and the diagnostic and maintenance rows drop to the bottom. That is expected and it is not a reason to stop selling them: their function is to create replacement work, not to carry the company. But it only holds if they actually convert, so the model has to link a diagnostic visit to the job that followed it. Once it does, a "low margin" job type often turns out to be the most valuable thing the business sells.
04 The same math changes how people are ranked
Ranking technicians by revenue rewards whoever gets dispatched to the biggest jobs. Ranking them by margin, hours per job and callback rate shows something else: the quiet one with a lower ticket average who never goes back twice is frequently the most profitable person on the roster. Callbacks are the cost nobody invoices.
01 Three systems, three grains
The data exists and it lives apart. The field platform holds jobs, revenue and assigned technician. Payroll holds hours and pay per period. Accounting holds parts and overhead accounts by month. Nothing joins them, and each is recorded at a different grain.
| Source | Grain | Bridge key |
|---|---|---|
| Field platform: jobs | one row per job | JobId |
| Field platform: dispatch log | one row per tech per job | JobId + TechId |
| Payroll | one row per tech per pay period | TechId + period |
| Accounting: overhead | one row per account per month | period |
The dispatch log is the bridge that makes everything else possible: it is the only place a technician hour is tied to the job it was spent on.
02 The burdened rate is a table
Not a constant in a formula. A rate per technician per period, so raises, insurance changes and a new truck all land in history correctly rather than retroactively rewriting last year:
-- Burdened hourly rate per technician, per period Burdened rate = VAR DirectPay = SUM( 'Payroll'[Wages] ) + SUM( 'Payroll'[EmployerTaxes] ) + SUM( 'Payroll'[Benefits] ) + SUM( 'Payroll'[Insurance] ) VAR Vehicle = SUM( 'TechCosts'[VehicleAndPhone] ) VAR PaidHours = SUM( 'Payroll'[PaidHours] ) RETURN DIVIDE( DirectPay + Vehicle, PaidHours )
03 The cost measure
Onsite and drive hours are summed before the multiplication, which is the whole point:
-- True job cost = burdened labor + parts + allocated overhead True job cost = VAR LaborCost = SUMX( 'Job Labor', ( 'Job Labor'[OnsiteHours] + 'Job Labor'[DriveHours] ) * RELATED( 'Technicians'[BurdenedRate] ) ) VAR PartsCost = CALCULATE( SUM( 'Job Parts'[Cost] ) ) VAR Overhead = [Billable hours on job] * [Overhead rate per hour] RETURN LaborCost + PartsCost + Overhead
04 The allocation basis, in one line
Overhead for the period divided by billable hours in the same period, so the rate moves with utilization rather than with how many jobs happened to be booked:
-- Overhead rate per billable hour, current period Overhead rate per hour = DIVIDE( CALCULATE( SUM( 'Overhead'[Amount] ) ), CALCULATE( SUM( 'Job Labor'[OnsiteHours] ) ) )
05 The grain trap
The most common error in a costing model is aggregating first and dividing last. Sum revenue, sum cost, divide, and you get a respectable company margin with the losing job types buried inside it. Compute per job, then aggregate:
-- Gross profit, computed per job then summed Gross profit = SUMX( VALUES( 'Jobs'[JobId] ), [Job revenue] - [True job cost] )
The same discipline applies to margin percentages: never average a percentage. Recompute it from the aggregated numerator and denominator at whatever level you are displaying.
06 Estimating what never got booked
Missed calls are worth quantifying precisely because the number is uncomfortable. Built from the company's own averages rather than an industry benchmark, and presented as an order of magnitude with the assumptions visible:
-- Estimated revenue lost on unconverted inbound calls Missed call exposure = VAR Unbooked = [Inbound calls] - [Calls converted to jobs] VAR AvgValue = DIVIDE( [Total revenue], [Job count] ) VAR CloseRate = [Booking rate] RETURN Unbooked * AvgValue * CloseRate
07 What stays editable
Vehicle cost per technician, which accounts belong in the overhead base, the callback window, membership pricing: all in an input sheet, none in formulas. Change a cell, refresh, and every historical period recomputes on the new definition. That is what makes the model survive contact with a real business.
Let's cost your jobs properly, on your own data
In a free review we take a month of your dispatch and payroll exports and show the real margin by job type, with drive time and overhead where they belong. No obligation.
