Shift hours and overtime

Vehicle drivers work shifts for a maximum number of hours. They start and end at specific times, and make visits throughout the day.

Sometimes, overtime might be necessary to complete all the tasks or to limit repeat travel to faraway or difficult to reach visits.

Shifts typically start at the driver’s location, however, labor regulations might permit starting and ending shifts at other locations, including the first and last visit of the shift.

This guide explains how to manage shift times and overtime with the following examples:

Prerequisite

To run the examples in this guide, you need to authenticate with a valid API key:

  1. Log in to Timefold Platform: app.timefold.ai

  2. From the Dashboard, click your username, and from the drop-down menu select API Keys.

  3. Copy the default API key.

In the examples, replace <API_KEY> with the API Key you just copied.

The times displayed in the visualizations are approximates only.

1. Shift start and end

A vehicle shift represents a time interval when the vehicle (and the technician who drives the vehicle to scheduled visits) are assigned to visits. A shift is typically one working day. Technicians work for a maximum number of hours, starting at or after a specific time.

For example, a nine to five shift starts at 09:00 and ends at 17:00, for a total of 8 hours (ignoring a lunch break).

vehicle shift start and end basic

Vehicle shifts are represented by the vehicle’s shifts, and include startLocation, minStartTime, and maxEndTime:

"vehicles": [
  {
    "id": "Carl",
    "shifts": [
      {
        "id": "Carl-2027-02-01",
        "startLocation": [33.68786, -84.18487],
        "minStartTime": "2027-02-01T09:00:00Z"
        "maxEndTime": "2027-02-01T17:00:00Z"
      }
    ]
  }
],

minStartTime is the earliest time a shift can start.

maxEndTime is the latest time a shift can end.

All travel and visits occur between the start and end times.

startLocation is the location the technician will drive from to the first visit. This could be their home or the depot.

endLocation can also be provided if the endLocation differs from the startLocation. If no endLocation is provided, Timefold defaults to the startLocation.

In the following example, Carl works from 09:00 (minStartTime) until 17:00 (maxEndTime) on Monday.

Carl takes PTO (personal time off) on Tuesday.

On Wednesday morning, he takes PTO and starts late at 13:00 (minStartTime) and works until 17:00 (maxEndTime).

vehicle shift start and end

Timefold assigns Carl three visits on Monday. Including travel time and the service durations, Carl arrives home at 16:37, which is well before the maxEndTime of 17:00.

Carl doesn’t work on Tuesday, and Timefold doesn’t assign him any visits.

