Shift hours and overtime
Vehicle drivers work shifts for a maximum number of hours. They start and end at specific times, and work on jobs throughout the day.
Sometimes, overtime might be necessary to complete all the jobs or to limit repeat travel to faraway or difficult to reach stops.
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 job of the shift.
This guide explains how to manage shift times with the following examples:
1. Shift start and end
Learn how to configure an API Key to run the examples in this guide:
In the examples, replace |
A driver shift represents a time interval when the driver is available to be assigned stops. A shift is typically one working day. Drivers 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 eight hours (ignoring breaks).
Driver shifts are represented by shifts, and include startLocation, endLocation, minStartTime, and maxEndTime:
{
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
]
}
shifts are added for each shift the driver works:
{
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
{
"id": "Carl Tue",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-02T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
}
]
}
minStartTime is the earliest time a shift can start.
maxEndTime is the latest time a shift can end.
All travel and stops occur between the start and end times.
startLocation is the location the driver will start from to the first stop. This could be their home or the depot.
endLocation is the location the driver will drive to after the final stop.
This could be their home or the depot.
If no endLocation is provided, Timefold defaults to the startLocation.
1.1. Shift start and end example
In the following example, Carl works from 09:00 (minStartTime) until 17:00 (maxEndTime) on Monday.
Carl is assigned four stops.
Including travel time and the stop durations, Carl arrives home at 14:14, which is well before the maxEndTime of 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/pickup-delivery-routing/v1/route-plans -d@sample.json
{
"config": {
"run": {
"name": "Shift hours example"
}
},
"modelInput": {
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"jobs": [
{
"id": "Job A",
"stops": [
{
"id": "A1",
"location": [33.78592, -84.46136],
"duration": "PT20M"
},
{
"id": "A2",
"location": [33.72757, -83.96354],
"duration": "PT20M"
}
]
},
{
"id": "Job B",
"stops": [
{
"id": "B1",
"location": [34.11110, -84.43002],
"duration": "PT20M"
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M"
}
]
}
]
}
}
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/pickup-delivery-routing/v1/route-plans/<ID>
{
"metadata": {
"id": "506b0f41-1522-4720-9446-3f7bce172ab2",
"parentId": null,
"originId": "506b0f41-1522-4720-9446-3f7bce172ab2",
"name": "Shift hours example",
"submitDateTime": "2025-07-16T08:03:25.48538455Z",
"startDateTime": "2025-07-16T08:03:31.123471426Z",
"activeDateTime": "2025-07-16T08:03:35.633743705Z",
"completeDateTime": "2025-07-16T08:08:35.830395749Z",
"shutdownDateTime": "2025-07-16T08:08:36.293093206Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-14068soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "B1",
"arrivalTime": "2027-02-01T10:02:52Z",
"startServiceTime": "2027-02-01T10:02:52Z",
"departureTime": "2027-02-01T10:22:52Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT1H2M52S",
"travelDistanceMetersFromPreviousStandstill": 69415,
"kind": "STOP"
},
{
"id": "A1",
"arrivalTime": "2027-02-01T11:08:56Z",
"startServiceTime": "2027-02-01T11:08:56Z",
"departureTime": "2027-02-01T11:28:56Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT46M4S",
"travelDistanceMetersFromPreviousStandstill": 52509,
"kind": "STOP"
},
{
"id": "B2",
"arrivalTime": "2027-02-01T12:14:10Z",
"startServiceTime": "2027-02-01T12:14:10Z",
"departureTime": "2027-02-01T12:34:10Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT45M14S",
"travelDistanceMetersFromPreviousStandstill": 48856,
"kind": "STOP"
},
{
"id": "A2",
"arrivalTime": "2027-02-01T13:25:38Z",
"startServiceTime": "2027-02-01T13:25:38Z",
"departureTime": "2027-02-01T13:45:38Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT51M28S",
"travelDistanceMetersFromPreviousStandstill": 48636,
"kind": "STOP"
}
],
"metrics": {
"totalTravelTime": "PT3H54M28S",
"travelTimeFromStartLocationToFirstStop": "PT1H2M52S",
"travelTimeBetweenStops": "PT2H22M46S",
"travelTimeFromLastStopToEndLocation": "PT28M50S",
"totalTravelDistanceMeters": 250935,
"travelDistanceFromStartLocationToFirstStopMeters": 69415,
"travelDistanceBetweenStopsMeters": 150001,
"travelDistanceFromLastStopToEndLocationMeters": 31519,
"endLocationArrivalTime": "2027-02-01T14:14:28Z",
"overtime": "PT0S"
}
}
]
}
]
},
"inputMetrics": {
"stops": 4,
"drivers": 1,
"driverShifts": 1
},
"kpis": {
"totalTravelTime": "PT3H54M28S",
"travelTimeFromStartLocationToFirstStop": "PT1H2M52S",
"travelTimeBetweenStops": "PT2H22M46S",
"travelTimeFromLastStopToEndLocation": "PT28M50S",
"totalTravelDistanceMeters": 250935,
"travelDistanceFromStartLocationToFirstStopMeters": 69415,
"travelDistanceBetweenStopsMeters": 150001,
"travelDistanceFromLastStopToEndLocationMeters": 31519,
"totalUnassignedStops": 0,
"totalAssignedStops": 4,
"totalActivatedDrivers": 1,
"totalOvertime": "PT0S"
}
}
2. Overtime
What happens if Carl works too long and overtime is forbidden?
If Carl’s assigned stops force him to work until 18:00 but his shift has a maxEndTime of 17:00, the schedule is infeasible.
The model penalizes the amount of time that Carl finishes his shift too late as a hard constraint by one hour.
Timefold is automatically incentivized to assign the stop to other drivers or potentially to leave some stops unassigned.
However, if overtime is allowed, Carl can work longer, but it’s 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 Mon",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxSoftEndTime": "2027-02-01T17:00:00Z",
"maxEndTime": "2027-02-01T18:00:00Z"
}
]
}
The Preferred shift end time soft constraint is invoked when a driver shift’s end-of-route arrival time exceeds the shift’s maximum soft end time.
The constraint adds a soft penalty to the dataset score that is equivalent to the number of seconds between the maxSoftEndTime and the time the driver arrives at the end location for their shift, incentivizing Timefold to minimize overtime by completing shifts within preferred time windows..
Stops can still be scheduled even if doing so breaks this constraint, but Timefold is incentivized to use the route plan with the best score.
3. The first travel doesn’t count
In some cases, the employer doesn’t have to pay for the travel to the first stop. Instead, it comes out of the employee’s personal time, regardless if the employee lives near the first stop or on the other side of the region.
In this case, Carl needs to leave home in time to arrive at the first stop. If the stop is one hour away, they will need to leave home at 08:00 to arrive at 09:00.
minFirstStopArrivalTime sets the time Carl needs to arrive at his first stop.
However, the amount of time Carl has to travel before his first stop starts can be limited with minStartTime. For instance, by setting minStartTime to the earliest time Carl could be expected to leave for the first stop.
3.1. The first travel doesn’t count example
In the following example, minFirstStopArrivalTime is 09:00 and minStartTime is 07:00.
To disregard the travel time to the first stop, use minFirstStopArrivalTime instead of minStartTime.
If Carl needs to arrive at his first stop at 09:00, set minFirstStopArrivalTime to 09:00:
{
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T07:00:00Z",
"minFirstStopArrivalTime": "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 stop, use minStartTime and minFirstStopArrivalTime.
| Timefold is incentivized to schedule as many stops as possible. If no limits are placed on travel before the shift starts, drivers could be assigned to start their shifts at stops a long distance away from their start location, requiring them to travel for a long time before their shifts even start. |
If there’s a stop on an island with a long travel time by ferry,
Timefold assigns that travel during Carl’s personal time.
To arrive at the island stop at 09:00, Carl must depart at 06:00.
Adding a minStartTime of 07:00 limits the time Carl must leave for the first stop.
{
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T07:00:00Z",
"minFirstStopArrivalTime": "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 stop 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 stop.
For example, Carl must finish his last stop at 17:00, regardless of how much time it takes him to get home.
maxLastStopDepartureTime sets the latest time Carl can depart from his last stop.
However, the amount of time Carl has to travel after his last stop can be limited with maxEndTime. For instance, by setting maxEndTime to the latest time Carl could be expected to arrive home.
To disregard the travel time from the last stop, use maxLastStopDepartureTime instead of maxEndTime.
{
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxLastStopDepartureTime": "2027-02-01T17:00:00Z"
}
]
}
To limit the amount of travel time Carl could be asked to complete after his last stop, use maxEndTime and maxLastStopDepartureTime.
{
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxLastStopDepartureTime": "2027-02-01T17:00:00Z",
"maxEndTime": "2027-02-01T19:00:00Z"
}
]
}
Next
-
See the full API spec or try the online API.