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

# Constraints

> Every constraint the V3 Routing solver enforces — what each one means, the fields that drive it, and whether it's hard or soft.

The solver distinguishes two kinds of rule:

* **Hard constraints** define *feasibility*. A plan that violates one is invalid — the
  job is left unassigned rather than placed illegally.
* **Soft constraints** are priced into the [objective](/guides/vrp/v3/api-design#objective).
  They can be bent for a cost, so the solver trades them off against everything else.

<Note>
  The V3 schema is final, but capabilities land in phases. Rules marked
  **Coming soon** are accepted by the schema and currently return a `400` that names
  the field — never a silently-ignored constraint.
</Note>

## Overview

| Constraint                                          | Kind | Driven by                                                           | Status        |
| --------------------------------------------------- | ---- | ------------------------------------------------------------------- | ------------- |
| [Capacity](#capacity)                               | hard | `vehicle.capacity` · `job.demand`                                   | **Available** |
| [Load compatibility](#load-compatibility)           | hard | `vehicle.incompatible_dimensions`                                   | Coming soon   |
| [Time windows](#time-windows)                       | hard | `job.time_windows` (no cost fields)                                 | **Available** |
| [Soft time windows](#time-windows)                  | soft | `job.time_windows.earliness_cost_per_hour / lateness_cost_per_hour` | Coming soon   |
| [Shift hours & overtime](#shift-hours-overtime)     | hard | `shift.from/to`, `shift.max_overtime_s`                             | **Available** |
| [Skills (hard)](#skills)                            | hard | `job.skills[].name` ⊆ `vehicle.skills`                              | **Available** |
| [Skills (soft)](#skills)                            | soft | `job.skills[].violation_cost`                                       | Coming soon   |
| [Allowed / excluded vehicles](#vehicle-eligibility) | hard | `job.eligible_vehicles.allowed/excluded`                            | **Available** |
| [Vehicle preferences](#vehicle-eligibility)         | soft | `job.preferences[].violation_cost`                                  | Coming soon   |
| [Max distance / drive time](#route-limits)          | hard | `vehicle.limits.max_distance_m / max_drive_time_s`                  | **Available** |
| [Max stops](#route-limits)                          | hard | `vehicle.limits.max_tasks`                                          | **Available** |
| [Sequence (ordered)](#sequencing)                   | hard | `relations: ordered` (job ids)                                      | **Available** |
| [Consecutive](#sequencing)                          | hard | `relations: consecutive`                                            | **Available** |
| [Same resource](#sequencing)                        | hard | `relations: same_resource`                                          | **Available** |
| [Same route](#sequencing)                           | hard | `relations: same_route`                                             | **Available** |
| [Group sequence](#sequencing)                       | hard | `relations: ordered` (tags)                                         | **Available** |
| [Same day](#sequencing)                             | hard | `relations: same_day`                                               | Coming soon   |
| [Pickup & delivery](#shipments)                     | hard | `job.pickup` + `job.delivery`                                       | **Available** |
| [Driver breaks](#driver-breaks)                     | hard | `shift.breaks`                                                      | Coming soon   |
| [Synchronisation](#sequencing)                      | hard | `relations: synchronized`                                           | Coming soon   |

## Capacity

Every vehicle has a multi-dimensional **capacity**; every job a **demand**. The load
on a vehicle may never exceed its capacity on any dimension. Dimensions are
**named** and matched by name, so `weight`, `volume`, `pallets` line up across jobs
and vehicles regardless of declaration order.

```json theme={null}
{
  "vehicles": [
    {
      "id": "v1",
      "capacity": { "weight": 1000, "volume": 50 },
      "shifts": [{ "from": "2026-04-01T08:00:00Z", "to": "2026-04-01T17:00:00Z" }]
    }
  ],
  "jobs": [
    {
      "id": "t1",
      "location": { "coordinate": [4.35, 50.85] },
      "demand": { "weight": 30, "volume": 2 }
    }
  ]
}
```

A job whose demand names a dimension no vehicle declares is a `400` — you never
get a silently-infeasible plan.

The response carries `load_after` on every stop as a named map (`{ "weight": 30, "volume": 2 }`),
matching the same dimension names.

### Load compatibility

<Info>**Coming soon.** `vehicle.incompatible_dimensions: [["general", "glass"]]` — declares pairs of load dimensions that may not share a vehicle at the same time (the waste-collection "don't mix streams" rule).</Info>

## Time windows

A job may carry one or more **`time_windows`**. A hard window must be
respected — if the vehicle can't arrive within any window, the job is unassigned.
Multiple windows mean "any of these slots is acceptable".

```json theme={null}
{
  "id": "t1",
  "location": { "coordinate": [4.35, 50.85] },
  "time_windows": [
    { "from": "2026-04-01T09:00:00Z", "to": "2026-04-01T12:00:00Z" }
  ]
}
```

<ParamField path="time_windows[].from / to" type="string (ISO 8601)">
  The allowed service window. Hard by default — arriving outside the window
  makes the job unassignable. **Available.**
</ParamField>

<ParamField path="time_windows[].earliness_cost_per_hour" type="number">
  Cost rate per hour of arriving **early**. When set, early arrival is allowed
  and priced instead of forbidden. *(Coming soon — currently returns 400.)*
</ParamField>

<ParamField path="time_windows[].lateness_cost_per_hour" type="number">
  Cost rate per hour of arriving **late**. When set, late arrival is allowed
  and priced instead of forbidden. *(Coming soon — currently returns 400.)*
</ParamField>

A window with `from == to` and both cost fields set models a **soft target time**
(penalised on both sides). The old `target_arrival` field is gone — this replaces it.

## Shift hours & overtime

A vehicle works only within its **shift** windows. Each shift has a `from`/`to`; the
route — including travel back to the end depot — must finish by `to`, unless
`max_overtime_s` allows a costed extension.

```json theme={null}
"shifts": [
  {
    "from": "2026-04-01T08:00:00Z",
    "to":   "2026-04-01T17:00:00Z",
    "start": { "coordinate": [4.30, 50.80] },
    "max_overtime_s": 3600
  }
]
```

Multiple shifts on one vehicle model **multi-day** availability. Tasks with time
windows spanning different shifts are automatically placed in the right shift.

## Skills

Each job can require **skills** the serving vehicle must have. Matching is a subset
check: `job.skills[].name` ⊆ `vehicle.skills`.

```json theme={null}
{
  "vehicles": [{ "id": "v1", "skills": ["fridge", "lift"], "shifts": [/* … */] }],
  "jobs": [
    {
      "id": "t1",
      "location": { "coordinate": [4.35, 50.85] },
      "skills": [{ "name": "fridge" }]
    }
  ]
}
```

<ParamField path="job.skills[].name" type="string">
  The required skill name. The serving vehicle must have this skill in its
  `skills` list. **Available (hard).**
</ParamField>

<ParamField path="job.skills[].violation_cost" type="number">
  When set, the skill becomes **soft** — assigning a vehicle that lacks it costs
  `violation_cost` instead of being infeasible. *(Coming soon — currently returns 400.)*
</ParamField>

## Vehicle eligibility

One object expresses every "which vehicle may serve this job" rule. Hard
restrictions live in `eligible_vehicles`; soft preferences live in `preferences`.

```json theme={null}
"eligible_vehicles": {
  "allowed":  ["v1", "v2"],
  "excluded": ["v9"]
}
```

<ParamField path="eligible_vehicles.allowed" type="array of strings">
  Hard whitelist — only these vehicles may serve the job. Omit to allow all
  vehicles. **Available.**
</ParamField>

<ParamField path="eligible_vehicles.excluded" type="array of strings">
  Hard blacklist — these vehicles may never serve the job. Mutually exclusive
  with `allowed`. **Available.**
</ParamField>

<ParamField path="preferences" type="array">
  Soft vehicle affinity: `[{ "vehicle": "v2", "violation_cost": 800 }]`. Serving
  a non-preferred vehicle costs `violation_cost` against the objective.
  *(Coming soon — currently returns 400.)*
</ParamField>

## Route limits

Per-vehicle caps on the shape of a route, grouped under `limits`. All hard.

```json theme={null}
"limits": {
  "max_distance_m": 250000,
  "max_drive_time_s": 28800,
  "max_duty_time_s": 36000,
  "max_tasks": 120
}
```

<ParamField path="limits.max_distance_m" type="integer">
  Total route distance cap, metres. **Available.**
</ParamField>

<ParamField path="limits.max_drive_time_s" type="integer">
  Total driving time cap, seconds (excludes service & wait). **Available.**
</ParamField>

<ParamField path="limits.max_duty_time_s" type="integer">
  Total duty time cap, seconds (includes service & wait). **Available.**
</ParamField>

<ParamField path="limits.max_tasks" type="integer">
  Maximum number of job stops on the route. **Available.**
</ParamField>

## Sequencing

Inter-job ordering and grouping are expressed through the `relations` array. Each
relation references jobs either explicitly (`job_ids`) or by tag (`group`), but
not both.

```json theme={null}
"relations": [
  { "type": "ordered",       "job_ids": ["a", "b", "c"] },
  { "type": "consecutive",   "job_ids": ["b", "c"], "ordered": true },
  { "type": "same_resource", "job_ids": ["f", "g"] },
  { "type": "same_route",    "job_ids": ["x", "y"] }
]
```

| Rule                | Meaning                                                          | Status        |
| ------------------- | ---------------------------------------------------------------- | ------------- |
| `ordered` (job ids) | Jobs appear in this order on one route                           | **Available** |
| `ordered` (tags)    | Tag groups appear in this order; order within a group is free    | **Available** |
| `consecutive`       | Jobs are adjacent; `ordered: true` also fixes their order        | **Available** |
| `same_resource`     | Jobs served by the same vehicle — continuity of care across days | **Available** |
| `same_route`        | Jobs on the same route in the same shift                         | **Available** |
| `same_day`          | Jobs on the same calendar day                                    | Coming soon   |
| `synchronized`      | Two vehicles meet at a location within `max_wait_s`              | Coming soon   |

`job_ids` entries are polymorphic — each string is a **job id** or a **tag**
(which expands to all jobs carrying that tag).

<Tip>
  **Synchronisation** (`type: "synchronized"`) is the home-health "two carers,
  one visit" rule. It's the hardest constraint for the engine — it couples two routes —
  and arrives in a later phase as a best-effort feature.
</Tip>

## Shipments

A shipment is a pickup → delivery pair: a `job` with `pickup` + `delivery` legs
instead of flat visit fields. The solver guarantees the pickup precedes the
delivery **on the same route**, and the carried `demand` counts against capacity only
between the two stops. Same-vehicle and ordering are implicit — never expressed as a
separate relation.

```json theme={null}
{
  "id": "ship-7",
  "demand": { "weight": 50 },
  "pickup":   { "location": { "coordinate": [4.30, 50.80] }, "service_duration_s": 120 },
  "delivery": { "location": { "coordinate": [4.50, 50.90] }, "service_duration_s": 120 },
  "skills": [{ "name": "tail_lift" }]
}
```

<Info>**Available** for mandatory shipments with coordinate legs, hard skills, hard time windows, and (optionally) a single allowed vehicle. Droppable shipments, soft skills/windows, multi-vehicle eligibility, and depot-referenced legs are Coming soon.</Info>

## Driver breaks

<Info>**Coming soon.** `shift.breaks[]` — `floating` (flexible within a window) and `fixed` (a mandatory off-duty interval), with an optional drive/duty `trigger`.</Info>

Breaks are inserted into the route's timeline and respect the surrounding travel and
service. Used for lunch breaks, EU 561/2006 driving rules, and scheduled unavailability.

***

<CardGroup cols={2}>
  <Card title="The V3 API Model" icon="cube" href="/guides/vrp/v3/api-design">
    How the full request is structured.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/vrp/v3/solve">
    The `POST /v3/routing/solve` schema.
  </Card>
</CardGroup>
