The job margin tracker: find out which jobs actually made you money
You know the year made money, but not which jobs made it, so you keep pricing from memory. Every job gets one short code on the hours, the supplier bills and the invoice. Each night the three are added up per job, so you see the real margin.
One job code on everything, and a screen that puts your worst-priced work at the top
Paste it into your AI assistant — ChatGPT, Claude, Gemini, whichever you use. It first asks what tools you already have, then rebuilds this blueprint to fit them.
- A job code written on the work before it starts - if it is assigned afterwards from memory, the numbers will be wrong and look right
- Time recorded per job by whoever does the work, daily; a weekly reconstruction is a guess
- A loaded internal hourly cost per person: annual employer cost divided by the hours they are actually on jobs, not their gross wage
- Supplier invoices booked against the job, not just against the supplier
- About 4 GB of RAM on the box - Metabase runs on the JVM and will swap on 1 GB
- One overhead figure you are willing to defend: last year's costs that belong to no job, divided by total direct job hours
Version history
Your accounts tell you the year made money. They do not tell you which jobs made it, and which ones your good jobs paid for. This one closes that gap with tools you may already be running.
The architecture
the same flow as text
│
┌─────┴──────────────┬────────────────────────┐
▼ ▼ ▼
[ Kimai ] [ Invoice Ninja ] [ Paperless-ngx ]
hours × internal project + expenses supplier PDF with a
rate = labour cost + customer invoice job_code custom field
│ │ │
└─────┬──────────────┴────────────────────────┘
▼
[ n8n — nightly collector ] three API pulls, one upsert
▼
[ PostgreSQL — table job_facts, one row per job ]
│ + overhead_rate table (€ per direct hour)
▼
[ Metabase — one screen, sorted worst margin first ]
Untagged spend gets its own row. If that row is big, the rest is fiction.
The whole blueprint is one rule: one short job code, written on everything, before the work starts. Kimai knows what an hour costs you. Invoice Ninja knows what you charged and what you bought. The nightly job puts the two next to each other. Nothing here is clever, and that is the point — the moment the code is optional, the numbers stop being true.
Paste it into your AI assistant — ChatGPT, Claude, Gemini, whichever you use. It first asks what tools you already have, then rebuilds this blueprint to fit them.
The problem
Revenue is recorded per job. You invoiced it, so it has a number and a name. The two biggest costs are not recorded per job at all: the hours end up on a paper timesheet or in someone's head, and the materials end up on a supplier invoice that names the supplier, not the job. At month end you have a bank balance and a feeling.
That gap has one expensive consequence: you keep quoting from memory, and memory keeps the jobs that went smoothly. The one with the second visit, the extra trip to the wholesaler and the hour on the phone blurs into the average. So the price stays where it was, and the job that loses money gets quoted again next month at the same price.
It is the same reason we sub-meter a building. The total bill tells you it is expensive. The sub-meter tells you it is the ventilation. The job-management platforms that give you the sub-meter are priced per user per month, which is the wrong shape when half your team touches the system twice a week, and they hold the numbers you would want to keep for ten years.
Tool choices — and why
po_number field, or in one of the four custom fields, and expenses book straight against the project. Licence in plain terms: free to run for your own business, you just may not resell it to others as a hosted service.job_code custom field and its API will filter on it directly. If you do not, book the supplier invoice as an Invoice Ninja expense and skip this box — one place for costs beats two.Setup outline
- Fix the job code format first: short, sortable, spoken out loud without confusion.
2608-014is year, month, sequence. Write the rule where the work is booked in. Working when: two different people quote the same job the same way. - Kimai: one project per job, named with the code. Set each person's internal rate to their loaded cost, not their gross wage. Working when: a 4-hour entry shows both a customer value and a lower-or-higher internal cost.
- Invoice Ninja: matching project, job code in
po_numberon the invoice, supplier invoices booked as expenses against that project. Working when: the project page shows a spend total you recognise. - Paperless-ngx, if you use it: add the
job_codecustom field and fill it at intake, not later. - n8n, nightly: pull Kimai timesheets, Invoice Ninja invoices and expenses, and Paperless documents. Upsert one row per job into
job_facts— code, revenue, labour cost, material cost, hours. - Overhead: one small table, month and € per direct hour. Take last year's costs that belong to no job (rent, insurance, software, accountant, your own admin time), divide by total direct job hours, recompute once a quarter.
- Metabase: one question. Job, revenue, labour, materials, overhead, margin in € and in %. Sort ascending on margin %, so the losses are at the top where you cannot miss them. Pin it.
- Pilot on one job type for two weeks before you tag everything. Working when: you can explain every number on the worst job without opening a second tool.
Pitfalls — what goes wrong when you build this
Verified repos
Kimai — AGPL-3.0, 4.9k stars, release 2.63.0 (03/08/2026)
Invoice Ninja — Elastic License 2.0, 10.0k stars, v5.13.30 (06/08/2026)
Metabase — AGPL-3.0 outside the enterprise directory, 48.6k stars, v0.63.2 (29/07/2026)
Grafana — AGPL-3.0, 76k stars, active (August 2026)
Paperless-ngx — GPL-3.0, 44k stars, active (August 2026)
n8n — Sustainable Use License (free self-hosted internal use), 200k stars, active (August 2026)