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

# Real-time re-optimization

> Update a running plan after a delay, cancellation, or urgent order, so the engine repairs its solution instead of starting over, and choose how often to update.

There is no dedicated "re-optimize now" endpoint. A running plan is always subject to re-optimization the moment its data changes — see [Continuous and interactive optimization](/concepts/how-the-optimization-engine-works#continuous-and-interactive-optimization). Reacting to a disruption is a matter of updating the right part of the plan, not calling a separate recalculation API.

## Types of triggering disruptions

| Disruption | What changed | How to reflect it |
| - | - | - |
| A resource is running late | Its real availability window shrank | Update that resource's `workingTimeWindow` (or `departure`) in the plan's `resources` array, then `PUT /plans/{planId}` with the full plan. |
| An order is cancelled | A stop no longer needs to be served | Remove that order from the plan's `orders` array, then `PUT /plans/{planId}` with the full plan. |
| An urgent new order comes in | A new stop must be added mid-shift | Add the new order to the plan's `orders` array with a new `id`, then `PUT /plans/{planId}` with the full plan. |
| A specific resource/stop pairing must be excluded (for example, a breakdown makes a resource unable to reach a stop it was going to serve) | A resource can no longer serve a specific order | Give that resource and that stop a tag unique to the pair (for example `forbid:resource12-stop7`), add a `forbiddenAssignment` entry in `additionalConstraints` referencing it, then `PUT /plans/{planId}` with the full plan. |

Every one of these is the same operation: mutate the relevant part of your local copy of the plan, then `PUT` the whole plan back — there is no endpoint that targets a single resource or order in isolation anymore.

> [!WARNING]
>
> `PUT /plans/{planId}` replaces the whole plan: resend every resource and order, not only the changes. Anything omitted from the body is removed from the plan.

## How this differs from a full recalculation

Submitting any of the preceding updates keeps the plan's `id` unchanged, so it isn't treated as a new problem: the engine repairs the previous solution, adjusting it only as much as needed to be valid for the new data, then keeps improving from there — it does not restart optimization from scratch. This is normally much faster than the plan's original optimization, which had to learn the problem's shape for the first time.

If an older version of the plan is still optimizing when a disruption update lands, the engine drops the stale version in favor of the newest one — you never get two versions "optimized" at once.

The engine starts from the previous solution even when the previous optimization has already finished, and for an update sent hours or days later. It starts from the best solution it found for the plan, which it keeps for a limited time. The same applies when you restart the current version with [`PUT /plans/{planId}/running`](/api-reference/plan/stop-or-restart-the-optimization-of-a-plan). Starting from the previous solution doesn't freeze it: to keep stops that are already started or announced in place, lock them with `state.assignments` (see [Live supervision as a tour progresses](/guides/manual-assignment-and-supervision#live-supervision-as-a-tour-progresses)).

An update starts from scratch instead when no previous solution is available: once that limited time has passed, or once the plan has been archived. A plan that hasn't been updated for 14 days is archived automatically (see [Data retention](/concepts/data-security#data-retention)), and a `PUT` on an archived plan is refused unless you add `force=true`, which unarchives it first. That forced update always creates a new version and re-optimizes the plan, even if its content is identical, so every stop is billed again (see [Pricing and credits](/reference/pricing-and-credits#how-credits-work)). Don't add `force=true` by default.

An update also starts from empty tours when the previous tours break a `maxCumulatedCost` cap in `globalConstraints`, for example after you lower its `maximum` (see [Capping a fleet-wide cost](/guides/cost-modeling#capping-a-fleet-wide-cost-with-maxcumulatedcost)).

In all these cases, whether the update starts from scratch or from empty tours, the result is still valid, but the update can take as long as a first optimization, and the tours can change a lot.

There is no way to start a plan from another plan's solution: the stored solution belongs to the plan's `id`, and the API has no field to supply one.

An update only consumes credits for stops that are new or have changed — a new `id`, or a move of more than about 11 metres — and for all stops once per 24-hour period since the plan's creation. Changing resources or constraints costs nothing extra; see [Pricing and credits](/reference/pricing-and-credits#how-credits-work).

## Best practices for call frequency

* **Batch disruptions that arrive close together** into a single update where possible, rather than issuing one `PUT` per individual change — every update re-triggers optimization, and consecutive updates just cause the engine to keep abandoning a version it hasn't finished with yet.
* **Poll before you push another update.** Check the plan's `status`, or `GET /plans/{planId}/state` for a simpler single-field `optimized` check (see [How the optimization engine works](/concepts/how-the-optimization-engine-works#the-quality-vs-computation-time-trade-off)), to see whether the previous update has already settled; there's little value in sending a new disruption update while the engine is still mid-search on the last one, beyond the disruption itself needing to be reflected immediately.
* **Don't reduce `maxOptimizationDuration` for these updates** just because they feel like small edits — the field caps *this version's* remaining search time, and a busy fleet mid-shift can still take real time to re-settle around a disruption; size it the same way you would for the initial plan (see [Handling large volumes](/guides/handling-large-volumes#sizing-maxoptimizationduration-for-a-large-problem)).

## See also

* [How the optimization engine works](/concepts/how-the-optimization-engine-works) — what happens when a plan is updated with the same `id`, and the three integration patterns for retrieving a solution.
* [Re-optimizing with `update`](/sdks/plan-lifecycle#re-optimizing-with-update) — the same update from the Python or TypeScript SDK.
* [Handling infeasibility](/guides/handling-infeasibility) — diagnosing a disruption that leaves a stop unplannable rather than just delayed.
* [Manual assignment and supervision](/guides/manual-assignment-and-supervision) — locking stops in place so an update doesn't move them.
* [Data model](/reference/data-model) — an overview of `Resource` and `Order`, each linking to its schema in the API reference — the two objects most commonly touched by a disruption.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.