On Wednesday, Carl works between 13:00 and 17:00, and Timefold assigns him two visits, and he’s home at 17:00.

  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/field-service-routing/v1/route-plans [email protected]
{
  "config": {
    "run": {
      "name": "Shift hours example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startLocation": [33.68786, -84.18487],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z"
          },
          {
            "id": "Carl-2027-02-03",
            "startLocation": [33.68786, -84.18487],
            "minStartTime": "2027-02-03T13:00:00Z",
            "maxEndTime": "2027-02-03T17:00:00Z"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT2H"
      },
      {
        "id": "Visit B",
        "location": [33.74699, -84.02504],
        "serviceDuration": "PT2H"
      },
      {
        "id": "Visit C",
        "location": [33.88664, -84.28118],
        "serviceDuration": "PT2H"
      },
      {
        "id": "Visit D",
        "location": [33.71030, -84.05439],
        "serviceDuration": "PT2H"
      },
      {
        "id": "Visit E",
        "location": [33.87673, -84.26024],
        "serviceDuration": "PT1H"
      }
    ]
  }
}
To request the solution, locate the ID from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/field-service-routing/v1/route-plans/<ID>
{
    "run": {
        "id": ID,
        "name": "Shift hours example",
        "submitDateTime": "2024-08-05T05:47:12.948859969Z",
        "startDateTime": "2024-08-05T05:47:18.689215927Z",
        "completeDateTime": "2024-08-05T05:52:19.11751485Z",
        "solverStatus": "SOLVING_COMPLETED",
        "score": "0hard/0medium/-9436soft",
        "tags": null,
        "validationResult": {
            "summary": "OK"
        }
    },
    "modelOutput": {
        "vehicles": [
            {
                "id": "Carl",
                "shifts": [
                    {
                        "id": "Carl-2027-02-01",
                        "startTime": "2027-02-01T09:00:00Z",
                        "itinerary": [
                            {
                                "id": "Visit A",
                                "kind": "VISIT",
                                "arrivalTime": "2027-02-01T09:29:57Z",
                                "startServiceTime": "2027-02-01T09:29:57Z",
                                "departureTime": "2027-02-01T11:29:57Z",
                                "effectiveServiceDuration": "PT2H",
                                "travelTimeFromPreviousStandstill": "PT29M57S",
                                "travelDistanceMetersFromPreviousStandstill": 31492,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            },
                            {
                                "id": "Visit B",
                                "kind": "VISIT",
                                "arrivalTime": "2027-02-01T12:13:33Z",
                                "startServiceTime": "2027-02-01T12:13:33Z",
                                "departureTime": "2027-02-01T14:13:33Z",
                                "effectiveServiceDuration": "PT2H",
                                "travelTimeFromPreviousStandstill": "PT43M36S",
                                "travelDistanceMetersFromPreviousStandstill": 49957,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            },
                            {
                                "id": "Visit D",
                                "kind": "VISIT",
                                "arrivalTime": "2027-02-01T14:21:39Z",
                                "startServiceTime": "2027-02-01T14:21:39Z",
                                "departureTime": "2027-02-01T16:21:39Z",
                                "effectiveServiceDuration": "PT2H",
                                "travelTimeFromPreviousStandstill": "PT8M6S",
                                "travelDistanceMetersFromPreviousStandstill": 7287,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            }
                        ],
                        "metrics": {
                            "totalTravelTime": "PT1H37M57S",
                            "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
                            "travelTimeBetweenVisits": "PT51M42S",
                            "travelTimeFromLastVisitToEndLocation": "PT16M18S",
                            "totalTravelDistanceMeters": 104095,
                            "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
                            "travelDistanceBetweenVisitsMeters": 57244,
                            "travelDistanceFromLastVisitToEndLocationMeters": 15359,
                            "endLocationArrivalTime": "2027-02-01T16:37:57Z"
                        }
                    },
                    {
                        "id": "Carl-2027-02-03",
                        "startTime": "2027-02-03T13:00:00Z",
                        "itinerary": [
                            {
                                "id": "Visit C",
                                "kind": "VISIT",
                                "arrivalTime": "2027-02-03T13:27:39Z",
                                "startServiceTime": "2027-02-03T13:27:39Z",
                                "departureTime": "2027-02-03T15:27:39Z",
                                "effectiveServiceDuration": "PT2H",
                                "travelTimeFromPreviousStandstill": "PT27M39S",
                                "travelDistanceMetersFromPreviousStandstill": 30741,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            },
                            {
                                "id": "Visit E",
                                "kind": "VISIT",
                                "arrivalTime": "2027-02-03T15:32:05Z",
                                "startServiceTime": "2027-02-03T15:32:05Z",
                                "departureTime": "2027-02-03T16:32:05Z",
                                "effectiveServiceDuration": "PT1H",
                                "travelTimeFromPreviousStandstill": "PT4M26S",
                                "travelDistanceMetersFromPreviousStandstill": 2712,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            }
                        ],
                        "metrics": {
                            "totalTravelTime": "PT59M19S",
                            "travelTimeFromStartLocationToFirstVisit": "PT27M39S",
                            "travelTimeBetweenVisits": "PT4M26S",
                            "travelTimeFromLastVisitToEndLocation": "PT27M14S",
                            "totalTravelDistanceMeters": 61577,
                            "travelDistanceFromStartLocationToFirstVisitMeters": 30741,
                            "travelDistanceBetweenVisitsMeters": 2712,
                            "travelDistanceFromLastVisitToEndLocationMeters": 28124,
                            "endLocationArrivalTime": "2027-02-03T16:59:19Z"
                        }
                    }
                ]
            }
        ]
    },
    "kpis": {
        "totalTravelTime": "PT2H37M16S",
        "travelTimeFromStartLocationToFirstVisit": "PT57M36S",
        "travelTimeBetweenVisits": "PT56M8S",
        "travelTimeFromLastVisitToEndLocation": "PT43M32S",
        "totalTravelDistanceMeters": 165672,
        "travelDistanceFromStartLocationToFirstVisitMeters": 62233,
        "travelDistanceBetweenVisitsMeters": 59956,
        "travelDistanceFromLastVisitToEndLocationMeters": 43483,
        "totalUnassignedVisits": 0
    }
}

modelOutput contains the shift itineraries for Carl’s visits.

2. Overtime

What happens if Carl works too long and overtime is forbidden?

If Carl’s assigned visits force him to work until 18:00, but his shift has a maxEndTime of 17:00. This is an infeasible schedule. The model penalizes the amount of time that Carl finishes his shift too late as a hard constraint by one hour. Timefold is automatically incentivised to assign the visit to other technicians or potentially to leaving some visits unassigned.

Learn about the hard and soft constraints in the Field Service Routing model.
vehicle shift overtime

