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

# Swap-body and container-exchange orders

> Model fleets that carry one exchangeable unit at a time and swap it at each site: skip trucks, tanker swaps, container exchange.

A different, unrelated vertical also produces multi-stop orders on a single resource: fleets where the vehicle carries exactly one exchangeable unit at a time — a container, a skip ("benne"), a tank — and each trip's job is to exchange whatever unit is already at a site for the next one, then haul the unit away to its destination (a disposal site, a treatment plant, a depot). Roll-off/skip trucks and tanker/cistern-swap vehicles are the two most common examples. This is a distinct pattern from the [depot-reload pattern](/guides/multi-trip-tours) — it isn't about a vehicle running low on capacity and returning to top up, it's about a vehicle that only ever carries one unit and needs to swap it out at (or near) the point of use.

## Two stop shapes, decided by what actually happens at the site

Whether a given exchange needs two stops or three is a question about the data, not a stylistic default to pick between:

* **Three stops, when an exchange happens at the origin.** The vehicle already has a unit to drop off before it can take on the next one: a `delivery` stop drops the returning/empty unit, immediately followed by a `pickup` stop that takes on the next unit — both **at the same position** — and then a third stop, at a *different* position, delivers/hauls the newly picked-up unit to its destination.
* **Two stops, when no exchange happens at the origin.** If the vehicle already has nothing to drop off at the pickup site (for example it starts the trip already running with an empty unit loaded, or the site has nothing to collect first), the order is a plain `pickup` at the origin followed by a `delivery` elsewhere — the same shape used elsewhere in this guide and in [Data model](/reference/data-model#order-stop).

Don't standardize on one shape across a fleet or a dataset. Each order should reflect what its own site actually requires: if the source data (or the client) confirms a real drop-off-then-pickup happens at that origin, model three stops; if nothing is dropped off there, two stops is the correct — not a simplified — model.

## Preventing double-booking: three legitimate mechanisms

A vehicle that carries only one unit at a time must never be assigned a second pickup before the current unit is off-loaded. There are three legitimate ways to enforce that, and they are not mutually exclusive — a real payload may use one, several, or all together, depending on what else the data needs to express:

1. **`successiveStops` alone.** Set `successiveStops: true` on the order (see [Order, Stop](/reference/data-model#order-stop)) with no tags, no `setupDurations`, and no capacity signal at all. Array order plus `successiveStops` is already enough to guarantee the drop-off and the pickup happen back-to-back, in that order, with nothing else interleaved — which is all that's needed to prevent double-booking when the exchange itself has no real handling-time cost worth modeling and there's no capacity dimension the exchange needs to express. This is often the simplest correct option, and in practice the one actually used, when neither of the other two mechanisms' extra machinery corresponds to anything real in the data — don't reach for tags + `setupDurations` by default on every swap-body order just because the pattern involves an exchange; reserve it for exchanges that genuinely carry their own timing cost (see the next option).
2. **Tags + `setupDurations`, combined with `successiveStops`.** Tag the relevant stops with something that captures the vehicle's state (for example a tag meaning "carrying a unit" versus "empty"), and use the plan-level `setupDurations` array to charge a duration whenever a resource transitions from a stop tagged one way to a stop tagged another way — see the field's own definition in the [API reference](/api-reference/plan/create-a-plan) (`SetupDuration`: `fromStopTag`, `toStopTag`, `setupDuration`). Set `successiveStops: true` on the order so nothing from another order can be interleaved between the drop-off and the pickup. Together, these charge a realistic handling/swap duration for the exchange itself and guarantee the two visits happen back-to-back, in that order.
3. **Signed `capacities` pairs.** Track the unit with two capacity dimensions that move in lockstep and opposite directions — one incrementing, one decrementing at each step (for example a stop that changes `capacities` by `{"unit": 1, "missingUnit": -1}`, and the matching drop-off stop by the inverse). The running capacity balance itself then prevents a second pickup from happening before the current unit is dropped off, because doing so would push a dimension out of range. This is the same family of mechanism as [shared capacity pools](/reference/data-model#shared-capacity-pools), applied within a single order rather than across several.

A bare count-based ceiling on its own — for example `capacities: {"units": 1}` on the resource and on every stop that adds or removes a unit, with no `successiveStops`, no tags/`setupDurations`, and no signed pair — is **not**, by itself, a correct model of this pattern once a real drop-off-then-pickup step exists at the same site. A capacity ceiling only bounds how many units are aboard at any point; it doesn't add the exchange's own handling time, and nothing about it forces the drop-off and the pickup to happen back-to-back without another order's stop landing in between. Use it only when the trip is genuinely the simpler two-stop shape described earlier (no exchange at the origin) — and even then, prefer whichever of the three preceding mechanisms the client's data actually calls for once an exchange is involved.

## Example: tags + `setupDurations` for a three-stop exchange

```json theme={null}
{
  "setupDurations": [
    { "fromStopTag": "unit:empty", "toStopTag": "unit:loaded", "setupDuration": "PT20M" },
    { "fromStopTag": "unit:loaded", "toStopTag": "unit:empty", "setupDuration": "PT20M" }
  ],
  "orders": [
    {
      "id": "order-container-exchange-1",
      "successiveStops": true,
      "stops": [
        {
          "type": "single",
          "id": "order-container-exchange-1-dropoff",
          "position": { "lat": 48.85, "lon": 2.35 },
          "kind": "delivery",
          "operationDuration": "PT1M",
          "tags": ["unit:empty"]
        },
        {
          "type": "single",
          "id": "order-container-exchange-1-pickup",
          "position": { "lat": 48.85, "lon": 2.35 },
          "kind": "pickup",
          "operationDuration": "PT1M",
          "tags": ["unit:loaded"]
        },
        {
          "type": "single",
          "id": "order-container-exchange-1-haul",
          "position": { "lat": 48.70, "lon": 2.10 },
          "kind": "delivery",
          "operationDuration": "PT5M",
          "tags": ["unit:loaded"]
        }
      ]
    }
  ]
}
```

The `PT20M` figure here is illustrative, not a universal constant — it stands in for whatever real handling/administrative time a given client's exchange actually takes, which you should confirm against their own operation rather than assume. Because the drop-off and pickup stops share the same position, the resource's travel time between them is zero; the `setupDurations` entry is what charges the exchange's own realistic duration on top of that, and `successiveStops: true` guarantees nothing else gets scheduled in between. The final haul stop keeps the `unit:loaded` tag since the resource is still carrying the unit it just picked up — no further transition (and so no further setup duration) is charged until it next transitions to a differently tagged stop.

Option 2 (signed `capacities` pairs) doesn't need a new worked example here — it follows the same [Capacities](/reference/data-model#capacities) mechanics already documented, just applied with two dimensions per unit instead of one.

## Skipping operation duration for a pre-loaded resource

A resource that starts its shift already carrying a swapped unit shouldn't pay the loading time for it again. `operationDurationPolicies` excludes specific operation durations from a resource's tour:

```json theme={null}
{
  "resources": [
    {
      "id": "trailer-swap-1",
      "operationDurationPolicies": [
        { "policy": "withoutFirstOperationDuration", "stopTags": ["unit:loaded"] }
      ]
    }
  ]
}
```

`withoutFirstOperationDuration` ignores the operation duration only for the first group of tagged stops the resource encounters in its tour — the case of a driver starting already loaded. `withoutOperationDurations` ignores it on every matching stop for the whole tour instead. Use the plan-level `operationDurationPoliciesByResourceTag` variant when the rule should apply to every resource sharing a tag rather than to one named resource.

## See also

* [Multi-trip tours](/guides/multi-trip-tours) — the depot-return/reload pattern, a different and unrelated multi-stop-order pattern.
* [Data model](/reference/data-model#order-stop) — `Order`/`Stop` field reference, including `successiveStops` and `tags`.
* [Modeling advanced constraints](/guides/advanced-constraints) — heterogeneous capacities, skills, breaks, and multiple time windows.
