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

# Building plans with typed models

> Describe your fleet and orders with the SDKs' typed models, load a JSON payload you already have, and validate a plan locally before you spend any credits.

A plan is the problem you submit: `resources` (vehicle-driver pairs), `orders` (each grouping one or more stops), and optional constraints and objectives. The SDKs don't add a modeling layer of their own: their models are generated from the API's OpenAPI specification, field for field. This page covers how to build a plan in code; what each field means is documented once, in the [API Reference](/api-reference/plan/create-a-plan) and the [Data model](/reference/data-model).

## Typed models and the API spec

<Tabs>
  <Tab title="Python">
    Models are `pydantic` classes, under `kardinal.routing.models` (`Resource`, `SingleStop`, `Position`, `TimeWindow`, and every other schema of the spec). `PlanCreate` and `Order` come from `kardinal.routing.plans`. A model refuses a misspelled key and a value of the wrong type as soon as you build it.
  </Tab>

  <Tab title="TypeScript">
    A plan is a plain object, typed by the package's exports: `PlanCreateInput` for a plan you build, and `Resource`, `SingleStop`, `Position`, `TimeWindow`, and the other schemas of the spec. Types are checked at compile time; `submit` also validates the plan at run time, with `zod`, before sending it.
  </Tab>
</Tabs>

Field names keep the API's camelCase in both languages (`workingTimeWindow`, `operationDuration`): what you write is what the API receives. Only method and argument names follow each language's convention, such as `plans.get_status` in Python and `plans.getStatus` in TypeScript.

## `PlanCreate` and `Plan`

Two shapes describe a plan, as in the API:

* **`PlanCreate`** is what you submit. It has no `id`: the server assigns one. Setting `id` yourself is refused before sending.
* **`Plan`** is what `submit`, `get`, and `update` return: the same content plus the fields the server manages (`id`, `version`, `status`, `state`, `agencyId`). To change a plan, modify the `Plan` you read and pass it to `update`.

A plan has no top-level list of stops: stops live inside `Order.stops`, and `Plan.orders` is the list of orders. An order groups the stops that must be served by the same resource, such as a pickup and its delivery.

## Starting from the example plan

The fastest way to a valid plan is to adapt the example both SDKs ship: two resources, eight single-stop orders, and a 10-second optimization. Each call returns a fresh plan you can modify freely.

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

  plan = examples.small_delivery_plan()
  plan.orders.append(
      Order(
          id="order-9",
          stops=[
              models.SingleStop(
                  type="single",
                  id="stop-9",
                  position=models.Position(lat=48.8584, lon=2.2945),
                  operationDuration="PT5M",
              )
          ],
      )
  )
  ```

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

  const plan = examples.smallDeliveryPlan();
  plan.orders = [
    ...(plan.orders ?? []),
    {
      id: "order-9",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "stop-9",
          position: { lat: 48.8584, lon: 2.2945 },
          operationDuration: "PT5M",
        },
      ],
    },
  ];
  ```
</CodeGroup>

The example's working window starts tomorrow at 8:00, computed when you call it, so the plan never refers to a date already past.

## Loading an existing JSON payload

If you already have a plan as JSON — exported from your own system, written by hand, or copied from a [modeling example](/case-studies/overview) — load it into the SDK's model rather than rebuilding it:

<CodeGroup>
  ```python Python theme={null}
  from pathlib import Path

  from kardinal import Kardinal
  from kardinal.routing.plans import PlanCreate

  plan = PlanCreate.model_validate_json(Path("plan.json").read_text())
  created = Kardinal().routing.plans.submit(plan)
  ```

  ```ts TypeScript theme={null}
  import { readFileSync } from "node:fs";
  import { Kardinal, parsePlanCreate } from "@kardinal-ai/sdk";

  const plan = parsePlanCreate(JSON.parse(readFileSync("plan.json", "utf8")));
  const created = await new Kardinal().routing.plans.submit(plan);
  ```
</CodeGroup>

Loading runs the same checks as `submit`: a missing required field, a wrong type, or a misspelled key fails here, with its path in the plan, before anything is sent.

## Validating locally before you spend credits

Every check below runs on your machine, with no API call and no credits:

* **Structure and types**: building a Python model, or `parsePlanCreate` in TypeScript, fails on a missing required field or a value of the wrong type.
* **Unknown keys**: `submit` refuses a key no model declares, and lists each one with its path and the closest real field name. To run that check yourself before sending, call `unknown_fields(plan)` (Python, from `kardinal.routing`) or `unknownFields(plan)` (TypeScript).
* **A plan file**: `kardinal routing plans validate plan.json` runs both checks on a JSON file, from a shell or a CI job — see [Local validation](/sdks/cli#local-validation).

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

  for field in unknown_fields(plan):
      print(field)  # plan.resources[0].maxWorkingDurationn  (did you mean maxWorkingDuration?)
  ```

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

  for (const field of unknownFields(plan)) {
    console.log(field); // plan.resources[0].maxWorkingDurationn, with the closest field name
  }
  ```
</CodeGroup>

These checks catch mistakes in the shape of the plan. They don't check its business consistency: whether the fleet can carry the total demand, or whether a time window is reachable, is the engine's call, and shows up in the solution as unplanned stops. See [Handling infeasibility](/guides/handling-infeasibility).

The two SDKs differ in the depth of their checks:

<Tabs>
  <Tab title="Python">
    Every object of the plan is checked, including the rarer nested ones (`cost`, `breaks`, `objectives`, constraints). `pydantic` accepts a string holding a number or a boolean, such as `"1"` or `"true"`, and converts it.
  </Tab>

  <Tab title="TypeScript">
    Resources, orders, stops, and the plan's top-level fields are checked with the same rules as in Python. The inside of the rarer nested objects (`cost`, `breaks`, `objectives`, constraints) isn't checked, and a misspelled key there isn't reported. `zod` refuses a string where a number or a boolean is expected.
  </Tab>
</Tabs>

## Where field documentation lives

The [API Reference](/api-reference/plan/create-a-plan) is the reference for every field: its meaning, unit, default, and constraints. The SDKs carry the same descriptions, generated from the same specification, so your editor shows them on hover. If the two ever disagree, the API Reference is right: report it as an issue on the SDK's repository.

## Using the SDKs with AI coding agents

An agent writing integration code gets the most out of the typed models: wrong field names and types fail fast, with the exact path, instead of producing a valid but wrong plan.

* Run [`kardinal init`](/sdks/cli#project-setup-for-ai-coding-assistants) in your project: it writes `.kardinal/SKILL.md`, a short pointer that tells a coding assistant the SDK is there and how to call it.
* Point the agent at the [AI agent modeling checklist](/getting-started/agent-modeling-checklist) before it models your operation.
* Have it validate every payload locally, with the checks on this page, before submitting anything with a production key.


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