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

URL: https://timetracking.tech/developers/concepts/hour-types-and-rule-packs
Last updated: 2026-10-24

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](https://timetracking.tech/developers/data-out/export-mappings) 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

```json
{
  "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

1. **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`), or `normal`.
2. **Deduct breaks.** Breaks are unpaid. Without exact break times, they come out of `normal` minutes first, then out of the largest other type. Surcharge minutes are therefore never inflated.
3. **Daily overtime** moves `normal` minutes above the threshold. The highest threshold applies first.
4. **Weekly overtime** works per placement and ISO week, chronologically, on the remaining `normal` minutes. The engine loads the whole week even when the export range starts mid-week.
5. **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:

```json
{ "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:

```json
{ "header": "Looncomponent", "source": "hourType", "transform": { "kind": "map", "table": "wageCodes", "fallback": "" } }
```

```json
"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](https://timetracking.tech/developers/data-out/export-mappings).
