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:
Prerequisites
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. 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.