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:
-
Log in to Timefold Platform: app.timefold.ai
-
From the Dashboard, click your username, and from the drop-down menu select API Keys.
-
Copy the default API key.
In the examples, replace <API_KEY>
with the API Key you just copied.
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 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
).
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. |
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:
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.
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.
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
-
Understand the constraints of the Field Service Routing model.
-
See the full API spec or try the online API.
-
Manage shift times with Time zones and daylight-saving time (DST) changes.
-
Schedule Lunch breaks and personal appointments.
-
Use Time windows to specify visit availability and limit when visits can be scheduled.