Skip to content

Multi-shift

Multi-Shift

Use multi-shift when a Driver works more than one Shift in the same request — several days, or a morning and afternoon Shift on the same day.

You still POST to /v2/vrp (or the polling equivalent). There is no separate endpoint. The request becomes a multi-shift request when every Driver has a multi_shift object instead of shift_start and shift_end.

The idea is in Concepts. This page is the request guide: where the multi_shift object goes, what belongs in it, and how reset differs from continue.

Reset versus continue

Reset

Monday
Depot
Jobs
Depot
Tuesday
Depot
Jobs
Depot

Continue

Day 1
Depot
Jobs
Stay put
Day 2
Same place
Jobs
End

Reset goes home every day. Continue starts Day 2 from where Day 1 stopped.

Where the multi_shift object goes

Multi-shift is not a single field. You add a multi_shift object in two places, and you keep start/end locations on the Driver as usual.

Where What you put there Required?
driver.multi_shift mode and shifts Yes — on every Driver
job.multi_shift serve_within (date windows for that Job) Only when the Job must fall on particular days
driver.start / driver.end Locations, same as a single-day request start is required; end is optional

Rules:

  1. Every Driver omits shift_start and shift_end and supplies multi_shift instead. You cannot mix the two shapes in one request, and you cannot put both on the same Driver.
  2. mode and shifts belong only on the Driver. Do not put them on Jobs.
  3. Jobs keep their usual fields (location, duration, parking_duration, pickup_from, later_than, size, unserved_penalty_factor, and so on).
  4. Date windows belong on the Job, as job.multi_shift.serve_within. Top-level arrive_after / leave_by are still allowed; they become the same clock times on every Planning Day and cannot cross midnight.
  5. Start and end locations stay on the Driver, not on each Shift.

A Driver with no work in this horizon still needs a multi_shift object. Give them "shifts": [] rather than mixing schedule shapes.

Planning times

Every Shift bound, and every serve_within bound, is a planning time:

{ "day": 1, "time": 8 } // (1)
  1. Planning Day 1 is the first day of this request. time also accepts "08:00".

time also accepts "08:00" or "08:00:00". day is a one-based Planning Day in this request (1 is the first day). The maximum is 100.

A Shift may cross midnight, for example day 1 at 22:00 to day 2 at 06:00. Two Shifts on the same Driver must not overlap. Back-to-back Shifts are allowed.

Reset mode

"mode": "reset" (the default if you omit mode) treats each Shift as a fresh trip.

  • Every Shift starts at driver.start.
  • Every Shift respects driver.end (return to start, finish anywhere, or a fixed location).
  • The vehicle is empty at the start of each Shift.
  • A Pickup Delivery pair must be served in the same Shift. The Driver cannot pick up on Monday and deliver on Tuesday.
  • Overnight travel between the depot and home is outside the optimisation.

Use reset for last-mile work that returns to a depot each day.

Example — two last-mile days

{
  "drivers": [
    {
      "uid": "driver_1",
      "start": {"lat": -33.867798, "lon": 151.166256},
      "end": "start",
      "multi_shift": {
        "mode": "reset",
        "shifts": [
          {
            "shift_start": {"day": 1, "time": 8},
            "shift_end": {"day": 1, "time": 17}
          },
          {
            "shift_start": {"day": 2, "time": 8},
            "shift_end": {"day": 2, "time": 17}
          }
        ]
      }
    }
  ],
  "jobs": [
    {
      "uid": "monday_delivery",
      "duration": 10,
      "location": {"lat": -33.849489, "lon": 151.127482},
      "multi_shift": {
        "serve_within": [
          {
            "arrive_after": {"day": 1, "time": 8},
            "leave_by": {"day": 1, "time": 17}
          }
        ]
      }
    },
    {
      "uid": "either_day",
      "duration": 10,
      "location": {"lat": -33.880661, "lon": 151.183096}
    }
  ],
  "settings": {}
}

monday_delivery can only be served on day 1. either_day can be served on day 1 or day 2 — whichever produces the better route.

Continue mode

"mode": "continue" treats all of that Driver's Shifts as one journey with rest in between.

  • Only the first Shift starts at driver.start.
  • Between Shifts the Driver stays where they are: at the last finished Job, at the next Job if they already arrived, or partway along a drive.
  • They cannot serve or make driving progress while off shift. An activity may finish exactly at shift_end.
  • driver.end applies to the final return after the last Job, which may happen in a later Shift that contains no Jobs.
  • Capacity and onboard pickups carry across Shifts. A Pickup Delivery pair must stay on the same Driver, but the delivery may be on a later Shift than the pickup.

Use continue for long-distance work with overnight rest.

Example — a two-day linehaul

The Driver leaves the depot on day 1, rests overnight wherever the route stops, and continues on day 2. The parcel stays on the vehicle.