However, if overtime is allowed, Carl can work longer, but it is undesirable. To allow for overtime, use maxSoftEndTime for the normal end of his shift (17:00) and maxEndTime for the end of the potential overtime period. The potential overtime period is between maxSoftEndTime and maxEndTime.

Carl could work one hour of overtime by adding a maxSoftEndTime of 17:00 and a maxEndTime of 18:00.

"shifts": [
  {
    "id": "Carl-2027-02-01",
    "startLocation": [33.68786, -84.18487],
    "minStartTime": "2027-02-01T09:00:00Z",
    "maxSoftEndTime": "2027-02-01T17:00:00Z",
    "maxEndTime": "2027-02-01T18:00:00Z"
  }
]

Timefold treats the amount of overtime as a soft constraint, and tries to assign all visits to other available drivers without overtime. However, Timefold will assign as much allowed overtime as needed to avoid leaving visits unassigned.

Sometimes it’s better to assign overtime. Given two visits on an island only reachable by ferry:

vehicle shift overtime opportunity

If overtime is prohibited, Timefold has no choice than to assign the two visits to separate shifts. That reduces productivity dramatically, because the travel time by ferry has to happen twice.

By incurring one hour of overtime, a single technician can service both visits on the same day, and be available for other visits on the other day.

3. The first travel doesn’t count

In some cases, the employer doesn’t have to pay for the travel to the first visit. Instead, it comes out of the employee’s personal time, regardless if the employee lives near the first visit or on the other side of the region.

In this case, Carl needs to leave home in time to arrive at the first visit. If the visit is one hour away, he will need to leave home at 08:00 to arrive at 9:00.

minFirstVisitArrival sets the time Carl needs to arrive at his first visit.

However, the amount of time Carl has to travel before his first visit starts can be limited with minStartTime. For instance, by setting minStartTime to the earliest time Carl could be expected to leave for the first visit.

vehicle shift first visit arrival

To disregard the travel time to the first visit, use minFirstVisitArrivalTime instead of minStartTime. If Carl needs to arrive at his first visit at 9:00, set minFirstVisitArrivalTime to 9:00:

"shifts": [
  {
    "id": "Carl-2027-02-01",
    "startLocation": [33.68786, -84.18487],
    "minFirstVisitArrivalTime": "2027-02-01T09:00:00Z",
    "maxEndTime": "2027-02-01T17:00:00Z"
  }
]

To limit the amount of travel time Carl could be asked to complete to arrive at his first visit, use minStartTime and minFirstVisitArrivalTime.

Timefold is incentivised to maximize use of that free travel time, to fit more visits during the normal shift hours, which could lead to Carl being assigned a first visit that requires him to travel for a long time to arrive at the visit on time.

If there’s a visit on an island with a long travel time by ferry, Timefold assigns that travel during Carl’s personal time. But to arrive at the island visit at 9:00, Carl must depart at 6:00. Adding a minStartTime of 07:00 limits the time Carl must leave for the first visit.

"shifts": [
  {
    "id": "Carl-2027-02-01",
    "startLocation": [33.68786, -84.18487],
    "minStartTime": "2027-02-01T07:00:00Z",
    "minFirstVisitArrivalTime": "2027-02-01T09:00:00Z",
    "maxEndTime": "2027-02-01T17:00:00Z"
  }
]

Now, Carl’s travel time will not start before 7:00. Carl departs at 7:00 to arrive at the island visit at 10:00.

4. The last travel doesn’t count

Similarly, the employer doesn’t always have to pay for travel back home from the last visit.

For example, Carl must finish his last visit at 17:00, regardless of how much time it takes him to get home.

maxLastVisitDepartureTime sets the latest time Carl can depart from his last visit.

However, the amount of time Carl has to travel after his last visit can be limited with maxEndTime. For instance, by setting maxEndTime to the latest time Carl could be expected to arrive home.

vehicle shift last visit departure

To disregard the travel time from the last visit, use maxLastVisitDepartureTime instead of maxEndTime.

"shifts": [
  {
    "id": "Carl-2027-02-01",
    "startLocation": [33.68786, -84.18487],
    "minStartTime": "2027-02-01T09:00:00Z",
    "maxLastVisitDepartureTime": "2027-02-01T17:00:00Z"
  }
]

To limit the amount of travel time Carl could be asked to complete after his last visit, use maxEndTime and maxLastVisitDepartureTime.

"shifts": [
  {
    "id": "Carl-2027-02-01",
    "startLocation": [33.68786, -84.18487],
    "minStartTime": "2027-02-01T09:00:00Z",
    "maxLastVisitDepartureTime": "2027-02-01T17:00:00Z",
    "maxEndTime": "2027-02-01T19:00:00Z"
  }
]

Next