Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Vehicle resource constraints
  • Route optimization

Field Service Routing

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with field service routing
    • Understanding the API
    • User guide
      • Constraints
      • Time zones and daylight-saving time (DST)
      • Routing with Timefold’s maps service
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Technician costs
      • Technician ratings
      • Route optimization
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Visit requirements
      • Multi-vehicle visits
      • Priority visits and optional visits
      • Visit service level agreement (SLA)
    • Recommendations
      • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
    • Real-time planning
      • Real-time planning
      • Real-time planning: extended visit
      • Real-time planning: reassignment
      • Real-time planning: emergency visit
      • Real-time planning: no show
      • Real-time planning: technician ill
      • Real-time planning: pinning visits
    • Scenarios
      • Long-running visits
    • Changelog
    • Upgrading to the latest versions
    • Feature requests
    • Reference guide

Route optimization

Technicians can have different requirements or preferences regarding the maximum amount of travel time during their shift.

When assigning visits, Timefold makes sure that such requirements and preferences are respected.

It is important to realize that limiting the maximum travel time may compete with other optimization goals, such as assigning all visits or fairness.

This guide explains how to limit a technician’s travel time with the following examples:

  • Required maximum shift travel time

  • Preferred maximum shift travel time

Prerequisites

To run the examples in this guide, you need to authenticate with a valid API key for this model:

  1. Log in to Timefold Platform: app.timefold.ai.

  2. From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, then choose API Keys.

  3. 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. Required maximum shift travel time

A vehicle shift’s maxTravelTime property defines the maximum amount of travel time (in ISO 8601 duration format) for the whole shift. It includes the travel time:

  • from the shift’s start location to the first visit,

  • between visits,

  • from the last visit back to the shift’s end location,

  • to and from any fixed breaks with a provided location.

In the following example, Timefold assigns only one of the visits to Beth because traveling to both visits would exceed the maximum required travel time for Beth’s shift ("maxTravelTime": "PT1H"):

  • 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": "Technician maximum travel time requirement example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Beth",
        "vehicleType": "VAN",
        "shifts": [
          {
            "id": "Beth-06-19",
            "startLocation": [
              49.288087,
              16.562172
            ],
            "endLocation": [
              49.288087,
              16.562172
            ],
            "minStartTime": "2025-06-19T08:00:00Z",
            "maxLastVisitDepartureTime": "2025-06-19T18:00:00Z",
            "maxTravelTime": "PT1H"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "1",
        "name": "Paul",
        "location": [
          49.190922,
          16.624466
        ],
        "timeWindows": [
          {
            "minStartTime": "2025-06-19T09:00:00Z",
            "maxEndTime": "2025-06-19T12:00:00Z"
          }
        ],
        "serviceDuration": "PT1H"
      },
      {
        "id": "2",
        "name": "Claire",
        "location": [
          50.0109,
          16.724466
        ],
        "timeWindows": [
          {
            "minStartTime": "2025-06-19T12:00:00Z",
            "maxEndTime": "2025-06-19T17:00:00Z"
          }
        ],
        "serviceDuration": "PT1H"
      }
    ],
    "planningWindow": {
      "startDate": "2025-06-19T00:00:00Z",
      "endDate": "2025-06-20T00: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",
    "originId": "OriginID",
    "name": "Technician maximum travel time requirement example",
    "submitDateTime": "2025-06-18T20:09:55.402458364+02:00",
    "startDateTime": "2025-06-18T20:09:55.439731601+02:00",
    "activeDateTime": "2025-06-18T20:09:55.559290991+02:00",
    "completeDateTime": "2025-06-18T20:09:56.693134429+02:00",
    "shutdownDateTime": "2025-06-18T20:09:56.714463962+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/-10000medium/-25112soft",
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-06-19",
            "startTime": "2025-06-19T08:00:00Z",
            "itinerary": [
              {
                "id": "1",
                "kind": "VISIT",
                "arrivalTime": "2025-06-19T08:14:03Z",
                "startServiceTime": "2025-06-19T09:00:00Z",
                "departureTime": "2025-06-19T10:00:00Z",
                "effectiveServiceDuration": "PT1H",
                "travelTimeFromPreviousStandstill": "PT14M3S",
                "travelDistanceMetersFromPreviousStandstill": 11713,
                "minStartTravelTime": "2025-06-19T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT28M6S",
              "travelTimeFromStartLocationToFirstVisit": "PT14M3S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT14M3S",
              "totalTravelDistanceMeters": 23426,
              "travelDistanceFromStartLocationToFirstVisitMeters": 11713,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 11713,
              "endLocationArrivalTime": "2025-06-19T10:14:03Z"
            }
          }
        ]
      }
    ]
  },
  "inputMetrics": {
    "visits": 2,
    "visitGroups": 0,
    "vehicles": 1,
    "mandatoryVisits": 2,
    "optionalVisits": 0,
    "vehicleShifts": 1,
    "visitsWithSla": 0
  },
  "kpis": {
    "totalTravelTime": "PT28M6S",
    "travelTimeFromStartLocationToFirstVisit": "PT14M3S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT14M3S",
    "totalTravelDistanceMeters": 23426,
    "travelDistanceFromStartLocationToFirstVisitMeters": 11713,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 11713,
    "totalUnassignedVisits": 1,
    "totalAssignedVisits": 1,
    "assignedMandatoryVisits": 1,
    "assignedOptionalVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 100.0,
    "averageTechnicianRating": 0.0,
    "absoluteVisitsInSla": 0
  }
}

