Key takeaways
We already have four pages on contractors. This one is deliberately not about any of them.
Contractor versus in house maintenance owns the make or buy argument, and contractor management in a CMMS owns the day to day practice.
Our maintenance labour data model specifies the time booking for the technicians a plant employs. It stops at the plant boundary on purpose, and names the contractor invoice as an input it does not model.
This page is that missing half: the records that hold the hours your plant does not employ and the money it otherwise cannot see per work order.
| Internal technician | Contractor |
|---|---|
| You own the time record | You receive the cost |
| Cost is derived: hours x rate | Hours must be reconstructed |
| Written the same day | Arrives weeks later |
| Complete to the hour | Complete to the euro |
| Rate is a finance input | Rate is a negotiated term |
An internal booking is a time record with a cost attached. A contractor invoice line is a cost record with time attached, if you are lucky.
That single inversion explains why contractor data is hard. Your maintenance system is built around a clock, and the contractor's document is built around a price.
Now mix the two in one report. A cost per asset, a cost per work order or a maintenance cost against replacement asset value all add internal labour to contractor spend.
The two halves are accurate in different dimensions and timed differently. Internal labour lands on the day the work happened, contractor cost on the day the invoice was posted.
| Half of the number | What it is accurate about |
|---|---|
| Internal labour | The hours, and the day they happened |
| Contractor spend | The euros, and the day they were posted |
The field that makes it possible is a cost_source discriminator on every cost line, holding booking or invoice. Without it, nobody can tell which half of a number came from which clock.
It matters most in maintenance cost as a percent of RAV, a ratio that has to hold its shape for years. Move contractor work in or out of the numerator between years and the trend is an artefact.
A contractor is not a supplier of parts and it is not an employee. It is a third thing, and giving it a real row is the first decision.
The grain is one row per legal entity you contract with, not per person who arrives at the gate.
| Field | Why it is mandatory |
|---|---|
| contractor_ | Stable key, never the trading name |
| legal_ | The entity the invoice comes from |
| trade_ | What they are allowed to be called for |
| status | approved, conditional, suspended |
| owner_ | The person inside the plant who owns it |
| active_ | First day work may be assigned |
| active_ | Last day. Never delete a leaver |
status is the one that earns its place on day one. A contractor whose insurance lapsed is not deleted, it is moved to suspended, and the assignment screen has to respect that.
trade_codes stops a lifting gear inspector being assigned an electrical call-out because they answered the phone first.
| Field | What it holds |
|---|---|
| default_ | The rate card used when none is named |
| purchase_ | The same entity in purchasing |
| insurance_ | Drives the status, not a reminder |
| certificate_ | Competency and safety documents |
| induction_ | Site induction, per entity or person |
| payment_ | Days. Explains an invoice date lag |
| currency | Never assume the plant's currency |
purchase_vendor_id is the field most plants forget, and it is the one that makes reconciliation possible at all.
Keep the purchase order, the goods receipt and the three way match where they belong, in the MRO purchasing data model. This page does not respecify them, it only needs the key.
An agreement is a dated container. A rate card is the set of priced lines inside it, and the two are not the same table.
Putting a rate directly on the contractor record is the mistake that makes historic costs move. A rate has a life, and last year's work order has to keep last year's price.
| Field | What it decides |
|---|---|
| agreement_ | Key, printed on every assignment |
| contractor_ | Who the agreement is with |
| commercial_ | The four values in the next section |
| valid_ | A rate card has a start date |
| valid_ | Null while it is current |
| response_ | The agreed commitment |
| response_ | calendar or contracted hours |
| tolerance_ | When a variance is queried |
response_clock looks like a detail and decides most arguments. Eight calendar hours from a Friday evening call is Saturday morning, and eight contracted hours is Monday afternoon.
| Field | What it holds |
|---|---|
| rate_ | One row per priced thing |
| rate_ | labour, travel, material, call-out |
| trade_ | Fitter, electrician, inspector |
| band | normal, out of hours, weekend |
| unit | hour, visit, job, km, each |
| rate_ | The agreed price per unit |
| minimum_ | The floor the invoice charges |
| material_ | Multiplier on parts they supply |
minimum_units is the field that turns a surprise into arithmetic. It is the difference between an invoice that looks wrong and an invoice that the plant predicted to the cent.
material_markup is the second one. A contractor who supplies the part and adds a percentage is selling you two different things on one line, and only one of them is labour.
Almost every contractor arrangement is one of four shapes. They price completely different objects, and a record that fits one fits the others badly.
| Model | What is actually priced |
|---|---|
| Time and materials | Hours, at an agreed rate |
| Fixed price per job | An outcome, whatever it takes |
| Retainer or service contract | A year of availability |
| Call-out with a minimum | A visit, with hours above a floor |
| Model | Fields it cannot work without |
|---|---|
| Time and materials | billed_ |
| Fixed price per job | quoted_ |
| Retainer | annual_ |
| Call-out | minimum_ |
Notice that only the first model has a meaningful hours field on the invoice. That is not a gap in the data, it is what the contract is about.
The tempting design is one cost column on the work order, filled in from whatever the invoice said.
| Model | What one cost column does to it |
|---|---|
| Time and materials | Survives. Cost really is hours x rate |
| Fixed price | Cost divided by hours invents a rate that was never agreed |
| Retainer | Included visits read 0.00, or the fee is counted twice |
| Call-out | Cost is not proportional to time, so any rate derived from it is false |
Three of the four break, and they break silently. Every row still holds a number, the report still adds up, and only the derived rate is nonsense.
The fix is not complicated. Keep commercial_model on the assignment, and refuse to compute a rate on any row whose model is not time and materials.
A service contract that includes a number of visits or hours a year is the model that misleads most confidently.
Take one real shape: a lifting gear inspection contract, agreement CON‑2026‑0412.
| Term | Value |
|---|---|
| Annual fee | 14,400.00 EUR |
| Included visits | 12 |
| Overage rate | 780.00 EUR per visit |
| Allocation rule | Fee spread evenly over included visits |
The allocation gives each included visit a booked cost of 14,400.00 divided by 12, which is 1,200.00 EUR.
That figure is what the plant's own cost per work order report will print against each of those twelve jobs.
| Visit | Marginal cost of calling them out |
|---|---|
| Visit 12 | 0.00 EUR. Already bought |
| Visit 13 | 780.00 EUR. Charged on top |
The marginal cost steps from zero to 780.00 EUR between two consecutive calls for identical work.
Now compare that 780.00 with the 1,200.00 the report prints per visit. The overage visit is 65.00% of the allocated cost of an included one, so the extra call is genuinely cheaper than the ones already paid for.
| Visits used | Total and average |
|---|---|
| 12 | 14,400.00 total, 1,200.00 each |
| 13 | 15,180.00 total, 1,167.69 each |
| 16 | 17,520.00 total, 1,095.00 each |
Going from 12 visits to 16 raises the bill by 3,120.00 EUR and lowers the average cost per visit by 105.00 EUR.
A manager watching average cost per visit sees an improving number while the invoice grows. That is not a reporting bug, it is what an average does to a fixed fee.
| Field | What it does |
|---|---|
| allowance_ | visits or hours. They behave differently |
| allowance_ | The number the contract bought |
| allowance_ | Counted in the current period only |
| allowance_ | Derived. The number a planner needs |
| period_ | When the counter last reset |
| period_ | When it resets next |
| rolls_ | Whether unused units survive the reset |
| overage_ | The price of the first call past the line |
allowance_remaining is the whole point of the table. Show it on the assignment screen and a planner knows, before dialling, whether this call is free at the margin or 780.00 EUR.
This is the record that joins a contractor to a job. It is not the work order and it is not the invoice line, it is the link between them.
The grain is one row per contractor per visit per work order. Two visits to finish one job are two rows.
| Field | Why it is mandatory |
|---|---|
| assignment_ | Own key. One row per visit |
| wo_ | The job the work belongs to |
| contractor_ | A real row, never a typed name |
| agreement_ | Which rate card applies to this visit |
| commercial_ | Copied from the agreement, frozen here |
| requested_ | When the plant decided it needed them |
| notified_ | When the plant actually told them |
| raised_ | The person who made the call |
requested_ts and notified_ts are two different moments, and keeping them apart is what stops a contractor being blamed for the plant's own delay.
A fault found at 07:20 and phoned through at 09:00 gave the contractor a commitment that started at 09:00. The 100 minutes before that belong to the plant.
| Field | Rule |
|---|---|
| arrival_ | From the gate or permit, not the report |
| departure_ | From the gate or permit |
| reported_ | What the service report claims |
| expected_ | Computed from the rate card, before the invoice |
| invoice_ | The invoice line, once it exists |
| billed_ | From the invoice. Often not reported_ |
| billed_ | What was actually charged |
| variance_ | matched, queried, accepted, credited |
| cost_ | invoice, accrual or allocation |
expected_cost is the field that separates a plant that manages contractors from a plant that pays them. It has to be written before the invoice arrives, or it is not an expectation, it is a copy.
cost_source carries the honesty. An accrual is a guess, an allocation is a policy, and an invoice is a fact, and a report that mixes all three should say so.
| Record | Join key and what it brings |
|---|---|
| Labour booking | wo_ |
| Contractor assignment | wo_ |
| The bridge | invoice_ |
Our labour model already carries invoice_ref, mandatory whenever the labour class is a contractor. That field is the pointer into this page's assignment row.
So the two join on wo_no for reporting, and on invoice_ref for reconciliation. Union the hours with cost_source attached, and never let a contractor row take its cost from a labour rate.
The one rule that matters: a contractor booking may carry hours, but its cost_amount must come from the invoice line, not from rate_snapshot.
This is the document the engineer hands over afterwards. It is evidence, and the only description of what was done.
| Field | What it holds |
|---|---|
| report_ | The contractor's own document number |
| assignment_ | The visit it describes |
| engineer_ | Who attended. Text is acceptable here |
| on_ | Claimed start. Compare with the gate |
| on_ | Claimed end. Compare with the gate |
| reported_ | Hours the report claims were worked |
| work_ | Free text, and the only failure narrative |
| parts_ | Their parts, with marks and serials |
| outcome | complete, partial, return required |
| accepted_ | The plant signature, with a timestamp |
parts_fitted is the field that pays for itself the first time a repair fails. Without it, nobody can tell whether a failed component was yours or theirs.
That is a warranty question, and it is answered by the claim record described in the warranty claim data model, not here.
The agreed rate and the invoiced rate disagree more often than anyone likes to admit. Most of the time neither side is behaving badly.
The gap is almost always between the contract and the plant's own expectation of the contract.
| Field | Where it comes from |
|---|---|
| expected_ | Rate card floor applied to reported hours |
| expected_ | The rate line for the trade and band |
| expected_ | Travel, call-out and markup lines |
| expected_ | The three above, added before the invoice |
| billed_ | The invoice |
| billed_ | The invoice |
| billed_ | The invoice |
| variance_ | billed minus expected |
| variance_ | A short coded list, not free text |
Calculate expected_cost before the invoice is opened, or it is not a control. Calculated afterwards, it is a rationalisation of whatever arrived.
One electrical call-out on agreement CON‑2026‑0412, invoice INV‑26‑4471.
| Term or event | Value |
|---|---|
| Standard labour rate | 62.00 EUR per hour |
| Minimum charge | 2.00 hours |
| Travel charge | 40.00 EUR per visit |
| Notified | 09:00 |
| Arrived at the gate | 15:40 |
| Left the gate | 17:05 |
| Reported hours worked | 1.20 |
The plant's planner wrote down an expectation of 1.20 hours at 62.00, which is 74.40 EUR.
The invoice charged the two hour minimum plus travel: 2.00 x 62.00 = 124.00, plus 40.00, which is 164.00 EUR.
| Line | Amount |
|---|---|
| Planner's expectation | 74.40 EUR |
| Invoiced | 164.00 EUR |
| Variance | 89.60 EUR |
| Variance against expectation | 120.43% |
Both charges were in the signed rate card. The minimum charge and the travel charge were agreed months earlier by somebody who no longer works in the plant.
The defect is not in the invoice. It is in expected_cost, which was computed as hours multiplied by rate instead of from the rate card.
| Expectation built how | Result and variance |
|---|---|
| Hours x rate | 74.40, variance 89.60 |
| Rate card, with floor and travel | 164.00, variance 0.00 |
A tolerance is a stored rule on the agreement header, not a mood.
Query the invoice when the absolute variance exceeds the greater of 25.00 EUR and 5.00% of expected_cost.
| Expectation | Threshold and verdict |
|---|---|
| 74.40 | 5% is 3.72, so 25.00 binds. 89.60 > 25.00, query |
| 164.00 | 5% is 8.20, so 8.20 binds. 0.00, no query |
The flat floor of 25.00 EUR stops a hundred small queries that cost more to raise than they recover. The percentage takes over on the large jobs, where 5% is worth an email.
The right outcome of this particular query is not a credit note. It is a correction to how expected_cost is computed, which removes the same false variance from every future call-out.
Every service agreement carries a response commitment, and most plants report against it using the contractor's own numbers.
That is avoidable. Several of these timestamps belong to the plant, and the ones that do are the only ones worth reporting.
| Timestamp | Whose record it comes from |
|---|---|
| requested_ | Plant. When the need was raised |
| notified_ | Plant. The call or the email out |
| acknowledged_ | Contractor, but into your inbox |
| arrival_ | Plant. Gate, permit or escort log |
| departure_ | Plant. Gate, permit or escort log |
| on_ | Contractor. The service report |
| on_ | Contractor. The service report |
The gate log is the underrated record here. A signing in book, a permit to work or a visitor badge system already holds arrival and departure, owned entirely by the plant.
| Measure | Formula |
|---|---|
| Actual response | arrival_ |
| Plant's own delay | notified_ |
| Time on site | departure_ |
| Response attainment | calls met divided by calls with both stamps |
| Billed against on site | billed_ |
Use the worked call-out above. Notified at 09:00 and arrived at 15:40 gives an actual response of 6.67 hours, inside an agreed 8.00.
Time on site is 15:40 to 17:05, which is 1.42 hours. Billed hours were 2.00, so billed against on site is 1.41.
That ratio above 1.00 is not fraud, it is the minimum charge doing exactly what it says. Tracking it tells you how much of your spend is floor rather than work.
| Quantity | Measurable without their word |
|---|---|
| Response time | Yes, if arrival comes from the gate |
| Plant's own delay | Yes, entirely yours |
| Time on site | Yes, from the gate log |
| Hours actually worked | No. Their service report |
| Hours billed | Yes, but only after the invoice |
| Wrench time on site | No, and no system will give it to you |
Say this plainly to anyone selling you contractor management: you cannot measure a contractor's wrench time, and neither can they.
Time on site is the honest proxy, and the gap between it and reported hours is the only signal you own. Our page on mean time to repair uses the same distinction between elapsed and worked time.
A packaging plant, calendar year 2026, four contractors on three different commercial models.
Money is rounded to two decimals, hours to two decimals, percentages to two decimals, and every total below is the sum of the rounded lines.
| Contractor and model | Work orders and cost |
|---|---|
| Mechanical, time and materials | 41 WOs, 23,896.00 |
| Lifting gear, retainer | 13 WOs, 15,180.00 |
| Electrical, call-out | 22 WOs, 4,445.00 |
| Gearbox, fixed price | 3 WOs, 13,750.00 |
| Total | 79 WOs, 57,271.00 |
| Line | Arithmetic |
|---|---|
| Mechanical | 318.0 h x 62.00 = 19,716.00, plus materials 4,180.00 |
| Lifting gear | 14,400.00 fee, plus 1 overage at 780.00 |
| Electrical | 57.5 h x 62.00 = 3,565.00, plus 22 travel at 40.00 |
| Gearbox | 6,900.00 plus 4,250.00 plus 2,600.00 |
Contractor cost per work order on this basis is 57,271.00 divided by 79, which is 724.95 EUR.
Invoices arrive after the work. Two adjustments move the ledger onto the date the job was finished.
| Adjustment | Amount and work orders |
|---|---|
| Invoice ledger, 2026 | 57,271.00, 79 WOs |
| Less 2025 work invoiced in 2026 | 4,860.00, 7 WOs |
| Plus 2026 work invoiced in 2027 | 9,740.00, 4 WOs |
| Completion date basis | 62,151.00, 76 WOs |
57,271.00 minus 4,860.00 is 52,411.00, and 52,411.00 plus 9,740.00 is 62,151.00 EUR.
The work order count moves the other way, because the four jobs added are fewer than the seven removed: 79 minus 7 plus 4 is 76.
| Basis | Cost per work order |
|---|---|
| By invoice date | 57,271.00 / 79 = 724.95 |
| By completion date | 62,151.00 / 76 = 817.78 |
The gap is 92.83 EUR per work order, which is 12.81% of the invoice date figure. The underlying rows are identical.
Put the same cut as a share of the maintenance budget and it moves too. With internal labour of 214,600.00 and parts of 88,120.00, the two totals are 359,991.00 and 364,871.00.
| Basis | Contractor share |
|---|---|
| By invoice date | 57,271.00 / 359,991.00 = 15.91% |
| By completion date | 62,151.00 / 364,871.00 = 17.03% |
That is a difference of 1.12 percentage points in the headline everyone quotes. Neither basis is wrong, and a plant that switches between them between years has published a trend that did not happen.
The second defensible choice is whether the annual fee is a work order cost at all.
| Treatment of the 14,400.00 fee | Cost per work order |
|---|---|
| Allocated to the visits | 57,271.00 / 79 = 724.95 |
| Held as a fixed plant cost | 42,871.00 / 79 = 542.67 |
57,271.00 minus 14,400.00 is 42,871.00 EUR, spread over the same 79 work orders.
The gap is 182.28 EUR per work order, or 25.14% of the allocated figure, and it comes entirely from a policy choice about one fee.
| Source of hours | Hours |
|---|---|
| Mechanical, invoiced | 318.0 |
| Electrical, invoiced | 57.5 |
| Lifting gear, service reports | 46.5 |
| Gearbox, service reports | 88.0 |
| Total | 510.0 |
Invoice backed hours are 318.0 plus 57.5, which is 375.5, and that is 375.5 divided by 510.0, or 73.63% of the total.
The remaining 26.37% exists only on a document the contractor wrote, because the contract for those jobs never priced an hour.
| Figure | Value and meaning |
|---|---|
| Blended cost per hour | 57,271.00 / 510.0 = 112.30 |
| Contracted labour rate | 62.00, from the rate card |
The 112.30 is not a rate and must never be compared with an internal one. It contains materials of 4,180.00, travel of 880.00, the 15,180.00 retainer and 13,750.00 of fixed price work.
Strip those four and you get 23,281.00 over 375.5 invoice backed hours, which is exactly 62.00 EUR per hour, the contracted rate.
Against the internal fully loaded rate of 34.20 used in our labour data model, that contracted 62.00 is 1.81 times the cost of an in house hour.
| Input | Count |
|---|---|
| Contractor work orders | 79 |
| With both notified and arrival stamps | 61 |
| Of those, met the commitment | 54 |
| Denominator chosen | Result |
|---|---|
| Calls with both stamps | 54 / 61 = 88.52% |
| All calls, unstamped counted as met | 72 / 79 = 91.14% |
| All calls, unstamped counted as missed | 54 / 79 = 68.35% |
The same 54 calls answered on time produce a spread of 22.79 percentage points, decided entirely by what to do with 18 missing timestamps.
The defensible figure is the first one, 88.52%, printed next to its denominator of 61 and the count of 18 calls that could not be assessed.
| Input | Status |
|---|---|
| 2026 work invoiced in 2027 | Assumption. An accrual from job confirmations |
| 46.5 and 88.0 report hours | Assumption. The contractor's own document |
| Even fee allocation | Policy choice. Any other rule moves every row |
| 18 calls treated as unassessed | Judgement. Two other treatments are shown |
| Internal labour and parts | From finance, not contractor data |
Print that list under the number. A cost per work order with its assumptions attached survives a challenge, and one without them does not.
Question one is the quickest disqualifier. A tool that can only express contractor cost as hours multiplied by a rate cannot hold three of the four models above (equipment maintenance software).
Fabrico is an OEE platform with a full CMMS built in, and it is where the work order that a contractor attends is created and confirmed by your team.
Work orders carry task level detail, an auto-timer and allocation with an over-allocation warning above eight hours. Your own team books time from the iOS, Android or web app, with QR scanners, work shifts, a personal working card and approval workflows.
Around the job you get notifications with smart rules, an audit log and analytics including task distribution, per-employee productivity, MTTR and MTBF. Parts consumption is booked against the work order.
Be clear about what Fabrico is not. Fabrico has no contractor record, no rate card, no service agreement, no vendor portal and no invoice matching, and a contractor does not log into Fabrico.
Suppliers exist as a listing only, with no supplier communications. The contractor records specified on this page belong in your purchasing or finance system.
Fabrico gives you the other side of the join: the work order, the asset, the dates, the internal hours and the parts, exported through Excel, the REST API, webhooks or the SAP and other ERP connectors.
Want to see how your work orders would carry the plant side of this model? Book a 30 minute demo with a Fabrico consultant, no commitment, or contact us with your questions.
Create it with assignment_id, wo_no, contractor_id, agreement_id, commercial_model, requested_ts, notified_ts and raised_by. Close it with arrival_ts, departure_ts, reported_hours, expected_cost, invoice_ref, billed_hours, billed_amount, variance_status and cost_source.
It works only for time and materials, where cost really is hours multiplied by a rate. For fixed price, a retainer and a call-out minimum, any rate derived from it is false, because the contract never priced an hour.
Reporting joins them on wo_no, reconciliation joins them on invoice_ref, and cost_source records whether a line came from a booking or an invoice. A contractor line takes its cost from the invoice, never an internal rate.
Nothing at the margin until the allowance runs out, then the overage rate applies in full. Visit 12 costs 0.00 EUR and visit 13 costs 780.00 EUR, against an allocated 1,200.00 EUR per included visit.
Notification, arrival and departure, because all three sit in the plant's own call log and gate record. Hours actually worked and wrench time on site can only be read off the contractor's service report.