{
  "drivers": [
    {
      "uid": "linehaul_1",
      "start": {"lat": -33.867798, "lon": 151.166256},
      "end": {"lat": -27.469771, "lon": 153.025124},
      "capacity": 20,
      "multi_shift": {
        "mode": "continue",
        "shifts": [
          {
            "shift_start": {"day": 1, "time": 8},
            "shift_end": {"day": 1, "time": 18}
          },
          {
            "shift_start": {"day": 2, "time": 8},
            "shift_end": {"day": 2, "time": 18}
          }
        ]
      }
    }
  ],
  "jobs": [
    {
      "uid": "pickup_depot",
      "duration": 20,
      "parking_duration": 10,
      "location": {"lat": -33.867798, "lon": 151.166256},
      "size": 8
    },
    {
      "uid": "drop_destination",
      "duration": 20,
      "parking_duration": 10,
      "location": {"lat": -27.469771, "lon": 153.025124},
      "size": -8,
      "pickup_from": "pickup_depot"
    }
  ],
  "settings": {}
}

If this Driver had used reset instead, the pickup and drop would have to finish on the same day, and the vehicle would return to the Sydney start at the end of day 1.

Job date windows (serve_within)

job.multi_shift.serve_within is a list of absolute periods. Each period has arrive_after, leave_by, or both, as planning times.

The Job may be served in any one of those periods (OR). Inside one period both bounds apply (AND). Combined with top-level arrive_after / leave_by:

recurring service hours
AND
(serve_within[0] OR serve_within[1] OR ...)

Omit multi_shift on the Job, or give "serve_within": [], to allow any time in the planning horizon (subject to Shifts and every other constraint).

Each period must set at least one bound. An empty {} is rejected.

serve_within is only valid when every Driver uses multi_shift.

Example — open Tuesday or Wednesday afternoon

{
  "uid": "appointment",
  "duration": 30,
  "arrive_after": 13,
  "leave_by": 17,
  "location": {"lat": -33.849489, "lon": 151.127482},
  "multi_shift": {
    "serve_within": [
      {
        "arrive_after": {"day": 2, "time": "00:00"},
        "leave_by": {"day": 2, "time": "23:59"}
      },
      {
        "arrive_after": {"day": 3, "time": "00:00"},
        "leave_by": {"day": 3, "time": "23:59"}
      }
    ]
  }
}

The recurring hours 1317 apply on both days. The serve_within list restricts the Job to day 2 or day 3.

How other constraints behave

Constraint reset continue
Pickup Delivery Same Driver and same Shift. The group is all-or-none. Same Driver; the pair may span Shifts. Still all-or-none.
Later Than Time order only. The two Jobs may be different Shifts or different Drivers. Same: time order only.
Capacity Load resets at every Shift. Load stays on the vehicle between Shifts.
Parking Duration Unchanged. Parking is waived when driving time into the stop is zero. Unchanged. A first Job at the overnight stop does not re-charge parking.
Route Segment Start / end apply inside each Shift. Start / end apply across the whole journey.
Unserved penalty factor Applies across the horizon. Does not pin a Job to a day. Same.
costs.run Charged once per active Shift. Charged once for the whole journey.

Reading the response

The runs array is still flat. Each Shift produces one Run, even if that Shift has no Jobs. Runs are ordered by Driver, then by Shift.

On a multi-shift response:

  • run.driver.multi_shift.mode is "reset" or "continue". Driver shift_start / shift_end are omitted.
  • run.multi_shift has the 1-based shift number, planned_start / planned_end as planning times, and the actual start and end locations for that Shift. In continue those locations are often not the configured depot.
  • Each Job has job.multi_shift.eta and job.multi_shift.etd as planning times. Top-level eta / etd are omitted. Unserved Jobs still have a multi_shift object, with null times.
{
  "runs": [
    {
      "driver": {
        "uid": "driver_1",
        "location": {"lat": -33.867798, "lon": 151.166256},
        "multi_shift": {"mode": "reset"}
      },
      "jobs": [
        {
          "uid": "monday_delivery",
          "duration": 10,
          "location": {"lat": -33.849489, "lon": 151.127482},
          "run": 1,
          "seq": 1,
          "multi_shift": {
            "eta": {"day": 1, "time": "08:25:00"},
            "etd": {"day": 1, "time": "08:35:00"}
          }
        }
      ],
      "multi_shift": {
        "shift": 1,
        "planned_start": {"day": 1, "time": "08:00:00"},
        "planned_end": {"day": 1, "time": "08:50:00"},
        "planned_start_location": {"lat": -33.867798, "lon": 151.166256},
        "planned_end_location": {"lat": -33.867798, "lon": 151.166256}
      }
    }
  ],
  "unserved_jobs": []
}

Prefer earlier Shifts

Travel time (and costs.run) do not care which day a Job lands on, so a reset request can leave Monday thin and park work on Thursday if that tour is shorter.

Set settings.later_shift_usage_penalty to penalise used time on later declared Shifts. It applies to ordinary single-Shift requests and to multi-shift (mode = reset and mode = continue). It is not a hard constraint.

Prefer fewer vehicles

Set settings.active_vehicle_penalty to penalise each compiled vehicle that serves at least one Job. Ordinary and continue: once per Driver. Reset: once per Shift. Continue does not pay per calendar day.

In cost mode this stacks with costs.run; they are different units. It also stacks with later_shift_usage_penalty (on/off versus used time on later Shifts). It is not a hard fleet cap.