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:

  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.

1. Time windows

For a customer visit that must occur between 13:00 and 17:00, a timeWindows object is specified with minStartTime and maxEndTime values:

Visits can have multiple time windows.
"visits": [
  {
    "id": "Visit A",
    "location": [33.77301, -84.43838],
    "serviceDuration": "PT1H30M",
    "timeWindows": [
        {
            "minStartTime": "2027-02-01T13:00:00Z",
            "maxEndTime": "2027-02-01T17: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": [33.77301, -84.43838],
        "serviceDuration": "PT1H30M",
        "timeWindows": [
            {
                "minStartTime": "2027-02-01T13:00:00Z",
                "maxEndTime": "2027-02-01T17: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-07-15T04:39:34.792750944Z",
        "startDateTime": "2024-07-15T04:39:40.233674574Z",
        "completeDateTime": "2024-07-15T04:39:42.884169539Z",
        "solverStatus": "SOLVING_COMPLETED",
        "score": "0hard/0medium/-3569soft",
        "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-01T13:00:00Z",
                                "departureTime": "2027-02-01T14:30:00Z",
                                "effectiveServiceDuration": "PT1H30M",
                                "travelTimeFromPreviousStandstill": "PT29M57S",
                                "travelDistanceMetersFromPreviousStandstill": 31492,
                                "minStartTravelTime": "2027-02-01T00:00:00Z"
                            }
                        ],
                        "metrics": {
                            "totalTravelTime": "PT59M29S",
                            "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
                            "travelTimeBetweenVisits": "PT0S",
                            "travelTimeFromLastVisitToEndLocation": "PT29M32S",
                            "totalTravelDistanceMeters": 65476,
                            "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
                            "travelDistanceBetweenVisitsMeters": 0,
                            "travelDistanceFromLastVisitToEndLocationMeters": 33984,
                            "endLocationArrivalTime": "2027-02-01T14:59:32Z"
                        }
                    }
                ]
            }
        ]
    },
    "kpis": {
        "totalTravelTime": "PT59M29S",
        "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
        "travelTimeBetweenVisits": "PT0S",
        "travelTimeFromLastVisitToEndLocation": "PT29M32S",
        "totalTravelDistanceMeters": 65476,
        "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
        "travelDistanceBetweenVisitsMeters": 0,
        "travelDistanceFromLastVisitToEndLocationMeters": 33984,
        "totalUnassignedVisits": 0
    }
}

modelOutput contains the itinerary for Carl’s shift and Visit A.

In this example, Carl’s shift begins at 9:00, but the time window for the visit isn’t until 13:00. Carl’s travel to the visit is scheduled so that he arrives at 13:00. 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.

visit time windows basic

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",
        "completeDateTime": "2024-07-15T06:33:11.696296779Z",
        "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.

visit time windows

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":

"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",
        "completeDateTime": "2024-08-21T05:16:11.947930487Z",
        "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.

visit time windows too early

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.
visit time windows the wait time paradox

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.

visit time windows too late

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 8: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:

visit multiple time windows

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