---
title: "The job margin tracker: find out which jobs actually made you money"
subtitle: "One job code on everything, and a screen that puts your worst-priced work at the top"
url: "https://rxed.ai/blueprints/job-margin-tracker.html"
id: "job-margin-tracker"
version: "1.0"
published: "2026-08-07"
kind: "workflow"
setup_effort: "A project"
setup_hours: "10-20"
running_cost_usd_per_month: "8-15"
paid_software_usd_per_month: "0"
time_saved: "saves ~0.5-0.75 h/week unattended at 20-40 jobs a month"
time_saved_basis: "rebuilding a job-cost spreadsheet at month end from timesheets and supplier invoices, 2-3 hours, and usually abandoned by month three"
runs_by: "unattended"
data_location: "your-server"
data_location_note: "Hours, rates, supplier costs and customer invoices all stay in your own containers; nothing leaves the box."
first_to_break: "the tagging discipline, not the software - a week of untagged hours or an untagged supplier invoice"
failure_signal: "silent"
problem: "You know the year made money, but not which jobs made it. So you keep pricing new work from memory, and memory keeps the jobs that went smoothly."
fix: "Every job gets one short code that goes on the hours, the supplier bills and the customer invoice. Each night those three are added up per job, so one screen shows what you earned, what it cost and what was left."
tools: ["Kimai", "Invoice Ninja", "Metabase", "n8n", "PostgreSQL", "Docker"]
tech: ["Kimai", "Invoice Ninja", "Metabase", "n8n"]
repos:
  - name: "Kimai"
    url: "https://github.com/kimai/kimai"
  - name: "Invoice Ninja"
    url: "https://github.com/invoiceninja/invoiceninja"
  - name: "Metabase"
    url: "https://github.com/metabase/metabase"
  - name: "Grafana"
    url: "https://github.com/grafana/grafana"
  - name: "Paperless-ngx"
    url: "https://github.com/paperless-ngx/paperless-ngx"
  - name: "n8n"
    url: "https://github.com/n8n-io/n8n"
categories: ["Reporting", "Dashboards", "Bookkeeping", "Invoicing", "Self-hosted", "n8n"]
source: "RXed AI News — https://rxed.ai"
---
# The job margin tracker: find out which jobs actually made you money

> One job code on everything, and a screen that puts your worst-priced work at the top

## Before you start

- 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

## What breaks first

the tagging discipline, not the software - a week of untagged hours or an untagged supplier invoice. Missing cost does not read as missing, it reads as margin. Put the untagged total on the dashboard and check it before you trust a single job number.

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

 

### The architecture

 
 [ ONE JOB CODE — 2608-014 ]  created once, written on everything 
         │ 
   ┌─────┴──────────────┬────────────────────────┐ 
   ▼                    ▼                        ▼ 
 [ 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.

 

### Tool choices — and why

 Kimai (AGPL-3.0, 4.9k stars, release 2.63.0 on 03/08/2026) is the only piece in the stack that knows what your labour actually costs. Its documentation is explicit about the two-rate model: the hourly price is "what actually goes on an invoice", the internal price is "what your employees cost you", and both are stored on every time record. Rates can be set per user, per customer, per project or per activity. That second number is the whole reason Kimai is here rather than any other timer.

 Invoice Ninja (Elastic License 2.0, 10.0k stars, v5.13.30 on 06/08/2026) carries the money side: projects, expenses, vendors and purchase orders, and the customer invoice, all behind one REST API. The job code lives in the invoice's 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.

 Paperless-ngx (GPL-3.0, 44k stars, pushed 07/08/2026) is optional. If you already run it for receipt intake, give it a 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.

 Metabase (AGPL outside the enterprise directory, 48.6k stars, v0.63.2 on 29/07/2026) is the screen. Be honest about what it costs in RAM: Metabase's own production guide asks for 1 core and 1 GB as a baseline, plus 1 core and 2 GB for every 20 concurrent users, and it runs on the JVM. On a 1 GB box it will swap and annoy you. Budget 4 GB for the whole stack. Grafana (AGPL-3.0, 76k stars) is the lighter alternative if the box is small — you write the SQL yourself instead of clicking it together.

 n8n (Sustainable Use License, free for internal business use, 200k stars) runs the nightly collection. If you already have it doing invoice chasing or receipt intake, this is one more workflow, not one more server.

 Monthly cost. Software €0 across the stack. Hosting: a 4 GB VPS is roughly €8–15/month at current European list prices, and it runs everything above. If Invoice Ninja and n8n are already on that box, the marginal cost of this blueprint is the RAM for Metabase and nothing else.

 

### Setup outline

 1. Fix the job code format first: short, sortable, spoken out loud without confusion. 2608-014 is year, month, sequence. Write the rule where the work is booked in. Working when: two different people quote the same job the same way. 
 2. 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. 
 3. Invoice Ninja: matching project, job code in po_number on the invoice, supplier invoices booked as expenses against that project. Working when: the project page shows a spend total you recognise. 
 4. Paperless-ngx, if you use it: add the job_code custom field and fill it at intake, not later. 
 5. 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. 
 6. 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. 
 7. 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. 
 8. 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 — the real ones

 Untagged cost does not look like a gap, it looks like profit. Your first month will show a handful of jobs at 70% margin. Those are the ones where nobody logged the hours. Put a "no job code" row on the dashboard and read it before anything else: if it holds 20% of the spend, every margin on that screen is wrong by an unknown amount. 
 Gross wage is not what an hour costs you. Employer contributions, holiday pay, sick days, the van, the tools, the phone. Use annual employer cost divided by the hours that person is actually on jobs — around 1,600 a year for a full-timer, not 1,800. Get this wrong and every job reads 25–40% healthier than it is, which is worse than not measuring at all. 
 Do not backfill. Start on the next job. Reconstructing last year from memory produces numbers that feel authoritative and are not, and it is where these projects die before they produce anything. 
 A dashboard you have to remember to open is a dashboard you forget. Have n8n mail you the five worst-margin jobs every Monday morning. The screen is for the follow-up question, not the alert. 
 Margin is not cash. A 30% job invoiced 90 days ago has not paid anyone's wages yet. Different question, different blueprint. 
 Your team will read it as surveillance unless you say otherwise. Report by job, never by person, and say plainly that it prices work, not people. In the EU, time records are personal data: keep them only as long as you need them, and tell staff what is collected.

 

### Verified repos

 
 [Kimai](https://github.com/kimai/kimai) — AGPL-3.0, 4.9k stars, release 2.63.0 (03/08/2026) 
 [Invoice Ninja](https://github.com/invoiceninja/invoiceninja) — Elastic License 2.0, 10.0k stars, v5.13.30 (06/08/2026) 
 [Metabase](https://github.com/metabase/metabase) — AGPL-3.0 outside the enterprise directory, 48.6k stars, v0.63.2 (29/07/2026) 
 [Grafana](https://github.com/grafana/grafana) — AGPL-3.0, 76k stars, active (August 2026) 
 [Paperless-ngx](https://github.com/paperless-ngx/paperless-ngx) — GPL-3.0, 44k stars, active (August 2026) 
 [n8n](https://github.com/n8n-io/n8n) — Sustainable Use License (free self-hosted internal use), 200k stars, active (August 2026)
 

 
 Get the next blueprint as it publishes — free, practical, verified tooling only.

 [Subscribe](https://subscribe.rxed.ai) · Want it audited or extended for your business? DM [@RXed_EU](https://x.com/RXed_EU)
