Skip to main content
Every field in a Kardinal plan falls into one of two categories: constraints the engine must respect, and preferences it will respect if it can. Knowing which is which explains why a stop sometimes goes unplanned even though “the API didn’t return an error.”

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 will never plan 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 (e.g. a tailgate-truck requirement).
  • Capacities — a stop’s capacities must fit within what a resource can carry (and what it has left after prior stops); there is no partial match.
  • Order structuresuccessiveStops (stops of an order must be visited back-to-back), maxStopSpan (maximum time between a pickup and its delivery), and removalStrategy: "lifo" (a resource can only unload the last thing it loaded) are all structural constraints the engine cannot relax.
  • workingTimeWindow / maxWorkingDuration / maxDistanceInKm on a resource — bounds on when and how much a resource can work, which the engine will not exceed.

A “contractual” window is not automatically a hard one

It’s tempting to model a client-facing delivery window as authorizedTimeWindows 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 missing the window is undesirable but not disqualifying — a school delivery running 20 minutes late is still worth making — model it as preferredTimeWindows. The engine will try to hit it and count any miss as delay, but it won’t drop the stop just because it can’t make the window exactly.
  • Only use authorizedTimeWindows when 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 will refuse the delivery.
A stated opening/closing hour is necessary, but not sufficient, evidence for “hard.” A data column that gives a site’s opening and closing hours only tells you the site has hours — it doesn’t by itself tell you what happens if a vehicle arrives outside them. That’s a separate fact: does the site actually turn a late-arriving vehicle away (or refuse to let it start early), or does it simply prefer being served within those hours while still accepting a late visit? The first case is genuinely 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.

Soft constraints

A soft constraint has a cost, not a wall. The engine will try to satisfy it, but will accept 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 the minimizeDelay objective 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.
  • lateDeparture: false (the default) — the engine prefers starting tasks as early as possible, which can create idle time between non-contiguous windows; this is a scheduling preference, not a rule.
  • priority on resources and orders — priority is a soft, graduated dial rather than a binary include/exclude flag. Lower numbers matter more (priority 0 outranks priority 3); the engine will drop any number of lower-priority resources or orders to protect higher-priority ones, but nothing is hard-coded as “always excluded.” Marking an order "optional": true is a simpler, coarser version of the same idea.

How the engine arbitrates conflicts

Two mechanisms do the arbitration, and both come from how the objectives are ordered:
  1. 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 objectives list.
  2. 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 because minimizeResources outranks 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.
In other words, “infeasible” in Kardinal is usually a property of a specific stop or resource, not a rejection of the whole request — the API will still return 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.