Skip to main content
This page is the exhaustive, field-by-field reference for every constraint the VRP solver evaluates. For the conceptual model — why hard/medium/soft levels exist, how weights interact, and how to debug conflicts — read the VRP Constraint System guide first. This page assumes that background and goes straight to “which field configures which constraint.”
Every request field referenced below belongs to the OnRouteRequest schema — resources[] (Resource), jobs[] (Job), relations[] (Relation), depots[], options, weights, and costs. Field paths below are written relative to the object they belong to, e.g. windows[].hard means job.windows[].hard.

Levels and how violations are reported

Every constraint is one of three levels:
  • hard — a plan that violates it is infeasible. The solver leaves the offending job unassigned (with options.partialPlanning, the default) rather than place it illegally.
  • medium — priced independently from hard and soft score components; used for a handful of “should hold, but a plan isn’t automatically invalid if it doesn’t” rules (min-side period rules, same-time synchronization).
  • soft — priced into the objective. Always somewhat “violated” — the goal is minimizing the penalty, not eliminating it.
When you call the explanation endpoint or read JobAssignment.violations/scoreExplanation, each violated constraint is named using the OnrouteConstraint enum — that’s the code shown in the Reported as column of every table below. A handful of relation-based constraints (noted inline) share a reporting bucket with a sibling constraint rather than having their own enum value.

Weights vs. costs

Soft constraints are tunable two ways — pick one per request, don’t mix mental models:
  • weights — unitless relative importance (e.g. travelTimeWeight: 1, minimizeResourcesWeight: 3600). This is the original system; all weight fields default to small integers and are compared against each other, not against real money.
  • costs — monetary units (EUR or your currency, treated as unitless by the solver). When costs is present, it’s converted internally to the equivalent weights and takes precedence over weights for the fields it covers.
costs.softSkillViolationCost, costs.timeWindowViolationCostPerHour, and costs.preferredResourceViolationCost are accepted by the schema but currently have no effect on live solving — soft tag and soft time-window penalties always use each item’s own weight field regardless. Use the per-item weight fields documented below instead.
A number of hard/medium constraints below (period rules, overtime, region preference) don’t expose a dedicated weights field at all — the solver applies a fixed internal weight and the constraint isn’t currently tunable except through costs where noted.

Overview

Time windows

A job can carry one or more time windows. Multiple windows mean “any of these is acceptable.”
windows[].from
string (ISO 8601)
required
Start of the window.
windows[].to
string (ISO 8601)
required
End of the window.
windows[].hard
boolean
default:"true"
When true, arriving outside every window makes the job unassignable (DATE_TIME_WINDOW_CONFLICT, hard). When false, arriving outside a window is allowed but penalized (DATE_TIME_WINDOW_CONFLICT_SOFT, soft).
windows[].weight
integer
default:"1"
Penalty weight for the soft path only — ignored when hard: true.

Planned commitments

Two fields pin a job to a pre-committed schedule — one hard, one soft:
job.plannedDate
string (date)
Fixes the job to a specific calendar date. Hard — the solver must honor it (PLANNED_DATE).
job.plannedArrival
string (ISO 8601)
Target arrival time. Soft — deviations are penalized proportionally to weights.plannedWeight, not forbidden (PLANNED_ARRIVAL).
Use plannedDate for commitments that must never move (e.g. already confirmed with a customer) and plannedArrival for a preferred time the solver should try to keep but can adjust if it substantially improves the rest of the route.

Wait time and urgency

weights.waitTimeWeight
integer
default:"0 (disabled)"
Penalizes idle time between arrival and a window opening, summed across every resource (WAIT_TIME, soft). Disabled unless set.
job.urgency
integer
Higher values push the job earlier in the day and earlier in the planning period. Affects execution order, not whether the job is selected (URGENCY, soft, scaled by weights.urgencyWeight).
job.padding (seconds added before/after service), job.durationSquash (reduced duration for a job immediately following another at the same location), and job.proficiency[].durationModifier (per-resource duration multiplier) aren’t constraints themselves, but they change what arrival times and windows are scored against.

Shift hours and overtime