modelOutput contains the single visit assigned to Beth’s shift itinerary, the other visit is left unassigned because Beth cannot travel to both visits within her travel time limit.

2. Preferred maximum shift travel time

A vehicle shift’s maxSoftTravelTime property defines the preferred maximum amount of travel time (in ISO 8601 duration format) for the whole shift. Again, it includes the travel time:

  • from the shift’s start location to the first visit,

  • between visits,

  • from the last visit back to the shift’s end location,

  • to and from any fixed breaks with a provided location.

In the following example, Timefold assigns the visit with ID "2" to Carl, even though the visit has declared Beth as the preferred technician. Traveling to both visits would exceed the maximum preferred travel time for Beth’s shift ("maxSoftTravelTime": "PT1H") and produce higher soft penalty than not assigning the visit to the preferred technician:

If you need to favor the preferred technician constraint, it can be made more important than the travel time preference by increasing its Preferred vehicles constraint weight.
  • 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": "Technician maximum travel time preference example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Beth",
        "vehicleType": "VAN",
        "shifts": [
          {
            "id": "Beth-06-19",
            "startLocation": [
              49.288087,
              16.562172
            ],
            "endLocation": [
              49.288087,
              16.562172
            ],
            "minStartTime": "2025-06-19T08:00:00Z",
            "maxLastVisitDepartureTime": "2025-06-19T18:00:00Z",
            "maxSoftTravelTime": "PT1H"
          }
        ]
      },
      {
        "id": "Carl",
        "vehicleType": "VAN",
        "shifts": [
          {
            "id": "Carl-06-19",
            "startLocation": [
              49.288087,
              16.562172
            ],
            "endLocation": [
              49.288087,
              16.562172
            ],
            "minStartTime": "2025-06-19T08:00:00Z",
            "maxLastVisitDepartureTime": "2025-06-19T18:00:00Z"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "1",
        "name": "Paul",
        "location": [
          49.190922,
          16.624466
        ],
        "timeWindows": [
          {
            "minStartTime": "2025-06-19T09:00:00Z",
            "maxEndTime": "2025-06-19T12:00:00Z"
          }
        ],
        "serviceDuration": "PT1H"
      },
      {
        "id": "2",
        "name": "Claire",
        "location": [
          50.0109,
          16.724466
        ],
        "timeWindows": [
          {
            "minStartTime": "2025-06-19T12:00:00Z",
            "maxEndTime": "2025-06-19T17:00:00Z"
          }
        ],
        "serviceDuration": "PT1H",
        "preferredVehicles": [
          "Beth"
        ]
      }
    ],
    "planningWindow": {
      "startDate": "2025-06-19T00:00:00Z",
      "endDate": "2025-06-20T00: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",
    "originId": "OriginID",
    "name": "Technician maximum travel time preference example",
    "submitDateTime": "2025-06-18T20:15:56.117820166+02:00",
    "startDateTime": "2025-06-18T20:15:56.15639788+02:00",
    "activeDateTime": "2025-06-18T20:15:56.272914714+02:00",
    "completeDateTime": "2025-06-18T20:15:56.40699513+02:00",
    "shutdownDateTime": "2025-06-18T20:15:56.421501084+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-203154soft",
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-06-19",
            "startTime": "2025-06-19T08:00:00Z",
            "itinerary": [
              {
                "id": "1",
                "kind": "VISIT",
                "arrivalTime": "2025-06-19T08:14:03Z",
                "startServiceTime": "2025-06-19T09:00:00Z",
                "departureTime": "2025-06-19T10:00:00Z",
                "effectiveServiceDuration": "PT1H",
                "travelTimeFromPreviousStandstill": "PT14M3S",
                "travelDistanceMetersFromPreviousStandstill": 11713,
                "minStartTravelTime": "2025-06-19T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT28M6S",
              "travelTimeFromStartLocationToFirstVisit": "PT14M3S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT14M3S",
              "totalTravelDistanceMeters": 23426,
              "travelDistanceFromStartLocationToFirstVisitMeters": 11713,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 11713,
              "endLocationArrivalTime": "2025-06-19T10:14:03Z"
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-06-19",
            "startTime": "2025-06-19T08:00:00Z",
            "itinerary": [
              {
                "id": "2",
                "kind": "VISIT",
                "arrivalTime": "2025-06-19T09:37:28Z",
                "startServiceTime": "2025-06-19T12:00:00Z",
                "departureTime": "2025-06-19T13:00:00Z",
                "effectiveServiceDuration": "PT1H",
                "travelTimeFromPreviousStandstill": "PT1H37M28S",
                "travelDistanceMetersFromPreviousStandstill": 81218,
                "minStartTravelTime": "2025-06-19T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT3H14M56S",
              "travelTimeFromStartLocationToFirstVisit": "PT1H37M28S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT1H37M28S",
              "totalTravelDistanceMeters": 162436,
              "travelDistanceFromStartLocationToFirstVisitMeters": 81218,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 81218,
              "endLocationArrivalTime": "2025-06-19T14:37:28Z"
            }
          }
        ]
      }
    ]
  },
  "inputMetrics": {
    "visits": 2,
    "visitGroups": 0,
    "vehicles": 2,
    "mandatoryVisits": 2,
    "optionalVisits": 0,
    "vehicleShifts": 2,
    "visitsWithSla": 0
  },
  "kpis": {
    "totalTravelTime": "PT3H43M2S",
    "travelTimeFromStartLocationToFirstVisit": "PT1H51M31S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT1H51M31S",
    "totalTravelDistanceMeters": 185862,
    "travelDistanceFromStartLocationToFirstVisitMeters": 92931,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 92931,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 2,
    "assignedMandatoryVisits": 2,
    "assignedOptionalVisits": 0,
    "totalActivatedVehicles": 2,
    "workingTimeFairnessPercentage": 50.44,
    "averageTechnicianRating": 0.0,
    "absoluteVisitsInSla": 0
  }
}

modelOutput contains the visit with ID "2" assigned to Carl’s shift itinerary because Beth cannot travel to both visits within her travel time limit.

Next

  • See the full API spec or try the online API.

  • Learn more about field service routing from our YouTube playlist.

  • Read Skills to learn how to avoid scheduling overqualified technicians for a job.

  • © 2025 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default