Hard constraints
A hard constraint rules out any solution that violates it. If the engine cannot find a way to serve a stop without breaking one, that stop is left unserved rather than the constraint being bent. Typical hard constraints in the Kardinal model:authorizedTimeWindows— the engine never plans a stop outside these windows. If none of a stop’s authorized windows can be reached, the stop is unserviceable. This is why the documentation recommends making authorized windows as wide as realistically possible: they define the outer bound of what’s even considered a valid visit, not a target.requiredSkills— a resource must have every skill an order requires, or it cannot be assigned to it (for example, a tailgate-truck requirement).- Capacities — a stop’s
capacitiesmust fit within what a resource can carry (and what it has left after prior stops); there is no partial match. - Order structure —
successiveStops(stops of an order must be visited consecutively),maxStopSpan(maximum time between a pickup and its delivery), andremovalStrategy: "lifo"(a resource can only unload the last thing it loaded) are all structural constraints the engine always respects strictly. workingTimeWindow/maxWorkingDuration/maxDistanceInKmon a resource — bounds on when and how much a resource can work, which the engine never exceeds.
A contractual window is not automatically a hard one
It’s tempting to model a client-facing delivery window asauthorizedTimeWindows because it’s contractual — but “contractual” and “hard” answer two different questions. The question that decides which one to use isn’t “is this window written into an agreement?” — it’s “what should happen if the fleet can’t hit it?”
- If arriving late is undesirable but not disqualifying — a school delivery running 20 minutes late is still worth making — model it as
preferredTimeWindows. The engine tries to hit it and counts any late arrival asdelay, but it won’t drop the stop just because it can’t make the window exactly. A preferred window only tolerates lateness, never earliness: the engine never plans a stop before the start of its preferred window. If arriving before the stated opening hour is acceptable, a preferred window won’t give you that — widen the window instead, moving itsbeginback to the earliest time a visit is genuinely acceptable. - Only use
authorizedTimeWindowswhen a visit outside the window is truly unserviceable — a site that’s physically closed outside those hours, a security checkpoint that won’t admit a vehicle early, a customer who refuses the delivery.
authorizedTimeWindows; the second is preferredTimeWindows built from the exact same hours. Don’t infer which one applies from the mere presence of an opening-hours column, and don’t treat “the site has hours” and “the site enforces those hours as a hard cutoff” as the same claim — they aren’t. If nothing in the data or the client context states the actual dispatch tolerance (an explicit penalty for a late arrival, a fact like “closes and locks the gate,” a confirmed refusal policy), that tolerance is exactly the kind of open question this page already asks you to check (“what should happen if the fleet can’t hit it?”) — treat it as unresolved and default to preferredTimeWindows rather than assuming hard.
Getting this wrong in the hard direction has a silent, expensive failure mode: every stop whose contractual window can’t be reached exactly is dropped and reported as an unaffectedStopIds entry instead of being delivered late. If most or all of your delivery windows come from a contract or SLA, default to preferredTimeWindows and confirm with the business which windows, if any, are truly non-negotiable — don’t assume “contractual” implies “hard.” See Handling infeasibility for how to recognize this failure pattern once it happens, or the requirements briefing checklist for the question to ask up front so this doesn’t need to be inferred later.
Soft constraints
A soft constraint has a cost, not a wall. The engine tries to satisfy it, but accepts a violation if that’s what it takes to do better on a higher-ranked objective.preferredTimeWindows— if a stop can’t be reached inside its preferred window, it isn’t dropped: the gap between the planned time and the window is counted as delay, which theminimizeDelayobjective then tries to reduce. The engine automatically intersects authorized and preferred windows, so a preferred window is only ever a tighter target inside an authorized one, never a way to widen it.priorityon resources and orders — priority is a soft, graduated dial rather than a binary include/exclude flag. Lower numbers matter more (priority0outranks priority3), and priority levels are strict tiers: the engine gives up any number of lower-priority orders to plan one more of a higher priority, and leaves the lowest-priority resources unused first (see Objectives split by priority). Nothing is hard-coded as “always excluded.” Marking an order"optional": trueisn’t just a coarser version of the same idea: it also moves the order to a different objective. Mandatory stops count towardsmaximizeMandatoryStops— however low theirpriority— and optional stops only towardsmaximizeOptionalStops, so an optional stop is only planned if that objective is in your list (it is by default, but a custom list can leave it out). In the default order the first comes beforeminimizeResourceswhile the second comes after it. So the engine can add a vehicle to plan a mandatory stop, but never just to plan an optional one.preferredStopTagson a resource — the soft counterpart torequiredSkills. Stops whose tags match are preferably given to that resource, scored through themaximizePreferredStopsobjective (in the default list), but another resource can still serve them if that does better on a higher-ranked objective.overlappingCapacitiesByStopTag(plan level) — a target maximum number of resources simultaneously present at stops sharing a tag, such as a depot’s loading docks. Overruns are minimized byminimizeOverOverlappingCapacitiesOnStops, according to its position inobjectives.avoidEarlyLoadingsByResourceTag(plan level) oravoidEarlyLoadings(per resource) — declares stop tags, and optionally capacities, at which early loading should be avoided. These declarations only take effect through theminimizeEarlyLoadingsobjective, which isn’t in the default list: add it to your objectives explicitly, or they have no effect.
lateDeparture isn’t a soft constraint: it isn’t traded off against any objective, it only decides how each resource’s departure is timed. With lateDeparture: false (the default), resources leave as early as their workingTimeWindow allows, which can create idle waiting time before a later stop window. With lateDeparture: true, the engine shifts a resource’s departure as late as possible while still arriving at each stop as early as possible.
How the engine arbitrates conflicts
Two mechanisms do the arbitration, and both come from how the objectives are ordered:- Lexicographic objectives decide what “better” means when trade-offs are unavoidable — for instance, whether protecting mandatory stops matters more than minimizing delay, or the reverse, depending on how you’ve ordered your
objectiveslist. - Priority elimination decides who gets sacrificed first when something has to give — the lowest-priority resources or orders absorb the impact before higher-priority ones are touched.
Why a stop, not always the problem, becomes infeasible
Kardinal’s model rarely declares an entire plan infeasible as a single event. Instead, the engine returns a solution that is valid for everything it could place, and reports the rest explicitly:unaffectedStopIds— stops that could not be planned within the hard constraints (no authorized time window reachable, no resource with matching skills/capacity/availability, etc.).unusedResourceIds— resources that ended up with nothing assigned to them, generally becauseminimizeResourcesoutranks using them, or because none of the remaining orders match their constraints.tours[].isValid— a per-resource flag reflecting whether that specific tour respects all hard constraints.
200 with a solution, just one where some stops or resources are left out. Diagnosing which hard constraint caused a given stop to end up in unaffectedStopIds is covered in the Handling infeasibility how-to guide.
See also
- Handling infeasibility — diagnosing which hard constraint left a specific stop unplanned.
- Requirements briefing — the questions to ask up front so hard-vs-soft doesn’t need to be inferred later.
- Modeling advanced constraints — worked examples for the fields named throughout this page.

