Time windows and opening hours
When a customer needs a technician to visit their home, the visit must be scheduled for a time when the customer will be home to let the technician in. For instance, the customer might only be available in the morning or the afternoon. Some visits have a time period in which the visit must occur, this is called a time window (and opening hours). Some visits must start at a specific time.
Often the time window is decided when the customer books the visit.
This guide explains time windows with the following examples:
Prerequisite
To run the examples in this guide, you need to authenticate with a valid API key for this model:
-
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 current model.
In the examples, replace <API_KEY>
with the API Key you just copied.
1. Time windows
For a customer visit that must occur between 10:00 and 13:00, a timeWindows
object is specified with minStartTime
and maxEndTime
values:
Visits can have multiple time windows. |
{
"visits": [
{
"id": "Visit A",
"location": [34.31785, -83.82816],
"serviceDuration": "PT1H30M",
"timeWindows": [
{
"minStartTime": "2027-02-01T10:00:00Z",
"maxEndTime": "2027-02-01T13:00:00Z"
}
]
}
]
}
minStartTime
and maxEndTime
use the ISO 8601 date and time format with offset to UTC format.
Below is an example dataset with one vehicle and one visit with a time window:
-
Input
-
Output
Try this example in Timefold Platform by saving the JSON into a file called time-windows-example-1.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": "Time window 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"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [34.31785, -83.82816],
"serviceDuration": "PT1H30M",
"timeWindows": [
{
"minStartTime": "2027-02-01T10:00:00Z",
"maxEndTime": "2027-02-01T13:00:00Z"
}
]
}
]
}
}
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": "Time window example",
"submitDateTime": "2024-09-16T04:31:15.391186569Z",
"startDateTime": "2024-09-16T04:31:21.096410685Z",
"activeDateTime": "2024-09-16T04:31:21.196410685Z",
"completeDateTime": "2024-09-16T04:31:24.144757106Z",
"shutdownDateTime": "2024-09-16T04:31:24.244757106Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-8695soft",
"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-01T10:11:58Z",
"startServiceTime": "2027-02-01T10:11:58Z",
"departureTime": "2027-02-01T11:41:58Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT1H11M58S",
"travelDistanceMetersFromPreviousStandstill": 94719,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT2H24M55S",
"travelTimeFromStartLocationToFirstVisit": "PT1H11M58S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT1H12M57S",
"totalTravelDistanceMeters": 187829,
"travelDistanceFromStartLocationToFirstVisitMeters": 94719,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 93110,
"endLocationArrivalTime": "2027-02-01T12:54:55Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT2H24M55S",
"travelTimeFromStartLocationToFirstVisit": "PT1H11M58S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT1H12M57S",
"totalTravelDistanceMeters": 187829,
"travelDistanceFromStartLocationToFirstVisitMeters": 94719,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 93110,
"totalUnassignedVisits": 0
}
}
modelOutput
contains the itinerary for Carl’s shift and Visit A.
In this example, Carl’s shift begins at 09:00. His travel time is scheduled as soon as he starts his shift and he arrives at the visit at 10:11, which means he can complete the visit in the time window.
The visit duration lasts for 1 hour and 30 minutes "PT1H30M"
.
This is the only visit on Carl’s itinerary today, so when Visit A is completed, he is scheduled to travel back to his startLocation
.
2. Time windows and other visits
Carl had a quiet shift in our first example, but he usually has more than one visit to complete per shift.
When there are multiple visits in a shift, Timefold assigns visits in an order that conforms to the specified time windows.
Below is an example dataset with one vehicle, two visits without time windows, and one visit with a time window.
-
Input
-
Output
Try this example in Timefold Platform by saving the JSON into a file called time-windows-example-2.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": "Time windows and other visits 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"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT1H30M",
"timeWindows": [
{
"minStartTime": "2027-02-01T13:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.87673, -84.26024],
"serviceDuration": "PT1H"
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT1H30M"
}
]
}
}
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": "Time windows and other visits example",
"submitDateTime": "2024-07-15T06:29:07.287034163Z",
"startDateTime": "2024-07-15T06:29:13.075791707Z",
"activeDateTime": "2024-07-15T06:29:13.175791707Z",
"completeDateTime": "2024-07-15T06:33:11.696296779Z",
"shutdownDateTime": "2024-07-15T06:33:11.796296779Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-5017soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:26:11Z",
"startServiceTime": "2027-02-01T09:26:11Z",
"departureTime": "2027-02-01T10:26:11Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT26M11S",
"travelDistanceMetersFromPreviousStandstill": 29015,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T10:30:36Z",
"startServiceTime": "2027-02-01T10:30:36Z",
"departureTime": "2027-02-01T12:00:36Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT4M25S",
"travelDistanceMetersFromPreviousStandstill": 2702,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T12:24:05Z",
"startServiceTime": "2027-02-01T13:00:00Z",
"departureTime": "2027-02-01T14:30:00Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT23M29S",
"travelDistanceMetersFromPreviousStandstill": 24210,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H23M37S",
"travelTimeFromStartLocationToFirstVisit": "PT26M11S",
"travelTimeBetweenVisits": "PT27M54S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89911,
"travelDistanceFromStartLocationToFirstVisitMeters": 29015,
"travelDistanceBetweenVisitsMeters": 26912,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T14:59:32Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT1H23M37S",
"travelTimeFromStartLocationToFirstVisit": "PT26M11S",
"travelTimeBetweenVisits": "PT27M54S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89911,
"travelDistanceFromStartLocationToFirstVisitMeters": 29015,
"travelDistanceBetweenVisitsMeters": 26912,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 0
}
}
modelOutput
contains the itinerary for Carl’s shifts with Visit A scheduled within the correct time window.
In the output, you can see Timefold assigns visit A as Carl’s 3rd visit that shift.
In the example below, Carl arrives at 14:00 to start work.
14:00 is after the minStartTime
of 13:00, and the visit will be completed before the maxEndTime
which is 17:00.
3. Fixed visits
Occasionally, visits have to happen at a specific time (fixed visits), and other visits must be scheduled around them.
This can be achieved by adding a maxStartTime
that is equal to the minStartTime
, and a maxEndTime
that occurs after the visit should end.
Given a serviceDuration
of "PT1H30M"
and minStartTime
of "13:00"
:
{
"id": "Visit A",
"timeWindows": [
{
"minStartTime": "2027-02-01T13:00:00Z",
"maxStartTime": "2027-02-01T13:00:00Z",
"maxEndTime": "2027-02-01T15:00:00Z"
}
]
}
In the following example, Visit A is a fixed visit that must start at 13: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": "Fixed visit 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"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [
33.77301,
-84.43838
],
"serviceDuration": "PT1H30M",
"timeWindows": [
{
"minStartTime": "2027-02-01T13:00:00Z",
"maxStartTime": "2027-02-01T13:00:00Z",
"maxEndTime": "2027-02-01T14:30:00Z"
}
]
},
{
"id": "Visit B",
"location": [
33.87673,
-84.26024
],
"serviceDuration": "PT1H"
},
{
"id": "Visit C",
"location": [
33.88664,
-84.28118
],
"serviceDuration": "PT1H30M"
}
]
}
}
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": "Fixed visit example",
"submitDateTime": "2024-08-21T05:11:03.373086923Z",
"startDateTime": "2024-08-21T05:11:11.526430596Z",
"activeDateTime": "2024-08-21T05:11:11.626430596Z",
"completeDateTime": "2024-08-21T05:16:11.947930487Z",
"shutdownDateTime": "2024-08-21T05:16:12.047930487Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-5017soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:26:11Z",
"startServiceTime": "2027-02-01T09:26:11Z",
"departureTime": "2027-02-01T10:26:11Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT26M11S",
"travelDistanceMetersFromPreviousStandstill": 29015,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T10:30:36Z",
"startServiceTime": "2027-02-01T10:30:36Z",
"departureTime": "2027-02-01T12:00:36Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT4M25S",
"travelDistanceMetersFromPreviousStandstill": 2702,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T12:24:05Z",
"startServiceTime": "2027-02-01T13:00:00Z",
"departureTime": "2027-02-01T14:30:00Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT23M29S",
"travelDistanceMetersFromPreviousStandstill": 24210,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H23M37S",
"travelTimeFromStartLocationToFirstVisit": "PT26M11S",
"travelTimeBetweenVisits": "PT27M54S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89911,
"travelDistanceFromStartLocationToFirstVisitMeters": 29015,
"travelDistanceBetweenVisitsMeters": 26912,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T14:59:32Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT1H23M37S",
"travelTimeFromStartLocationToFirstVisit": "PT26M11S",
"travelTimeBetweenVisits": "PT27M54S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89911,
"travelDistanceFromStartLocationToFirstVisitMeters": 29015,
"travelDistanceBetweenVisitsMeters": 26912,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 0
}
}
modelOutput
contains the solution with Visit A scheduled to start at 13:00 and finish at 14:30.
4. Too early (wait time)
With time windows it is sometimes necessary for a technician to arrive early and wait.
For example, if Carl arrives too early at 12:00, before the minStartTime of 13:00, he needs to wait for one hour to start work.
Despite the lost time spent waiting, this can be the most productive solution.
Time windows automatically incentivizes Timefold to fill up any waiting time with other visits. However, the Field Service Routing model attempts to minimize travel time and maximize productivity so wait times are kept to a minimum.
Learn about the hard and soft constraints in the Field Service Routing model. |
5. Too late
If Carl arrives too late, he can’t finish the job in time. In the example below, if he arrives at 12:30 for a job that ends at 13:00, he won’t have time to finish the job. This is an infeasible schedule.
Because the end time is earlier than Carl would finish, Timefold is incentivized to assign that visit earlier, or leave it unassigned.
6. Multiple time windows (opening hours)
A visit can have multiple time windows when it’s possible to schedule a visit.
For example, Visit A could occur on:
-
Monday 1-feb between 08:30 and 11:30
-
Monday 1-feb between 13:30 and 17:00
-
Tuesday 2-feb between 09:30 and 12:30
Timefold assigns that visit to a vehicle shift during one of those time windows, for instance, to Carl on his Monday shift:
Multiple time windows are assigned to a single visit as follows:
{
"visits": [
{
"id": "Visit A",
"timeWindows": [
{
"minStartTime": "2027-02-01T08:30:00Z",
"maxEndTime": "2027-02-01T11:30:00Z"
},
{
"minStartTime": "2027-02-01T13:30:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
{
"minStartTime": "2027-02-02T09:30:00Z",
"maxEndTime": "2027-02-02T12:30:00Z"
}
]
}
]
}
The examples assume the visit time windows use the UTC time zone offset. Please see Time zones and daylight-saving time (DST) chapter for more details.
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.
-
Learn about Shift hours and overtime.
-
Schedule Lunch breaks and personal appointments.