A resource works only within its declared shift windows.
shift.to
string (ISO 8601)
required
End of the shift. Without overtimeEnd, finishing later is a hard violation (SHIFT_END_CONFLICT).
shift.overtimeEnd
string (ISO 8601)
When set (and later than shift.from), finishing after to but before overtimeEnd becomes a soft penalty instead of infeasible, scaled via costs.overtimeCostPerHour (no direct weights field is currently exposed for this). Finishing after overtimeEnd itself is always hard (OVERTIME_END_CONFLICT), regardless of the cost/weight configuration.
resource.hourlyCost
integer
Cost per hour of active (driving, servicing, or waiting) time. Not a violation — an always-on cost-minimization objective (HOURLY_COST, soft).
shift.breaks
array
Windowed (WINDOWED), duty/drive-triggered (DRIVE), and unavailability (UNAVAILABILITY) breaks. Breaks aren’t scored directly — they shift the arrival-time shadow variable, which the time-window and shift-end constraints above then score. Set job.resumable: true to let a job be split by an unavailability break instead of blocked by it.

Drive time, distance, and travel

Per-resource caps on driving, plus the always-on travel-time objective.
resource.maxDriveTimeInSeconds
integer
Cumulative driving time cap per shift/trip (excludes service time). Hard (MAX_DRIVE_TIME).
resource.maxDriveDistance
integer
Cumulative distance cap per shift/trip, meters. Hard (MAX_DRIVE_DISTANCE).
resource.maxDriveTimeJob
integer
Cap on a single leg’s travel time (from the previous stop, or the final leg back to the shift end). Prevents excessive backtracking within one hop, distinct from the cumulative cap above. Hard (MAX_DRIVE_TIME_JOB).
weights.travelTimeWeight
integer
default:"1"
Baseline objective weight for total travel time. Also covers the return leg to the shift’s end location. Equivalent costs.drivingCostPerHour (default 25.0). Soft (TRAVEL_TIME / END_LOCATION_TRAVEL_TIME).
weights.driveTimeWeight
integer
default:"0 (disabled)"
Separate objective for minimizing total driving time specifically (as opposed to travel time generally). Soft (DRIVE_TIME).
resource.region
object (Location)
A preferred operating location for the resource. The solver tries to keep the resource’s jobs close to it. No dedicated weights field is currently exposed — tune via costs.outOfRegionCostPerHour (default 10.0 EUR/hour outside the region). Soft (REGION_TIME).

Capacity and load compatibility

Every resource has a multi-dimensional capacity (up to 5 dimensions); every job a load.
resource.capacity
array of integers
Multi-dimensional capacity, e.g. [500, 200] for weight and volume. Hard (TRIP_CAPACITY).
job.load
array of integers
Load consumed on each capacity dimension. Must match the dimension count of any resource it could be assigned to.
resource.loadCompatibility
array of arrays of integers
Matrix restricting which load dimensions may coexist non-zero on the vehicle at once: loadCompatibility[i][j] is the effective max capacity for dimension j when dimension i currently carries load. Must match capacity’s dimension count. Omit for no restriction. Hard (LOAD_COMPATIBILITY).
For pickup-and-delivery requests, load is tracked cumulatively as the vehicle picks up and drops off along the route rather than accumulated per-trip — see Pickup and delivery.

Skills, rankings, and eligibility

Four independent mechanisms answer “which resource may or should serve this job,” from strict (hard tags, allow/deny lists) to soft (rankings).
job.tags[].name
string
required
Skill/capability name matched against resource.tags (or the active shift.tags, which override the resource’s tags for that shift).
job.tags[].hard
boolean
default:"true"
true: only resources with the tag may take the job (TAG_HARD, hard). false: preferred but not required, penalized by .weight if violated (TAG_SOFT, soft).
job.tags[].weight
integer
Penalty for the soft path, in travel-time-equivalent units.
job.rankings[].name
string
required
A resource name this job prefers.
job.rankings[].ranking
integer
default:"1-100"
Lower is more preferred (1 = best). Unranked resources are treated as rank 100. Soft (RANKING_SOFT), scaled by weights.rankingWeight / costs.rankingViolationCostPerRank.
job.allowedResources
array of strings
Deprecated.
Whitelist of resource names. Hard (ALLOWED_RESOURCES), scaled by weights.allowedResourcesWeight.
job.disallowedResources
array of strings
Blacklist of resource names. Hard (DISALLOWED_RESOURCES), same weight field as above.
job.plannedResource
string
Pins the job to exactly this resource — used for warm-starting or committed assignments. Hard (PLANNED_RESOURCE).
resource.compatibleResources
array of strings
Other resource names this resource may cooperate with on linked jobs. Only enforced when a SAME_TIME relation sets enforceCompatibility: true. Hard (RESOURCE_COMPATIBILITY).

