Menu
Contractor Work Data Model: Fields, Rates and Real Cost

Contractor Work Data Model: Fields, Rates and Real Cost

The fields a plant needs for contractor work: rate card, assignment, service report, four commercial models and two defensible costs per work order.
Contractor Work Data Model: Fields, Rates and Real Cost

Key takeaways

  • For an internal technician you own the time record and derive the cost. For a contractor you receive the cost and have to reconstruct the time.
  • The same 79 contractor work orders give a cost per work order of 724.95 or 817.78 EUR, a gap of 92.83 EUR, or 12.81%, with nothing changed on the floor.
  • A service contract's thirteenth visit costs 780.00 EUR where the plant's own report prices a visit at 1,200.00 EUR.
  • An invoice of 164.00 EUR against an expectation of 74.40 EUR was 120.43% over, and every charge in it was inside the contract.
  • One response commitment and the same 54 calls answered on time give 88.52%, 91.14% or 68.35%, depending only on the denominator.

What this page covers that our other contractor pages do not

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.

The asymmetry that shapes every contractor record

Internal techni­cianCon­trac­tor
You own the time recordYou receive the cost
Cost is derived: hours x rateHours must be recon­structed
Written the same dayArrives weeks later
Complete to the hourComplete to the euro
Rate is a finance inputRate is a nego­tiated 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.

What it does to any cost per asset number

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 numberWhat it is accurate about
Internal labourThe hours, and the day they happened
Con­trac­tor spendThe 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.

The contractor record, field by field

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.

Mandatory when the contractor is created

FieldWhy it is manda­tory
con­tractor_idStable key, never the trading name
legal_nameThe entity the invoice comes from
trade_codesWhat they are allowed to be called for
statusapproved, condi­tional, sus­pended
owner_idThe person inside the plant who owns it
active_fromFirst day work may be assigned
active_toLast 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.

Commercial and compliance fields

FieldWhat it holds
default_agree­ment_idThe rate card used when none is named
pur­chase_vendor_idThe same entity in purchasing
insur­ance_expiryDrives the status, not a reminder
cer­tif­icate_refsCompe­tency and safety documents
induc­tion_valid_toSite induc­tion, per entity or person
pay­ment_termsDays. Explains an invoice date lag
cur­rencyNever 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.

The service agreement and its rate card

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.

The agreement header

FieldWhat it decides
agree­ment_idKey, printed on every assign­ment
con­tractor_idWho the agreement is with
com­mer­cial_modelThe four values in the next section
valid_fromA rate card has a start date
valid_toNull while it is current
response_hoursThe agreed commit­ment
response_clockcalendar or contracted hours
tol­erance_ruleWhen 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.

The rate line

FieldWhat it holds
rate_line_idOne row per priced thing
rate_typelabour, travel, mate­rial, call-out
trade_codeFitter, elec­trician, inspec­tor
bandnormal, out of hours, weekend
unithour, visit, job, km, each
rate_amountThe agreed price per unit
min­imum_unitsThe floor the invoice charges
mate­rial_markupMulti­plier 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.

Four commercial models, four different records

Almost every contractor arrangement is one of four shapes. They price completely different objects, and a record that fits one fits the others badly.

ModelWhat is actually priced
Time and mate­rialsHours, at an agreed rate
Fixed price per jobAn outcome, whatever it takes
Retainer or service con­tractA year of avail­ability
Call-out with a min­imumA visit, with hours above a floor

What each model needs on the record

ModelFields it cannot work without
Time and mate­rialsbilled_hours, rate_amount, band, mate­rial_lines
Fixed price per jobquoted_amount, scope_ref, var­iation_lines
Retainerannual_fee, allow­ance_units, over­age_rate, allo­cation_rule
Call-outmin­imum_units, travel_charge, billed_hours

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.

Why a single cost column destroys three of the four

The tempting design is one cost column on the work order, filled in from whatever the invoice said.

ModelWhat one cost column does to it
Time and mate­rialsSurvives. Cost really is hours x rate
Fixed priceCost divided by hours invents a rate that was never agreed
RetainerIncluded visits read 0.00, or the fee is counted twice
Call-outCost is not propor­tional 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.

The allowance trap, worked with numbers

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.

TermValue
Annual fee14,400.00 EUR
Included visits12
Over­age rate780.00 EUR per visit
Allo­cation ruleFee 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 12 and visit 13

VisitMarginal cost of calling them out
Visit 120.00 EUR. Already bought
Visit 13780.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.

