What the engine optimizes
Optimization is driven by an ordered list of objectives. The engine processes them lexicographically: it fully prioritizes improving the first objective before considering the second, the second before the third, and so on. Order reflects business priority, not just a weighted average. The default sequence covers most cases (99% according to Kardinal):minimizeCosts is a no-op unless at least one resource declares a cost object (per km, per capacity unit, fixed cost — see Data model). Including it in your objectives list without configuring any resource’s cost doesn’t error, it just has nothing to optimize — the objective silently falls through to the next one in the list. If no resource in your plan declares a cost object, omit minimizeCosts from the list entirely rather than keeping a guaranteed no-op entry.minimizeResources before minimizeDelay tells the engine that fleet size matters more than on-time delivery — it will happily let stops run late to avoid dispatching an extra vehicle. Putting maximizeMandatoryStops first (the recommended default) means the engine will never leave a mandatory stop unplanned just to save a vehicle or some kilometers.
This isn’t limited to the two objectives called out above — every adjacent pair in the list carries the same kind of trade-off, including minimizeCosts vs. minimizeResources: placing minimizeCosts first prioritizes total cost even if reaching it takes an extra vehicle, while placing minimizeResources first caps fleet size first and lets cost settle wherever that leaves it. Neither position is a safe, order-independent default — the two are not interchangeable, and swapping them changes which solutions the engine considers better. Decide the position of every objective in your list deliberately, based on which trade-off matters more for the operation you’re modeling, rather than assuming any pair not spelled out by name in this page is order-insensitive.
Treat the default sequence above as a starting point to re-check against your specific plan’s constraint structure, not a template to copy unconditionally — a default built for the general case can include an objective that’s a no-op for your case, or omit one that matters for it. For example, check whether every window in your plan is a hard
authorizedTimeWindows constraint: if so, there’s no “delay” left to minimize, and minimizeDelay has nothing to do (see Hard vs soft constraints). Re-derive the list from what your plan’s own constraints actually look like, rather than reusing a default sequence unchanged because it “usually works.”minimizeDistance, minimizeWorkingDuration, maximizeOptionalStops (for stops marked "optional": true), maximizePreferredStops (favors assigning tagged stops to resources with matching preferredStopTags), minimizeLargestTourDuration (caps how unbalanced the longest single tour can get relative to the rest of the fleet, rather than only minimizing the total across all tours), and custom cost-based objectives for advanced pricing models (overtime, per-stop-type costs, etc.).
The quality vs. computation time trade-off
There is no single “optimize until done” call — instead, you control how much time the engine is allowed to spend viamaxOptimizationDuration (an ISO 8601 duration, e.g. "PT10M"):
- The engine is guaranteed to never regress: each new solution it publishes is at least as good as the previous one on the objective sequence above. There’s no risk of “rolling back” to something worse.
- The longer you let it run, the better the solution can get — but returns diminish. If the engine hasn’t found an improvement in a while, it considers itself done, even before
maxOptimizationDurationelapses. maxOptimizationDurationonly counts time actually spent searching. It excludes queueing time (if no worker is free), and the time spent building the underlying math problem (fetching travel times, etc.). Add a margin before you fetch a solution to account for this.
Static — one shot, generous duration
Static — one shot, generous duration
Set a long
maxOptimizationDuration (e.g. PT30M), submit the plan, and fetch the solution after that window (plus margin). Simple, but you wait for the full window even if the engine converged early.Iterative — short duration, manual restarts
Iterative — short duration, manual restarts
Use a short
maxOptimizationDuration and, if the solution isn’t good enough, restart optimization with PUT /agencies/{agencyId}/plans/{planId}/running (body true). Updating the plan has the same restarting effect. Restarting won’t help if the duration is too short for the problem size, or if the engine already considers the current solution final.Polling — preferred
Polling — preferred
Poll the plan’s
status field and the solution’s objective values, and decide for yourself when the result is “good enough” for your business — without waiting for the engine to fully settle. This is the most responsive pattern and the one Kardinal recommends.status field tracks a plan through its lifecycle — waiting room (if you’re over your max simultaneous running plans quota), creation (fetching travel times, building the problem), optimization, and, if predictive traffic is enabled, an asynchronous traffic-fetching stage running in parallel:
Continuous and interactive optimization
Submitting a plan again with the sameid doesn’t start a new problem from scratch — it tells the engine “this is the same problem, here’s what changed.” The version number increments automatically, and the engine degrades the previous solution just enough to remain valid for the new data, then keeps improving from there. This first optimization on a given plan layout is slower (the engine is learning the problem’s shape); subsequent updates are typically much faster.
Because of this, if you update a plan while an older version is still optimizing, the engine will not let two versions be considered “optimized” at the same time — it stops work on the stale version in favor of the newest one.
What you control vs. what the engine controls
You control: the objectives list and its order,maxOptimizationDuration, when to restart or stop optimization (running: false), resource/order priority, and lateDeparture (whether the engine compacts routes to minimize idle time or starts tasks as early as possible).
The engine controls: the actual search strategy, and the guarantee that solution quality never regresses between versions. Problem complexity — and therefore how much maxOptimizationDuration you should budget — is mostly driven by the number of stops and resources, whether traffic-aware vehicle profiles (withTraffic: true) are used, and whether advanced constraints (alternative stops, LIFO removal strategy, overlapping-capacity limits) are in play; some of these switch the engine to alternative algorithms that are markedly slower.
Event-driven notifications (event bus / webhooks) so you don’t have to poll are not implemented yet — contact Kardinal if this is a requirement for your integration.