Job relations

Inter-job rules live in the top-level relations[] array, each referencing job names (or, for GROUP_SEQUENCE, tags).
relation.type
enum
required
One of SAME_TRIP, SEQUENCE, DIRECT_SEQUENCE, NEIGHBOR, SAME_TIME, PICKUP_AND_DELIVERY, SAME_RESOURCE, SAME_DAY, GROUP_SEQUENCE — see the table below.
relation.jobs
array of strings
required
Job names in the relation. Order matters for SEQUENCE/DIRECT_SEQUENCE. Ignored for GROUP_SEQUENCE (membership comes from relation.tags matching job.tags instead).
relation.resource
string
Optional — pins every job in the relation to one named resource.
relation.partialPlanning
boolean
default:"false"
When true, allows some (not necessarily all) member jobs to be scheduled if the full relation can’t be satisfied.
relation.minTimeInterval
integer
Minimum seconds between consecutive jobs. SEQUENCE, DIRECT_SEQUENCE, and SAME_TIME only. Hard by default; set relation.hardMinWait: false to make it soft instead.
relation.maxTimeInterval
integer
Maximum seconds between consecutive jobs. Same relation types as above.
relation.maxWaitingTime
integer
default:"1200"
For SAME_TIME: how far apart member arrivals may be and still count as synchronized.
relation.timeInterval
enum
default:"FROM_ARRIVAL"
FROM_ARRIVAL measures the gap from service start to service start; FROM_DEPARTURE from service end to service start.
relation.tags
array of strings
GROUP_SEQUENCE only — ordered tag names; jobs carrying each tag form one group.
relation.enforceCompatibility
boolean
SAME_TIME only — requires the resources ultimately assigned to member jobs to be mutually listed in each other’s compatibleResources. See Resource compatibility.
relation.weight
integer
Weight modifier for this specific relation instance, independent of other relations of the same type.
A SEQUENCE relation with exactly one job is automatically treated as a First job constraint: that job must be the very first stop on its trip. It shares the SEQUENCE reporting bucket rather than getting its own name.

Pickup and delivery

Setting any relation to PICKUP_AND_DELIVERY switches the two named jobs into a pickup → delivery pair (adding pickup/delivery legs instead of a single visit) and enables cumulative load tracking for the whole request.
Two hard constraints are added automatically — there’s no separate field to configure them beyond declaring the relation:
  • Delivery on same vehicle (DELIVERY_NOT_ON_SAME_VEHICLE) — the pickup and its paired delivery must be on the same vehicle.
  • Delivery after pickup (DELIVERY_BEFORE_PICKUP) — the pickup’s arrival must precede the delivery’s arrival.
Once any PICKUP_AND_DELIVERY relation is present, trip capacity and load compatibility switch from a flat per-trip sum to cumulative tracking — load increases at each pickup and decreases at each delivery, so capacity can never be exceeded at any point along the route, not just in total.

Depots

Depots are optional intermediate stops (disposal sites, resupply points) resources can be routed through mid-route.
depot.cost
number
Per-visit cost, used to prefer cheaper depots when a detour is optional. Soft (DEPOT_COST), also settable via costs.depotCost.
depot.capacity
array of integers
Multi-dimensional cap on cumulative load routed through this depot across every job that selects it. Hard (DEPOT_CAPACITY).
depot.windows
array (DateWindow)
Opening hours. Arrival outside every window is infeasible. Hard (DEPOT_TIME_WINDOW).
depot.duration
integer
Service/unloading time at the depot, in seconds. Not a constraint by itself, but extends the route’s timeline, which the shift-end and time-window constraints then score against.
shift.unloadBeforeEndShift (End depot, hard, END_DEPOT) forces the last job of a route to select a depot if the vehicle would otherwise still be carrying non-zero load at shift end.

Period rules