The average that falls while the bill rises

Visits usedTotal and average
1214,400.00 total, 1,200.00 each
1315,180.00 total, 1,167.69 each
1617,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.

The field that tells a planner which side of the line the next call falls on

FieldWhat it does
allow­ance_unitvisits or hours. They behave differently
allow­ance_includedThe number the contract bought
allow­ance_consumedCounted in the current period only
allow­ance_remain­ingDerived. The number a planner needs
period_startWhen the counter last reset
period_endWhen it resets next
rolls_overWhether unused units survive the reset
over­age_rateThe 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.

The contractor work assignment

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.

Mandatory when the assignment is created

FieldWhy it is manda­tory
assign­ment_idOwn key. One row per visit
wo_noThe job the work belongs to
con­tractor_idA real row, never a typed name
agree­ment_idWhich rate card applies to this visit
com­mer­cial_modelCopied from the agree­ment, frozen here
requested_tsWhen the plant decided it needed them
notified_tsWhen the plant actually told them
raised_byThe 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.

Written when the visit closes

FieldRule
arrival_tsFrom the gate or permit, not the report
depar­ture_tsFrom the gate or permit
reported_hoursWhat the service report claims
expected_costComputed from the rate card, before the invoice
invoice_refThe invoice line, once it exists
billed_hoursFrom the invoice. Often not reported_hours
billed_amountWhat was actually charged
var­iance_statusmatched, queried, accepted, credited
cost_sourceinvoice, accrual or allo­cation

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.

How this record joins the internal labour booking

RecordJoin key and what it brings
Labour bookingwo_no. Hours, person, activity, derived cost
Con­trac­tor assign­mentwo_no. Visit, invoice, billed cost
The bridgeinvoice_ref, present on both

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.

The service report the contractor returns

This is the document the engineer hands over afterwards. It is evidence, and the only description of what was done.

FieldWhat it holds
report_refThe contractor's own document number
assign­ment_idThe visit it describes
engi­neer_namesWho attended. Text is acceptable here
on_site_fromClaimed start. Compare with the gate
on_site_toClaimed end. Compare with the gate
reported_hoursHours the report claims were worked
work_doneFree text, and the only failure narrative
parts_fittedTheir parts, with marks and serials
outcomecomplete, partial, return required
accepted_byThe 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.

Rate card against invoice

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.

The comparison fields

FieldWhere it comes from
expected_hoursRate card floor applied to reported hours
expected_rateThe rate line for the trade and band
expected_extrasTravel, call-out and markup lines
expected_costThe three above, added before the invoice
billed_hoursThe invoice
billed_rateThe invoice
billed_amountThe invoice
var­iance_amountbilled minus expected
var­iance_reasonA 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.

A worked case that was entirely inside the contract

One electrical call-out on agreement CON‑2026‑0412, invoice INV‑26‑4471.

Term or eventValue
Standard labour rate62.00 EUR per hour
Min­imum charge2.00 hours
Travel charge40.00 EUR per visit
Notified09:00
Arrived at the gate15:40
Left the gate17:05
Reported hours worked1.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.

LineAmount
Planner's expec­tation74.40 EUR
Invoiced164.00 EUR
Var­iance89.60 EUR
Var­iance against expec­tation120.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.

Expec­tation built howResult and var­iance
Hours x rate74.40, var­iance 89.60
Rate card, with floor and travel164.00, var­iance 0.00

The tolerance rule that decides whether it is queried

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.

Expec­tationThresh­old and verdict
74.405% is 3.72, so 25.00 binds. 89.60 > 25.00, query
164.005% 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.

Response and completion commitments

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.

The timestamps and who owns them

TimestampWhose record it comes from
requested_tsPlant. When the need was raised
notified_tsPlant. The call or the email out
acknowl­edged_tsCon­trac­tor, but into your inbox
arrival_tsPlant. Gate, permit or escort log
depar­ture_tsPlant. Gate, permit or escort log
on_site_fromCon­trac­tor. The service report
on_site_toCon­trac­tor. 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.

The formulas and their denominators

MeasureFormula
Actual responsearrival_ts minus notified_ts
Plant's own delaynotified_ts minus requested_ts
Time on sitedepar­ture_ts minus arrival_ts
Response attain­mentcalls met divided by calls with both stamps
Billed against on sitebilled_hours divided by time on site

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.

What is honestly measurable from a plant's own records

