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

# Time windows

> Set the time windows of a stop: what a window limits, when to make it hard or preferred, how to use several windows or reserve one for some resources, and how to check the result.

A time window tells the engine when a stop may be served. This guide covers the windows of a stop: `authorizedTimeWindows`, `preferredTimeWindows` and `resourceTags`. 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 windows.

A [`workingTimeWindow`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) is the window of a resource, not of a stop. A stop can only be served while one of the resources works.

## What a window limits

A window limits the **start of service**, not the arrival. The start of service is the `beginTime` of the stop in the solution. A resource that arrives before `begin` waits until `begin`, and the stop is still planned.

The wait is not a delay. For one stop, the wait is `beginTime` minus `arrivalTime`. The `waitingDuration` of a tour is the total of these waits over the whole tour.

For example, a clinic accepts deliveries from 10:00. A van reaches the clinic at 08:13, starts service at 10:00, and leaves at 10:10. The wait is about 1 hour 47 minutes, and the delay is zero.

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

## Hard or preferred window

Use `authorizedTimeWindows` for a **hard** window. Outside all of them, the stop can't be planned at all, and it appears in `unaffectedStopIds`. Use `preferredTimeWindows` for a **preferred** window. A late start is allowed, and it costs the `minimizeDelay` objective.

To choose, ask the business one question: what should happen if the fleet can't reach the window? If a late visit is acceptable, use a preferred window. If a visit outside the window is useless, for example because the site is closed, use a hard window. A contractual window is not automatically a hard one. [Hard vs soft constraints](https://developers.kardinal.ai/concepts/hard-vs-soft-constraints.md#a-contractual-window-is-not-automatically-a-hard-one) explains why.

A preferred window is soft at its end only. The delay of a stop is the start of service minus the `end` of the window. A stop that starts before `end` has no delay.

A preferred window is hard at its start. The engine never starts service before `begin`, and a resource that arrives early waits. A preferred window can therefore drop a stop. For example, the window opens at 21:00 and every resource stops working at 20:00. The stop stays in `unaffectedStopIds`. If the business says deliver at 21:00 if possible, and otherwise earlier, don't use a preferred window.

When the preferred window closes before the authorized window opens, the engine serves the stop inside the authorized window. For example, the authorized window is 10:00 to 12:00 and the preferred window is 06:00 to 06:30. The engine serves the stop at 10:00, inside the authorized window. The delay is 3 hours 30 minutes, the time between 06:30 and 10:00. When the preferred window opens after the authorized window closes, the engine doesn't plan the stop. When the preferred window sits inside a wider authorized window, the van waits for the preferred window to open.

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

## Several windows on one stop

Each of the two fields takes an array. A stop with several windows can be served in any one of them. For example, a shop that opens in the morning and again in the evening has two authorized windows, and the engine picks the one that fits the tour.

## Windows reserved for some resources

A window can carry `resourceTags`. Only the resources that carry a matching tag can use this window. The tags of a resource are in its `tags` field.

* A resource that carries **any one** of the tags of a window can use it. A resource doesn't need all the tags.
* A resource without a matching tag doesn't see this window. It can still use the windows without `resourceTags`, and any other window that shares one of its tags.
* A stop whose only windows are tagged can't be served by a resource without the tag. The stop appears in `unaffectedStopIds`.
* The same rules apply to `preferredTimeWindows`. If a stop has only tagged preferred windows and no authorized window, a resource without the tag can't serve the stop. If the stop also has an authorized window with no tag, that resource serves the stop there, with no delay.

For example, a clinic has a window from 08:00 to 09:00 reserved for the tag `certified`, and an open window from 10:00 to 12:00. A van with the tag `certified` can use the first window. A van without the tag waits until 10:00.

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

## Finishing before a deadline

A window bounds the start of service, so a stop can end after `end`. The stop ends at the start of service plus its `operationDuration`.

If the business says the stop must be done by 15:00, subtract `operationDuration` from `end` yourself. For a 15-minute operation, set `end` to 14:45.

```json theme={null}
{
  "operationDuration": "PT15M",
  "authorizedTimeWindows": [
    { "begin": "2026-10-29 13:00", "end": "2026-10-29 14:45" }
  ]
}
```

