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:
Prerequisites
Learn how to configure an API Key to run the examples in this guide:
-
Log in to Timefold Platform: app.timefold.ai
-
From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, then choose API Keys.
-
Create a new API key or use an existing one. Ensure the list of models for the API key contains the Pick-up and Delivery Routing model.
In the examples, replace <API_KEY> with the API Key you just copied.
1. Shift start and end
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 8 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.
In the following example, Carl works from 09:00 (minStartTime) until 17:00 (maxEndTime) on Monday.
Carl is assigned 4 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 [email protected]
{
"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",
"stopDependencies": [
{
"id": "jobA_dep1",
"precedingStop": "A1"
}
]
}
]
},
{
"id": "Job B",
"stops": [
{
"id": "B1",
"location": [34.11110, -84.43002],
"duration": "PT20M"
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M",
"stopDependencies": [
{
"id": "jobB_dep1",
"precedingStop": "B1"
}
]
}
]
}
]
}
}
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.
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 stop to other technicians or potentially to leaving some stops unassigned.
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 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 Max shift end time (soft) soft constraint is invoked when a solution schedules a driver to continue working after the maxSoftEndTime of their shift.
To optimize for assigning as little overtime as possible, the constraint adds a soft penalty to the dataset score for every second between maxSoftEndTime and the time they arrive at the end location for their shift.
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, he will need to leave home at 08:00 to arrive at 9: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.
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 9:00, set minFirstStopArrivalTime to 9: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 incentivised to schedule as many stops as possible. If no limits are placed on travel before the shift starts, it is possible 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. to fit more stops during the normal shift hours, which could lead to Carl being assigned a first stop that requires him to travel for a long time to arrive at the stop on time. |
If there’s a stop 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 stop at 9:00, Carl must depart at 6: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.