> ## 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.

# Plan lifecycle

> Submit a plan, wait for or track its optimization, re-optimize it after a change, stop or restart it, then list and delete plans with the Kardinal SDKs.

A plan goes through the same steps whatever the SDK: you submit it, the engine optimizes it, you read the solution, and you update the plan as reality changes. This page follows those steps. See [How the optimization engine works](/concepts/how-the-optimization-engine-works) for what happens on the engine's side.

## Submitting a plan

<CodeGroup>
  ```python Python theme={null}
  from kardinal import Kardinal
  from kardinal.routing import examples

  k = Kardinal()
  plan = k.routing.plans.submit(examples.small_delivery_plan())
  print(plan.id, plan.version)
  for warning in plan.warnings:
      print(warning.code, warning.properties)
  ```

  ```ts TypeScript theme={null}
  import { Kardinal, examples } from "@kardinal-ai/sdk";

  const k = new Kardinal();
  const plan = await k.routing.plans.submit(examples.smallDeliveryPlan());
  console.log(plan.id, plan.version);
  for (const warning of plan.warnings) {
    console.log(warning.code, warning.properties);
  }
  ```
</CodeGroup>

`submit` creates the plan and starts its optimization in the same call. It returns the stored plan, with the `id` the server assigned and the [warnings](/sdks/errors-and-warnings#warnings-on-successful-calls) about anything the API amended. Submitting consumes credits with a production key: see [Pricing and credits](/reference/pricing-and-credits).

`submit` is never retried automatically, even after a network error: the API has no idempotency key, so a retry could create the plan twice. If a `submit` fails without a response, check with `plans.list` whether the plan exists before submitting again.

## Waiting for the solution

<CodeGroup>
  ```python Python theme={null}
  solution = k.routing.solutions.wait(plan.id)            # no time limit
  solution = k.routing.solutions.wait(plan.id, timeout=600)  # at most 10 minutes
  print(solution.final_state)
  ```

  ```ts TypeScript theme={null}
  let solution = await k.routing.solutions.wait(plan.id); // no time limit
  solution = await k.routing.solutions.wait(plan.id, 600_000); // at most 10 minutes
  console.log(solution.finalState);
  ```
</CodeGroup>

`wait` follows the plan until the optimization of its current version is over, then returns the solution:

* It reads the plan's state every 10 seconds at first, stretching to every 60 seconds on long optimizations, and fetches the solution only once the plan is done. It doesn't sleep through the whole `maxOptimizationDuration`: the engine often stops earlier, once it stops finding improvements.
* The solution's final state says how the optimization ended: `optimized`, or `interrupted` or `stopped` when it was cut short. Check it before treating the result as final.
* It fails with `KardinalOptimizationError` when there's no solution to return: `NO_SOLUTION` (the version ended without one), `PLAN_SUPERSEDED` (a newer version was submitted meanwhile), or `OPTIMIZATION_STALLED` (still optimizing 5 minutes past its `maxOptimizationDuration`). A plan deleted while waiting fails with `KardinalNotFoundError`.
* The optional time limit (`timeout` in seconds in Python, `timeoutMs` in milliseconds in TypeScript) fails with `KardinalTimeoutError` once elapsed, after checking the state one last time.

## Tracking progress without blocking

To follow a plan without waiting, read its state and fetch the solution yourself:

<CodeGroup>
  ```python Python theme={null}
  state = k.routing.plans.get_status(plan.id)  # waiting, processing, optimizing, optimized, ...
  if state == "optimized":
      solution = k.routing.solutions.get(plan.id)
  objectives = k.routing.solutions.get_objectives(plan.id)  # objective values only
  ```

  ```ts TypeScript theme={null}
  const state = await k.routing.plans.getStatus(plan.id); // waiting, processing, optimizing, optimized, ...
  if (state === "optimized") {
    const solution = await k.routing.solutions.get(plan.id);
  }
  const objectives = await k.routing.solutions.getObjectives(plan.id); // objective values only
  ```
</CodeGroup>

`solutions.get` returns the current best solution right away, even while the engine is still improving it. Right after `submit`, it fails with a `404` until the engine has a first solution. Like any manual tracking, **read a plan's state no more than once every 10 seconds**.

## Waiting in a web backend or serverless function

`wait` is meant for scripts, notebooks, and workers. In a request handler, don't hold the request open while the engine runs:

* Submit the plan, store its `id`, and return right away.
* Read the state later — from a scheduled job, a queue worker, or the next request from your front end — and fetch the solution once the plan is done.
* A serverless function usually has a shorter time limit than an optimization: track the plan across invocations instead of waiting in one.

<Tabs>
  <Tab title="Python">
    The client is synchronous. In an `asyncio` app, run blocking calls in a thread (`await asyncio.to_thread(k.routing.solutions.wait, plan_id)`) so they don't block the event loop, or track the plan with `get_status`, as shown earlier.
  </Tab>

  <Tab title="TypeScript">
    `wait` doesn't block the event loop, but it keeps a promise pending for the whole optimization. Prefer `getStatus` calls spread over time when many plans run at once.
  </Tab>
</Tabs>

## Re-optimizing with `update`

When something changes — a new order, a cancelled one, a vehicle out of service — update the plan rather than submitting a new one. The engine continues from the current solution instead of starting over: see [Real-time re-optimization](/guides/real-time-reoptimization).

<CodeGroup>
  ```python Python theme={null}
  from kardinal.routing import models
  from kardinal.routing.plans import Order

  plan = k.routing.plans.get(plan.id)
  plan.orders.append(
      Order(
          id="order-10",
          stops=[models.SingleStop(type="single", id="stop-10", position=models.Position(lat=48.86, lon=2.35))],
      )
  )
  plan = k.routing.plans.update(plan)  # same id, new version
  solution = k.routing.solutions.wait(plan.id)
  ```

  ```ts TypeScript theme={null}
  let current = await k.routing.plans.get(plan.id);
  current.orders = [
    ...(current.orders ?? []),
    { id: "order-10", priority: 0, stops: [{ type: "single", id: "stop-10", position: { lat: 48.86, lon: 2.35 } }] },
  ];
  current = await k.routing.plans.update(current); // same id, new version
  const updated = await k.routing.solutions.wait(current.id);
  ```
</CodeGroup>

* `update` replaces the whole plan: anything left out is removed. Start from the plan returned by `get`, `submit`, or a previous `update`, and modify it.
* The version increments, and the plan is re-optimized, only if the content actually changed. An identical update is a free no-op that keeps the same version, which makes replaying an `update` harmless.
* A field the API added after your SDK version, read by `get`, is kept and sent back, so updating with an older SDK doesn't delete it.
* On an archived plan, `force=True` (Python) or `{ force: true }` (TypeScript) unarchives it first; on any other plan, `force` has no effect.

## Starting and stopping optimization

<CodeGroup>
  ```python Python theme={null}
  k.routing.plans.stop(plan.id)   # stop the optimization in progress
  k.routing.plans.start(plan.id)  # restart it, on the same version
  ```

  ```ts TypeScript theme={null}
  await k.routing.plans.stop(plan.id); // stop the optimization in progress
  await k.routing.plans.start(plan.id); // restart it, on the same version
  ```
</CodeGroup>

Both keep the plan's content and version unchanged, and return a reference to the plan (`agencyId`, `planId`, `planVersion`). `force` unarchives an archived plan first, as for `update`.

## Listing plans

<CodeGroup>
  ```python Python theme={null}
  page = k.routing.plans.list(page=1, items_per_page=20)
  print(page.total, page.total_pages, page.next_page)

  for light_plan in k.routing.plans.iter_all():  # every page, one plan at a time
      print(light_plan.id, light_plan.state)
  ```

  ```ts TypeScript theme={null}
  const page = await k.routing.plans.list({ page: 1, itemsPerPage: 20 });
  console.log(page.total, page.totalPages, page.nextPage);

  for await (const lightPlan of k.routing.plans.iterAll()) {
    // every page, one plan at a time
    console.log(lightPlan.id, lightPlan.state);
  }
  ```
</CodeGroup>

Plans come in their light form, without resources and orders: call `get` for the full plan. Archived plans are left out by default; pass `archived="included"` or `"only"` (Python) or `{ archived: "included" }` (TypeScript) to change that. A page past the last one returns the last page: its `page` field gives the page actually returned. See [Paginating results](/guides/handling-large-volumes#paginating-results).

## Deleting a plan

<CodeGroup>
  ```python Python theme={null}
  k.routing.plans.delete(plan.id)
  ```

  ```ts TypeScript theme={null}
  await k.routing.plans.delete(plan.id);
  ```
</CodeGroup>

Deleting a plan removes it and its solution permanently, and stops any optimization in progress. It can't be undone.


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