Hour types and rule packs
How approved minutes are split into payroll hour types (normal, overtime, ORT bands, holidays) by a JSON rule pack, which compliance warnings it raises, and the full list of hour-type codes.
Last updated: · Markdown version
At export time, TimeTracking turns approved days into pay lines. A pay line is person × date × hour type × minutes (and an amount for allowances). The org's rule pack decides the hour types. The export mapping then turns pay lines into the payroll file.
The rule engine is deterministic: the output depends only on the input days and the rule pack version.
Hour-type codes
| Code | Produced by | Notes |
|---|---|---|
normal |
every pack | Minutes not taken by a band, holiday or overtime rule |
overtime_125, overtime_150, … |
dailyOvertime / weeklyOvertime rules |
Name is free ([a-z0-9_]{1,40}) |
ort_evening, ort_night, ort_saturday, ort_sunday |
NL basic pack bands | ORT = irregular hours |
night, sunday |
DE basic pack bands | — |
holiday |
holidayHourType |
Minutes on public holidays (NL, DE, BE calendars) |
allowance_<code> |
placement components with unit per_day |
e.g. allowance_travel; minutes 0, amountCents set |
expense_km, expense_travel_time, expense_public_transport, expense_parking, expense_meal, expense_other |
approved expenses | travel_time carries minutes; the others carry amountCents |
leave_vacation, leave_sick, leave_special, leave_unpaid, leave_<code> |
approved leave | minutes_per_day on each working day (Mon–Fri, excluding public holidays) |
Built-in packs
id |
Name | Behaviour |
|---|---|---|
plain |
Geen toeslagen (alles normaal) | Everything is normal. The default. |
nl-basic |
NL basis (voorbeeld — afstemmen op CAO) | NL holidays → holiday; Sun → ort_sunday; Sat → ort_saturday; Mon–Fri 00:00–06:00 → ort_night; Mon–Fri 20:00–24:00 → ort_evening; weekly overtime after 40 h → overtime_125; ATW breaks (>5.5 h → 30 min, >10 h → 45 min); 11 h rest; max 12 h shift |
de-basic |
DE Basis (Beispiel — an Tarifvertrag anpassen) | DE holidays; Sun → sunday; nights (Mon–Sat 00:00–06:00 and 23:00–24:00) → night; daily overtime after 8 h → overtime_125; ArbZG breaks (>6 h → 30 min, >9 h → 45 min); 11 h rest; max 10 h |
The NL and DE packs are examples. Align thresholds and bands with your CAO or Tarifvertrag. An admin picks a pack, or pastes a custom JSON pack, in "Regels & koppelingen" → "Looncomponenten en toeslagen". Custom packs are validated before saving. There is no API for rule packs.
Rule pack JSON schema
{
"id": "acme-cao",
"name": "ACME CAO 2026",
"version": 1,
"holidays": "NL",
"includeOptionalHolidays": false,
"holidayHourType": "holiday",
"bands": [
{ "hourType": "ort_sunday", "days": [7], "from": 0, "to": 1440 },
{ "hourType": "ort_night", "days": [1, 2, 3, 4, 5], "from": 0, "to": 360 }
],
"dailyOvertime": [{ "afterMin": 540, "hourType": "overtime_125" }],
"weeklyOvertime": [{ "afterMin": 2400, "hourType": "overtime_125" }, { "afterMin": 2880, "hourType": "overtime_150" }],
"breakRules": [{ "workedOverMin": 330, "minBreakMin": 30 }, { "workedOverMin": 600, "minBreakMin": 45 }],
"minDailyRestMin": 660,
"maxShiftMin": 720
}
| Field | Type | Rules |
|---|---|---|
id, name |
string | required |
version |
integer | required |
holidays |
NL | DE | BE | none |
required |
includeOptionalHolidays |
boolean | optional |
holidayHourType |
[a-z0-9_]{1,40} |
optional. Without it, holidays are not special |
bands[] |
{hourType, days: 1..7 (ISO weekday), from, to} |
minutes [from, to), to ≤ 1440, to > from. The first matching band wins |
dailyOvertime[], weeklyOvertime[] |
{afterMin, hourType} |
afterMin ≤ 10080 |
breakRules[] |
{workedOverMin, minBreakMin} |
warnings only |
minDailyRestMin, maxShiftMin |
integer minutes | warnings only |
How the split works
- Classify each day minute by minute. Holiday first, then the first matching band, else
normal. Minutes after midnight use the next day's weekday and holiday status. When only a total is known (no start/end), the whole day gets the holiday type, a full-day band (from: 0, to: 1440), ornormal. - Deduct breaks. Breaks are unpaid. Without exact break times, they come out of
normalminutes first, then out of the largest other type. Surcharge minutes are therefore never inflated. - Daily overtime moves
normalminutes above the threshold. The highest threshold applies first. - Weekly overtime works per placement and ISO week, chronologically, on the remaining
normalminutes. The engine loads the whole week even when the export range starts mid-week. - Warnings (pay is never changed):
break_too_short,rest_too_short,shift_too_long.
Warnings appear in the export preview and are stored on the export run. Captured example:
{ "key": "00000000-0000-4000-8000-0000000000e4", "date": "2026-12-02", "code": "break_too_short", "message": "Break 0 min, at least 30 min required", "person": "Sanne Janssen" }
Warnings never block a commit. Validation issues from the export mapping do.
Mapping hour types to payroll codes
Payroll systems use their own wage codes (looncomponenten / Lohnarten). Translate hour types with a map transform and a code table in the export mapping:
{ "header": "Looncomponent", "source": "hourType", "transform": { "kind": "map", "table": "wageCodes", "fallback": "" } }
"tables": { "wageCodes": { "normal": "1000", "overtime_125": "1125", "ort_night": "2100", "expense_km": "8010", "leave_vacation": "3000" } }
Unmapped hour types become the fallback (or empty). Add a required validation rule on that column so a missing code blocks the commit. See export mappings.