QuantityMeasur­able without their word
Response timeYes, if arrival comes from the gate
Plant's own delayYes, entirely yours
Time on siteYes, from the gate log
Hours actually workedNo. Their service report
Hours billedYes, but only after the invoice
Wrench time on siteNo, 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.

Worked example: one year, one plant, four contractors

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.

1. The invoice ledger as finance holds it

Con­trac­tor and modelWork orders and cost
Mechan­ical, time and mate­rials41 WOs, 23,896.00
Lifting gear, retainer13 WOs, 15,180.00
Elec­trical, call-out22 WOs, 4,445.00
Gearbox, fixed price3 WOs, 13,750.00
Total79 WOs, 57,271.00
LineArith­metic
Mechan­ical318.0 h x 62.00 = 19,716.00, plus mate­rials 4,180.00
Lifting gear14,400.00 fee, plus 1 over­age at 780.00
Elec­trical57.5 h x 62.00 = 3,565.00, plus 22 travel at 40.00
Gearbox6,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.

2. The same rows, cut by completion date instead

Invoices arrive after the work. Two adjustments move the ledger onto the date the job was finished.

Adjust­mentAmount and work orders
Invoice ledger, 202657,271.00, 79 WOs
Less 2025 work invoiced in 20264,860.00, 7 WOs
Plus 2026 work invoiced in 20279,740.00, 4 WOs
Com­ple­tion date basis62,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.

BasisCost per work order
By invoice date57,271.00 / 79 = 724.95
By com­ple­tion date62,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.

BasisCon­trac­tor share
By invoice date57,271.00 / 359,991.00 = 15.91%
By com­ple­tion date62,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.

3. The retainer cut, on the same invoice date rows

The second defensible choice is whether the annual fee is a work order cost at all.

Treat­ment of the 14,400.00 feeCost per work order
Allo­cated to the visits57,271.00 / 79 = 724.95
Held as a fixed plant cost42,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.

4. Hours you can check and hours you cannot

Source of hoursHours
Mechan­ical, invoiced318.0
Elec­trical, invoiced57.5
Lifting gear, service reports46.5
Gearbox, service reports88.0
Total510.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.

FigureValue and meaning
Blended cost per hour57,271.00 / 510.0 = 112.30
Con­tracted labour rate62.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.

5. Response attainment, three honest answers

InputCount
Con­trac­tor work orders79
With both notified and arrival stamps61
Of those, met the commit­ment54
Denom­inator chosenResult
Calls with both stamps54 / 61 = 88.52%
All calls, unstamped counted as met72 / 79 = 91.14%
All calls, unstamped counted as missed54 / 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.

6. Which inputs are assumptions

InputStatus
2026 work invoiced in 2027Assump­tion. An accrual from job confir­mations
46.5 and 88.0 report hoursAssump­tion. The con­trac­tor's own document
Even fee allo­cationPolicy choice. Any other rule moves every row
18 calls treated as unas­sessedJudge­ment. Two other treat­ments are shown
Internal labour and partsFrom finance, not con­trac­tor 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.

Five questions to ask a vendor about contractor data

  1. Can a work order carry a contractor assignment that is not a labour booking? Ask them to put a fixed price job on a work order with no hours, then show the cost per work order report.
  2. Where does the rate card live, and is the applicable rate frozen on the assignment? Ask what happens to a job closed last March when the contractor raises rates in April.
  3. Can the system hold an expected cost before the invoice arrives? Ask to see the variance between expected and billed, and the coded reason list behind it.
  4. Is arrival time taken from your gate record or from the contractor's report? Ask which field their response attainment report divides by, and how many calls it silently excludes.
  5. How is a service contract allowance modelled? Ask them to show the remaining included visits on the screen a planner uses to raise the call.

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).

How Fabrico helps

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.

Frequently asked questions

What fields belong on a contractor work assignment?

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.

Why can a single cost column not hold contractor cost?

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.

How do contractor records join the internal labour model?

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.

What does an included visit in a service contract really cost?

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.

Which contractor response times can a plant measure on its own?

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.

Dernières nouvelles de notre blog

Définissez votre feuille de route en matière de fiabilité
Validez votre retour sur investissement potentiel : réservez une démonstration en direct
Définissez votre feuille de route en matière de fiabilité
En cliquant sur le bouton Accepter, vous donnez votre consentement à l'utilisation de cookies lors de l'accès à ce site Web et de l'utilisation de nos services. Pour en savoir plus pour en savoir plus sur la manière dont les cookies sont utilisés et gérés, veuillez consulter notre Politique de confidentialité et Déclaration relative aux cookies