Resources can carry rules that cap or floor total work over a longer period (a week, a month, the whole planning horizon) rather than a single shift.
rule.period
object (PeriodDto)
The sub-window the rule applies to. Omit to cover the whole planning period.
rule.maxServiceTime / minServiceTime
integer
Cap/floor on total service time (job durations only) across the period, in seconds. Max is hard (RESOURCE_PERIOD_MAX_SERVICE_TIME); min is medium (RESOURCE_PERIOD_MIN_SERVICE_TIME).
rule.maxDriveTime / minDriveTime
integer
Same pattern, for driving time. RESOURCE_PERIOD_MAX_DRIVE_TIME (hard) / RESOURCE_PERIOD_MIN_DRIVE_TIME (medium).
rule.maxWorkTime / minWorkTime
integer
Same pattern, for work time (service + drive/travel combined). RESOURCE_PERIOD_MAX_WORK_TIME (hard) / RESOURCE_PERIOD_MIN_WORK_TIME (medium).
rule.maxJobComplexity / minJobComplexity
integer
Cap/floor on the summed job.complexity of jobs completed in the period. RESOURCE_PERIOD_MAX_COMPLEXITY (hard) / RESOURCE_PERIOD_MIN_COMPLEXITY (medium).
rule.jobTypeLimitations
object (map of string → integer)
Maximum count of jobs per job.jobTypes value, for the period. Hard (JOBTYPE_VIOLATION). The equivalent shift.jobTypeLimitations applies the same cap per shift instead of per period.
rule.groupTag
string
Turns a per-resource rule into a shared pool: every resource declaring an identical rule (same period, same field values) under the same groupTag shares one bound across all of them, instead of each resource getting its own. A resource with a mismatched declaration is silently excluded from the pool.
Period-rule penalties use a fixed internal weight — none of them currently expose a dedicated weights field.

Fairness and vehicle usage

Solver-wide objectives that shape how work is spread and how many resources get used, controlled through options + weights rather than per-job/per-resource fields.
options.fairWorkloadPerTrip
boolean
default:"false"
Balance service time across all resources and trips within options.workloadSensitivity tolerance. Soft (FAIR_WORK), scaled by weights.workloadSpreadWeight / costs.workloadImbalanceCostFactor.
options.fairWorkloadPerResource
boolean
default:"false"
Same balancing, aggregated per resource across all its trips/days instead of globally. Soft (FAIR_TOTAL_WORK).
options.fairComplexityPerTrip / fairComplexityPerResource
boolean
default:"false"
Same fairness mechanism, applied to job.complexity instead of service time. Soft (FAIR_COMPLEXITY_PER_TRIP / FAIR_COMPLEXITY_PER_RESOURCE). The tolerance is fixed internally, not currently exposed as an option.
options.minimizeResources
boolean
default:"false"
When true, consolidate jobs onto fewer active resources, even at the cost of more travel time. Soft (RESOURCE_USAGE), scaled by weights.minimizeResourcesWeight (units: seconds of travel time an extra resource is “worth” — e.g. 3600 means one resource ≈ 1 hour of travel).
job.priority
integer
default:"1"
When options.partialPlanning leaves jobs unserved, higher-priority (× duration) jobs are penalized more for being left out, biasing the solver to serve them first. Medium (UNSERVED_JOBS), scaled by weights.priorityWeight.
weights.asapWeight
integer
Pushes jobs toward earlier days/shifts in the planning period when possible. Soft (JOB_DAY_INDEX).
job.hard / job.hardWeight
boolean / integer
In partial-planning mode, marks a job that must be included in the solution rather than left on a dummy trip. Hard (HARD_JOBS), scaled per-job by hardWeight.

Geographic optimization

options.jobProximityRadius
integer
default:"0 (disabled)"
Jobs within this radius (meters) of each other are treated as geographic neighbors; the solver penalizes visiting a job whose immediately preceding stop isn’t one of its neighbors. Soft (JOB_PROXIMITY), scaled by weights.jobProximityWeight.
options.jobProximityDistanceType
enum
default:"HAVERSINE"
HAVERSINE (straight-line, fast) or REAL (routing-engine distance, slower but accurate) for the proximity radius check above.
options.enableClustering
boolean
default:"false"
Penalizes overlapping bounding boxes between any two routes, encouraging geographically distinct territories per resource. Soft, scaled by weights.clusteringWeight and buffered by options.clusteringThresholdMeters (default 10000m). Not currently surfaced in the score-explanation breakdown.

VRP Constraint System

The conceptual model: hard/medium/soft levels, weight tuning, and debugging conflicts.

Advanced Constraints

Deeper walkthroughs of drive-time limits, tag matching, and vehicle restrictions.

Request Schema

Full field reference for OnRouteRequest.

Unassigned Reasons

Why a job was left unserved — surfaces which of these constraints blocked it.