> ## Documentation Index
> Fetch the complete documentation index at: https://developers.kardinal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Breaks

> Model the breaks of a resource: the three kinds of break, where the engine can place one, when it is owed, how to plan a night rest, and what a solution can and cannot prove about its breaks.

A break is a pause in the work of a resource. A lunch and a night rest are two examples. The engine places each break inside the tour it plans. A tour is the route of one resource, from its start to its end. In the solution, a tour is a list of waypoints. A break waypoint is the waypoint where the engine placed a break. This guide covers the `breaks` array of a resource. Each section states one rule, with a link to the API reference where the exact behavior is written. The last section shows how to check a solution against its breaks.

## Three kinds of break

The `breaks` array of a resource accepts three types of break. You can combine them to model a realistic shift.

| Type | Triggered by | Typical use |
| - | - | - |
| `timeWindowBreak` | A fixed clock time | A lunch break that must happen inside a specific window, regardless of workload |
| `workingDurationSlidingBreak` | Cumulative working time | Labor-contract rules. The duration varies by sector and collective agreement |
| `travelDurationSlidingBreak` | Cumulative driving time | Road-safety rules. For example, EU Regulation (EC) 561/2006 sets a minimum of 45 minutes after every 4 hours 30 minutes of driving for professional road transport |

These snippets use local times. The plan you put them in needs `"tz"`. See [Dates and time zones](https://developers.kardinal.ai/reference/data-model.md#dates-and-time-zones).

```json theme={null}
{
  "id": "driver-1",
  "workingTimeWindow": { "begin": "2026-08-03 05:00", "end": "2026-08-03 20:00" },
  "breaks": [
    { "type": "workingDurationSlidingBreak", "minBreakDuration": "PT30M", "maxInterBreakDuration": "PT6H" },
    { "type": "travelDurationSlidingBreak", "minBreakDuration": "PT45M", "maxInterBreakDuration": "PT4H30M" }
  ]
}
```

The two figures are not equally safe to default. The driving-time figure comes from public law, and it doesn't depend on one employer. You can apply `PT45M` and `PT4H30M` once you have established the jurisdiction. See [step 4 of the agent modeling checklist](https://developers.kardinal.ai/getting-started/agent-modeling-checklist.md#4-do-not-guess-on-vehicle-profile-break-triggers-or-other-underspecified-fields-check-what-the-data-actually-supports).

The working-time figure usually comes from a collective agreement, on top of a lower legal minimum. A single number would be wrong for many employers. Don't copy a duration from this page for `workingDurationSlidingBreak`. Ask the client to confirm it.

See [`Break`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) for the rule.

## One break for two rules

The two sliding breaks count the same break toward both of their counters. One counter is the cumulative driving time, and the other is the cumulative working time. The engine looks for one moment that satisfies both rules. In the sandbox, it didn't schedule a separate break for each rule. For a long shift, define the driving-time rule and the working-time rule together. A sliding break of one type only covers its own counter.

The earlier documentation also said that any two overlapping breaks count once. For example, it said that a `timeWindowBreak` that coincides with a `workingDurationSlidingBreak` counts for both. This claim isn't confirmed.

The [`Break`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) reference states only that the two sliding types share one break.

## Where a break can fall

The engine places a break only where the vehicle already stands. It never places a break in the middle of a drive. This holds for all three types.

A `timeWindowBreak` can fall at the start of the tour or at a stop. A sliding break falls only after a stop, never at the start of the tour.

What happens to a long drive depends on the type:

* **`timeWindowBreak`.** A drive that covers the whole time the break could take place makes that drive impossible. The engine leaves the stops that need it unplaced. It returns no error and no violation. If every tour of the vehicle needs that drive, the vehicle isn't used at all.
* **`travelDurationSlidingBreak` and `workingDurationSlidingBreak`.** Currently, the engine allows a drive longer than `maxInterBreakDuration`. It marks the tour valid and reports no violation. It places the break at a stop, in two cases:

  * When the next drive would go over the limit, the engine places a break at the stop before it.
  * When the limit is already passed on arrival at a stop, and the tour goes on after that stop, the engine adds the missed breaks at that stop. This is the catch-up. To get its length, divide the driving or working time since the last break by `maxInterBreakDuration`. Round the result up, then multiply it by `minBreakDuration`. At such a stop, the catch-up is the only break placed.

  The catch-up starts after the work at the stop, so the times of the stop don't change. Everything after the stop moves later. Nothing catches up the last drive of a tour. This is the return to the depot, or the drive to the last stop when the van has no end depot. This describes the current behaviour of the engine. It explains what the sandbox showed, and it isn't a guarantee. The only real check is to measure the gaps yourself. See [Checking a sliding break](#checking-a-sliding-break).

  The engine plans with the catch-up included. It can change the order of the stops. It can also push a later stop out of its time window, or push the tour past `maxWorkingDuration`. The engine then changes the order of the stops or leaves stops unplaced, with no error.

For example, a van has a `timeWindowBreak` of 45 minutes in a window from 08:30 to 09:30. The van starts work at 08:00. Its only stop is a drive of 1 hour 48 minutes away. The stop only accepts the van from 09:48 to 10:00, so the van has to leave between 08:00 and 08:12. The drive then runs through the whole window. The break can't fall before the drive, because the van leaves before the window opens. It can't fall after the drive, because the window closed at 09:30. The engine leaves the stop unplaced.

Now give the same van a `travelDurationSlidingBreak` of 45 minutes after 1 hour, and no `timeWindowBreak`. In this example, the stop has no time window, and the van has no end depot. The engine plans the same drive of 1 hour 48 minutes. It places no break, because nothing follows the stop. The tour is valid.

Now give the van a return to the depot. The van reaches the stop after 1 hour 48 minutes of driving, which is two limits of 1 hour, rounded up. The engine adds 2 × 45 minutes = 1 hour 30 minutes of break at the stop. Then the van drives back with no break, because nothing catches up the last drive. The tour is still valid, and it ends 1 hour 30 minutes later.

To keep a long drive within the rule, give the route a stop where each break can fall. Each drive between two stops should stay under `maxInterBreakDuration`. For example, a drive of 1 hour 48 minutes split into two legs of 54 minutes, with a rest stop between them, gets one break of 45 minutes at the rest stop. If the route already has a natural stop, such as a delivery, a pickup or a customs post, use it. If it has none, add a stop where the driver can rest. It is a stop like any other. It must be reachable. In production, it costs one credit. See [Pricing and credits](https://developers.kardinal.ai/reference/pricing-and-credits.md).

Then check the gaps in the solution. A sliding break doesn't report a drive that is too long. See [Checking the solution](#checking-the-solution).

See [`Resource.breaks`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) for the rule.

## When a break is owed

The engine checks breaks only inside the tour it plans. It knows nothing about what the driver does before or after that tour. A break is owed only when the tour reaches the point where the rule applies.

* **`timeWindowBreak`.** It is a hard rule: the engine drops stops rather than skip the break. For example, a van is busy at one stop from 11:30 to 13:30, and a 45-minute break is due between 12:00 and 14:00. The tour would end at 13:31, with only 29 minutes of the window left, so the engine leaves that stop unplaced. When a tour keeps running past the end of the window, the solution has a break waypoint.
* **No break when the tour ends first.** The solution has no break waypoint when the whole tour ends before the window opens. For example, a tour that ends at 08:12 has no lunch break due from 12:00. The solution has no break waypoint, no violation, and no message.
* **A tour that ends inside the window can have no break waypoint.** In the sandbox, a van waited at a stop from 08:01 to 13:00 and the tour ended at 13:11, inside a window of 12:00 to 14:00. No break appeared. The same happened when the stop opened at 12:20, so the wait covered only 20 minutes of the window. In both tours, more than 45 minutes of the window remained after the tour ended. The rule behind this isn't confirmed, so don't read a wait as a break.
* **Sliding breaks.** A sliding break is not scheduled when the tour is shorter than `maxInterBreakDuration`. For example, a 30-minute break after 6 hours of work doesn't appear in a tour of 12 minutes.

Keep this in mind when a driver keeps working after the tour. The engine doesn't see that work, so it doesn't count it toward any break.

See [`Break`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) for the rule.

## Night rest on a multi-day trip

A `workingTimeWindow` that spans more than one calendar day doesn't force any rest between working days. See the `Modelling pitfall` on `workingTimeWindow` in the [API reference](https://developers.kardinal.ai/api-reference/plan/create-a-plan.md#body-resources). You need to encode the rest in `breaks`.

```json theme={null}
{
  "id": "driver-1",
  "workingTimeWindow": { "begin": "2026-08-03 05:00", "end": "2026-08-04 20:00" },
  "maxWorkingDuration": "PT37H",
  "breaks": [
    { "type": "workingDurationSlidingBreak", "minBreakDuration": "PT11H", "maxInterBreakDuration": "PT13H" },
    { "type": "timeWindowBreak", "duration": "PT11H", "timeWindow": { "begin": "2026-08-03 19:00", "end": "2026-08-04 07:00" } }
  ]
}
```

The long `workingDurationSlidingBreak` asks for a full night's rest. Here it lasts `PT11H`. This figure is an illustration. Confirm the convention or regulation that applies before you publish it, as for the working-time figure in the first section.

Currently, the engine allows a drive longer than `maxInterBreakDuration`. It adds the missed breaks at a stop that the tour continues from. See [Where a break can fall](#where-a-break-can-fall).

This matters for a night rest. Take a `workingDurationSlidingBreak` of 11 hours after 13 hours of work, and no other break. A driver reaches a stop after 14 hours 38 minutes of driving, works there for 10 minutes, and the tour goes on after that stop. The engine adds 2 × 11 hours = 22 hours of break at that stop. To avoid this, give the route a stop before the limit is reached.

The `timeWindowBreak` is the part that the engine treats as hard. It fixes the window in which the rest has to fall, for example to prevent a restart before a given hour the next day.

`maxWorkingDuration` counts breaks and waiting too. On a vehicle that works several days, size it for the whole trip. Here, `PT37H` is 13 hours of work, the 11-hour rest, and 13 more hours of work.

A longer trip extends `workingTimeWindow` and `maxWorkingDuration`. It also needs a stop where each night's rest can fall. See [Where a break can fall](#where-a-break-can-fall).

## Checking the solution

The engine applies the rule as you wrote it, inside the tour it plans. A solution can prove some things about a break, and not others.

A waypoint is one line of a tour in the solution: the start, each stop, each break, and the end.

**What a solution shows:**

* A break waypoint, with `arrivalTime` and `departureTime`, shows where and when the engine placed a break. It always follows the start of the tour or a stop.
* `unaffectedStopIds` lists the stops that the engine left out, for example because of a `timeWindowBreak`.

**What a solution doesn't show:**

* How long the break lasted. A break waypoint has no duration field. It can last longer than the break, for example when it includes a wait. Don't read the length of the break from it. See [Checking a sliding break](#checking-a-sliding-break) for the one case that can be a catch-up.
* Why a break waypoint is absent. The solution looks the same when the tour ended before the window opened and when it ended inside the window.
* That a sliding limit was respected. A drive longer than `maxInterBreakDuration` has no violation. See [Where a break can fall](#where-a-break-can-fall).

### Example plan

This plan has one van with a 45-minute lunch break due between 12:00 and 14:00. It has two stops. The morning stop has no window. The afternoon stop opens at 15:00, so the tour runs through the lunch window.

```json theme={null}
{
  "tz": "Europe/Paris",
  "maxOptimizationDuration": "PT5S",
  "resources": [
    {
      "id": "van-1",
      "vehicleProfile": { "type": "car" },
      "workingTimeWindow": { "begin": "2026-10-29 08:00", "end": "2026-10-29 20:00" },
      "departure": { "lon": 2.35, "lat": 48.85 },
      "arrival": { "lon": 2.35, "lat": 48.85 },
      "breaks": [
        {
          "type": "timeWindowBreak",
          "duration": "PT45M",
          "timeWindow": { "begin": "2026-10-29 12:00", "end": "2026-10-29 14:00" }
        }
      ]
    }
  ],
  "orders": [
    {
      "id": "order-morning",
      "stops": [
        {
          "type": "single",
          "id": "stop-morning",
          "kind": "delivery",
          "position": { "lon": 2.36, "lat": 48.855 },
          "operationDuration": "PT10M"
        }
      ]
    },
    {
      "id": "order-afternoon",
      "stops": [
        {
          "type": "single",
          "id": "stop-afternoon",
          "kind": "delivery",
          "position": { "lon": 2.34, "lat": 48.845 },
          "operationDuration": "PT10M",
          "authorizedTimeWindows": [
            { "begin": "2026-10-29 15:00", "end": "2026-10-29 16:00" }
          ]
        }
      ]
    }
  ]
}
```

The same plan with the SDKs:

```python Python theme={null}
from kardinal.routing import models
from kardinal.routing.plans import Order, PlanCreate

depot = models.Position(lon=2.35, lat=48.85)

van = models.Resource(
    id="van-1",
    vehicleProfile=models.VehicleProfileCar(type="car"),
    workingTimeWindow=models.TimeWindow(begin="2026-10-29 08:00", end="2026-10-29 20:00"),
    departure=depot,
    arrival=depot,
    breaks=[
        models.TimeWindowBreak(
            type="timeWindowBreak",
            duration="PT45M",
            timeWindow=models.TimeWindow(begin="2026-10-29 12:00", end="2026-10-29 14:00"),
        )
    ],
)

morning = models.SingleStop(
    type="single",
    id="stop-morning",
    kind="delivery",
    position=models.Position(lon=2.36, lat=48.855),
    operationDuration="PT10M",
)

afternoon = models.SingleStop(
    type="single",
    id="stop-afternoon",
    kind="delivery",
    position=models.Position(lon=2.34, lat=48.845),
    operationDuration="PT10M",
    authorizedTimeWindows=[models.TaggedTimeWindow(begin="2026-10-29 15:00", end="2026-10-29 16:00")],
)

plan = PlanCreate(
    tz="Europe/Paris",
    maxOptimizationDuration="PT5S",
    resources=[van],
    orders=[Order(id="order-morning", stops=[morning]), Order(id="order-afternoon", stops=[afternoon])],
)
```

```ts TypeScript theme={null}
import type { PlanCreateInput } from "@kardinal-ai/sdk";

const depot = { lon: 2.35, lat: 48.85 };

const plan: PlanCreateInput = {
  tz: "Europe/Paris",
  maxOptimizationDuration: "PT5S",
  resources: [
    {
      id: "van-1",
      priority: 0,
      vehicleProfile: { type: "car" },
      workingTimeWindow: { begin: "2026-10-29 08:00", end: "2026-10-29 20:00" },
      departure: depot,
      arrival: depot,
      breaks: [
        {
          type: "timeWindowBreak",
          duration: "PT45M",
          timeWindow: { begin: "2026-10-29 12:00", end: "2026-10-29 14:00" },
        },
      ],
    },
  ],
  orders: [
    {
      id: "order-morning",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "stop-morning",
          kind: "delivery",
          position: { lon: 2.36, lat: 48.855 },
          operationDuration: "PT10M",
        },
      ],
    },
    {
      id: "order-afternoon",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "stop-afternoon",
          kind: "delivery",
          position: { lon: 2.34, lat: 48.845 },
          operationDuration: "PT10M",
          authorizedTimeWindows: [{ begin: "2026-10-29 15:00", end: "2026-10-29 16:00" }],
        },
      ],
    },
  ],
};
```

### What the solution shows

The sandbox returns these waypoints for the plan, with the offset of `Europe/Paris` and with seconds. The other fields are left out.

```json theme={null}
{
  "resourceId": "van-1",
  "isValid": true,
  "waitingDuration": "PT2H12M48S",
  "wayPoints": [
    { "type": "begin", "departureTime": "2026-10-29T08:00:00+01:00" },
    {
      "type": "stop",
      "stopId": "stop-morning",
      "arrivalTime": "2026-10-29T08:01:06+01:00",
      "beginTime": "2026-10-29T08:01:06+01:00",
      "departureTime": "2026-10-29T08:11:06+01:00"
    },
    {
      "type": "break",
      "arrivalTime": "2026-10-29T08:11:06+01:00",
      "departureTime": "2026-10-29T12:45:00+01:00"
    },
    {
      "type": "stop",
      "stopId": "stop-afternoon",
      "arrivalTime": "2026-10-29T12:47:12+01:00",
      "beginTime": "2026-10-29T15:00:00+01:00",
      "departureTime": "2026-10-29T15:10:00+01:00"
    },
    { "type": "end", "arrivalTime": "2026-10-29T15:11:06+01:00" }
  ]
}
```

* The break waypoint comes right after `stop-morning`, not in the middle of a drive.
* The waypoint lasts 4 hours 33 minutes 54 seconds, for a break of 45 minutes. It can last longer than the break, for example when it includes a wait. Here the van stands from 08:11 until the window opens at 12:00, and the break ends at 12:45.
* The tour is valid, and it has no violation. The solution doesn't say that the break was owed. You know it from the plan: the tour ends at 15:11, after the window opened.
* `unaffectedStopIds` is empty, because both stops were planned.

### A check for a `timeWindowBreak`

For a `timeWindowBreak`, you can check each tour against the plan. The check has two steps:

1. If the tour ends before the window opens, no break waypoint is expected.
2. If the tour keeps running past the end of the window, look for a break waypoint that overlaps the window.
3. If the tour ends inside the window and no break waypoint appears, the result is consistent with the sandbox tours only when at least `duration` of the window remains after the tour ends. Otherwise, the engine would have left the stops unplaced. The rule behind this isn't confirmed.

The check assumes that the resource has an `arrival`, so that the tour has an end waypoint. It also assumes that the tour starts before the window closes: for a tour that starts after the window closes, the result isn't confirmed. Convert every time to an instant with the `tz` of the plan. The overlap with the window can include a wait, so the check shows that the break could fit. It doesn't show how long the break lasted.

For the example plan, the tour ends at 15:11, after the window closes at 14:00. The break waypoint runs from 08:11 to 12:45, so it overlaps the window from 12:00 to 12:45. That is 45 minutes, so the break fits. Send an explicit offset for a time close to a clock change. See [Dates and time zones](https://developers.kardinal.ai/reference/data-model.md#dates-and-time-zones).

### Checking a sliding break

For a sliding break, the solution can't tell you whether the rule was respected. You measure the gaps yourself. For each tour, walk the waypoints in order. Add up the driving time or the working time between two break waypoints, and between the start of the tour and the first break. Compare each total with your own `maxInterBreakDuration`. Use the `tz` of the plan to read the times.

A break waypoint longer than `minBreakDuration`, right after a stop, with no later time window to wait for, can be a catch-up. The formula in [Where a break can fall](#where-a-break-can-fall) gives its expected length. A multiple of `minBreakDuration` is not a sign, because a wait can be folded into the waypoint. The measuring described here is the only real check.

It isn't confirmed whether a wait at a stop counts as a break for a sliding break. It isn't confirmed for a `timeWindowBreak` either. In the sandbox, the engine added a break before a long wait and another after it. To stay safe, count a wait as work, unless a break waypoint is there.

### The engine's break is not a legal break

The engine doesn't check a law, and this guide gives no legal advice. A break that the engine plans, or leaves out, may not match a legal break. Driving done before the tour is not seen. If the plan must prove compliance, check the result against the rule that applies to your driver and your country.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.