This snippet is not a whole plan. The plan it goes into needs a `tz` value. See [Dates and time zones](https://developers.kardinal.ai/reference/data-model.md#dates-and-time-zones).

## Checking the solution

The `isValid` and `violations` fields of a tour only report broken hard constraints. A stop that the engine dropped, or a late start against a preferred window, doesn't appear there. Read these fields of the solution as well:

* **`unaffectedStopIds`** lists the stops that no window, or no other constraint, allowed.
* **`beginTime`** of each stop is the start of service. Compare it with the authorized windows that apply to the resource of the tour. Ignore the windows whose `resourceTags` the resource doesn't carry.
* **`delay`** of each stop is the delay against its preferred windows, as an ISO 8601 duration.
* **`waitingDuration`** of each tour is its total waiting time.

The solution writes its times with the offset of the plan's `tz`, for example `2026-10-29T10:00:00+01:00`. The plan can hold local times, for example `2026-10-29 10:00`. Don't compare the two as strings. Convert both to instants with the `tz` of the plan, then compare the instants.

### Example plan

This plan has one van with no tags and two stops. The clinic has two authorized windows, and the first one is reserved for the tag `certified`. The shop has a preferred window that closes at 06:30, before the van starts working at 08:00.

```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 }
    }
  ],
  "orders": [
    {
      "id": "order-clinic",
      "stops": [
        {
          "type": "single",
          "id": "stop-clinic",
          "kind": "delivery",
          "position": { "lon": 2.36, "lat": 48.855 },
          "operationDuration": "PT10M",
          "authorizedTimeWindows": [
            { "begin": "2026-10-29 08:00", "end": "2026-10-29 09:00", "resourceTags": ["certified"] },
            { "begin": "2026-10-29 10:00", "end": "2026-10-29 12:00" }
          ]
        }
      ]
    },
    {
      "id": "order-shop",
      "stops": [
        {
          "type": "single",
          "id": "stop-shop",
          "kind": "delivery",
          "position": { "lon": 2.34, "lat": 48.845 },
          "operationDuration": "PT10M",
          "preferredTimeWindows": [
            { "begin": "2026-10-29 06:00", "end": "2026-10-29 06:30" }
          ]
        }
      ]
    }
  ]
}
```

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,
)

clinic = models.SingleStop(
    type="single",
    id="stop-clinic",
    kind="delivery",
    position=models.Position(lon=2.36, lat=48.855),
    operationDuration="PT10M",
    authorizedTimeWindows=[
        models.TaggedTimeWindow(begin="2026-10-29 08:00", end="2026-10-29 09:00", resourceTags=["certified"]),
        models.TaggedTimeWindow(begin="2026-10-29 10:00", end="2026-10-29 12:00"),
    ],
)

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

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

```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,
    },
  ],
  orders: [
    {
      id: "order-clinic",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "stop-clinic",
          kind: "delivery",
          position: { lon: 2.36, lat: 48.855 },
          operationDuration: "PT10M",
          authorizedTimeWindows: [
            { begin: "2026-10-29 08:00", end: "2026-10-29 09:00", resourceTags: ["certified"] },
            { begin: "2026-10-29 10:00", end: "2026-10-29 12:00" },
          ],
        },
      ],
    },
    {
      id: "order-shop",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "stop-shop",
          kind: "delivery",
          position: { lon: 2.34, lat: 48.845 },
          operationDuration: "PT10M",
          preferredTimeWindows: [{ begin: "2026-10-29 06:00", end: "2026-10-29 06:30" }],
        },
      ],
    },
  ],
};
```

### 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",
  "waitingDuration": "PT1H46M42S",
  "wayPoints": [
    {
      "type": "stop",
      "stopId": "stop-shop",
      "arrivalTime": "2026-10-29T08:01:06+01:00",
      "beginTime": "2026-10-29T08:01:06+01:00",
      "departureTime": "2026-10-29T08:11:06+01:00",
      "delay": "PT1H31M6S"
    },
    {
      "type": "stop",
      "stopId": "stop-clinic",
      "arrivalTime": "2026-10-29T08:13:18+01:00",
      "beginTime": "2026-10-29T10:00:00+01:00",
      "departureTime": "2026-10-29T10:10:00+01:00",
      "delay": "PT0S"
    }
  ]
}
```

* The shop is served at 08:01:06. Its preferred window closed at 06:30, so the delay is 1 hour 31 minutes 6 seconds. The stop is served late, not dropped.
* The clinic is reached at 08:13:18, but the van has no tag `certified`. It can't use the window from 08:00 to 09:00, so it waits until 10:00. The wait is 1 hour 46 minutes 42 seconds, the `waitingDuration` of the tour. The delay is zero.
* `unaffectedStopIds` is empty, because both stops were planned.

### A check in code

These snippets read the solution you got for the example plan. They list the unplanned stops, then print for each stop if its start of service falls inside an authorized window that applies to the resource, and its delay. They convert every time to an instant with the `tz` of the plan.

```python Python theme={null}
from datetime import datetime
from zoneinfo import ZoneInfo

zone = ZoneInfo(plan.tz.root)


def instant(text: str) -> datetime:
    """Read "2026-10-29 10:00" in the plan's tz, or "2026-10-29T10:00:00+01:00" as is."""
    value = datetime.fromisoformat(text)
    return value if value.tzinfo else value.replace(tzinfo=zone)


stops = {s.id.root: s for order in plan.orders for s in order.stops}
tags = {r.id.root: {t.root for t in (r.tags or [])} for r in plan.resources}

for stop_id in solution.unaffectedStopIds or []:
    print(stop_id, "was not planned")

for tour in solution.tours:
    for wp in tour.wayPoints:
        wp = wp.root
        if wp.type != "stop":
            continue
        stop = stops[wp.stopId.root]
        begin = instant(wp.beginTime.root)
        applying = [
            w
            for w in stop.authorizedTimeWindows or []
            if not w.resourceTags or tags[tour.resourceId.root] & {t.root for t in w.resourceTags}
        ]
        inside = not stop.authorizedTimeWindows or any(
            instant(w.begin.root) <= begin <= instant(w.end.root) for w in applying
        )
        print(stop.id.root, "authorized: ok" if inside else "authorized: OUTSIDE", "delay:", wp.delay.root)
```

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

// Reads "2026-10-29 10:00" in the plan's tz, or "2026-10-29T10:00:00+01:00" as is.
function instant(text: string, tz: string): number {
  if (/(Z|[+-]\d\d:\d\d)$/.test(text)) return Date.parse(text);
  const asUtc = Date.parse(`${text.replace(" ", "T")}Z`);
  const shown = new Date(asUtc).toLocaleString("sv-SE", { timeZone: tz });
  return asUtc - (Date.parse(`${shown.replace(" ", "T")}Z`) - asUtc);
}

export function check(plan: PlanCreateInput, solution: WaitedSolution): void {
  const tz = plan.tz ?? "UTC";
  const stops = new Map((plan.orders ?? []).flatMap((o) => o.stops).map((s) => [s.id, s] as const));
  const tags = new Map((plan.resources ?? []).map((r) => [r.id, new Set(r.tags ?? [])] as const));

  for (const stopId of solution.unaffectedStopIds ?? []) console.log(stopId, "was not planned");

  for (const tour of solution.tours ?? []) {
    for (const wp of tour.wayPoints ?? []) {
      if (wp.type !== "stop" || !wp.beginTime) continue;
      const stop = stops.get(wp.stopId ?? "");
      if (stop?.type !== "single") continue;
      const begin = instant(wp.beginTime, tz);
      const applying = (stop.authorizedTimeWindows ?? []).filter(
        (w) => !w.resourceTags?.length || w.resourceTags.some((t) => tags.get(tour.resourceId ?? "")?.has(t)),
      );
      const inside =
        (stop.authorizedTimeWindows ?? []).length === 0 ||
        applying.some((w) => instant(w.begin, tz) <= begin && begin <= instant(w.end, tz));
      console.log(stop.id, inside ? "authorized: ok" : "authorized: OUTSIDE", "delay:", wp.delay);
    }
  }
}
```

For the example plan, both print the same two lines:

```text theme={null}
stop-shop authorized: ok delay: PT1H31M6S
stop-clinic authorized: ok delay: PT0S
```

The shop has no authorized window, so it always passes. The delay of `PT1H31M6S` is the value you would act on: it tells you the shop is served late.

The TypeScript helper reads a local time by looking at how the plan's `tz` shows it. It can be wrong by an hour close to a clock change. For those times, send an explicit offset. See [Dates and time zones](https://developers.kardinal.ai/reference/data-model.md#dates-and-time-zones).


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