Route optimization
By default, Timefold minimizes the amount of time technicians spend driving between visits. This lowers costs, reduces emissions, and makes technicians more productive as they spend less time driving and more time at visits.
Timefold can be configured to minimize travel distance.
Technicians can have different requirements or preferences regarding the maximum amount of travel time during their shifts.
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.
Maximum travel time can be set for a shift and per visit.
When maximum travel time is set for a shift, this limits the total time spent traveling in a shift. When maximum travel time is set per visit, this limits how much time technicians can spend driving to individual visits.
This guide explains how to limit a technician’s travel time with the following examples:
Prerequisites
Learn how to configure an API Key to run the examples in this guide:
-
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. Minimize travel time
The Minimize travel time
soft constraint adds a soft score penality to the run score for every second of travel in a route plan.
Timefold is incentivized to use the route plan with the lowest score.
Minimizing travel time is an optimization goal and is not typically used with the Minimize travel distance
constraint.
By default, this constraint has a weight of 1
.
All constraints with a weight of 1
are equally important.
Use the constraint configuration’s minimizeTravelTimeWeight
attribute to update the weight of the constraint and make it more or less important comparatively to other constraints.
{
"config":
{
"model":
{
"overrides":
{
"maxSoftShiftTravelTimeWeight": "10"
}
}
}
}
0
disables the constraint.
A constraint weight of 10
, means the constraint is 10 times more important than a constraint with a weight of 1.
2. Minimize travel distance
The Minimize travel distance
soft constraint adds a soft score penalty to the run score for every meter of travel in a route plan.
Timefold is incentivized to use the route plan with the lowest score.
Minimizing travel distance is an optimization goal and is not typically used with the Minimize travel time
constraint.
By default, this constraint has a weight of 0
, meaning it is disabled.
Use the constraint configuration’s minimizeTravelDistanceWeight
attribute to enable the constraint and make it more important comparatively to other constraints.
{
"config":
{
"model":
{
"overrides":
{
"maxSoftShiftTravelTimeWeight": "10"
}
}
}
}
0
disables constraints.
A constraint weight of 10
, means the constraint is 10 times more important than a constraint with a weight of 1.
3. Required maximum shift travel time
A vehicle shift’s maxTravelTime
property defines the maximum amount of allowed travel time (in ISO 8601 duration format) for the whole shift.
{
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxTravelTime": "PT1H"
}
]
}
Travel time includes:
-
Travel from the shift’s start location to the first visit.
-
Travel between visits.
-
Travel from the last visit back to the shift’s end location.
-
Travel to and from any fixed breaks with a provided location.
The Max shift travel (hard)
constraint is triggered if the total travel time of a vehicle shift exceeds the limit specified in maxTravelTime
.
The solution is penalized proportionally to the exceeding travel time in seconds.
Visits will be left unassigned, if assigning them breaks this constraint.
In the following example, there are 2 visits and 1 technician (Beth).
Beth’s shift has a maximum travel time of 1 hour ("maxTravelTime": "PT1H"
).
Timefold only assigns 1 visit to Beth because traveling to both visits would exceed the maximum required travel time for Beth’s shift.

-
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": "Required maximum shift travel time example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"vehicleType": "VAN",
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxTravelTime": "PT1H"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.72349, -84.56957],
"serviceDuration": "PT2H"
},
{
"id": "Visit B",
"location": [33.91554, -84.45041],
"serviceDuration": "PT2H"
}
]
}
}
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",
"parentId": null,
"originId": "ID",
"name": "Required maximum shift travel time example",
"submitDateTime": "2025-07-24T08:24:44.271812726Z",
"startDateTime": "2025-07-24T08:24:56.452742647Z",
"activeDateTime": "2025-07-24T08:24:57.074439654Z",
"completeDateTime": "2025-07-24T08:25:27.393239568Z",
"shutdownDateTime": "2025-07-24T08:25:27.689478216Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-10000medium/-3006soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-02-01",
"startTime": "2027-01-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-01-01T09:24:59Z",
"startServiceTime": "2027-01-01T09:24:59Z",
"departureTime": "2027-01-01T11:24:59Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT24M59S",
"travelDistanceMetersFromPreviousStandstill": 24329,
"minStartTravelTime": "2027-01-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT50M6S",
"travelTimeFromStartLocationToFirstVisit": "PT24M59S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT25M7S",
"totalTravelDistanceMeters": 48715,
"travelDistanceFromStartLocationToFirstVisitMeters": 24329,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 24386,
"endLocationArrivalTime": "2027-01-01T11:50:06Z"
}
}
]
}
]
},
"inputMetrics": {
"visits": 2,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 2,
"optionalVisits": 0,
"vehicleShifts": 1,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT50M6S",
"travelTimeFromStartLocationToFirstVisit": "PT24M59S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT25M7S",
"totalTravelDistanceMeters": 48715,
"travelDistanceFromStartLocationToFirstVisitMeters": 24329,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 24386,
"totalUnassignedVisits": 1,
"totalAssignedVisits": 1,
"assignedMandatoryVisits": 1,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0
}
}
modelOutput
contains the itinerary for the vehicle shifts.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output.
4. 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.
{
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxSoftTravelTime": "PT1H"
}
]
}
Travel time includes:
-
Travel from the shift’s start location to the first visit.
-
Travel between visits.
-
Travel from the last visit back to the shift’s end location.
-
Travel to and from any fixed breaks with a provided location.
The Max shift travel time (soft)
is triggered when the total travel time of a vehicle shift exceeds the limit specified in maxSoftTravelTime
.
Visits will still be assigned if they break this constraint, but the solution is penalized proportionally to the exceeding travel time in seconds, incentivizing Timefold to find the best solution.
If you use this soft limit in combination with a hard limit, make sure to set it to an earlier time than the corresponding hard limit (maxTraveltime
).
In the following example, there are 2 visits and 2 technicians (Beth and Carl). Beth’s and Carl’s shifts each have a preferred maximum travel time of 1 hour ("maxSoftTravelTime": "PT1H"
).
Beth and Carl are assigned 1 visit each.
Beth’s total travel time is 44 minutes. Carl’s total travel time is 1 hour and 33 minutes, which is above the preferred maximum travel time of 1 hour, however, assigning 1 visit each to Beth and Carl resulted in a lower soft score penalty than assigning both visits to a single technician would have.

-
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": "Preferred maximum shift travel time example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"vehicleType": "VAN",
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxSoftTravelTime": "PT1H"
}
]
},
{
"id": "Carl",
"vehicleType": "VAN",
"shifts": [
{
"id": "Carl-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxSoftTravelTime": "PT1H"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.71541, -84.51106],
"serviceDuration": "PT2H"
},
{
"id": "Visit B",
"location": [33.84246, -83.98872],
"serviceDuration": "PT2H"
}
],
"planningWindow": {
"startDate": "2027-01-01T00:00:00Z",
"endDate": "2027-01-02T00: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",
"parentId": null,
"originId": "ID",
"name": "Preferred maximum shift travel time example",
"submitDateTime": "2025-07-24T08:43:26.732481985Z",
"startDateTime": "2025-07-24T08:43:38.02015025Z",
"activeDateTime": "2025-07-24T08:43:38.754131801Z",
"completeDateTime": "2025-07-24T08:44:09.082771011Z",
"shutdownDateTime": "2025-07-24T08:44:09.540141679Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-10324soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-02-01",
"startTime": "2027-01-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-01-01T09:21:57Z",
"startServiceTime": "2027-01-01T09:21:57Z",
"departureTime": "2027-01-01T11:21:57Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT21M57S",
"travelDistanceMetersFromPreviousStandstill": 19813,
"minStartTravelTime": "2027-01-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT44M10S",
"travelTimeFromStartLocationToFirstVisit": "PT21M57S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT22M13S",
"totalTravelDistanceMeters": 39691,
"travelDistanceFromStartLocationToFirstVisitMeters": 19813,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 19878,
"endLocationArrivalTime": "2027-01-01T11:44:10Z"
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-02-01",
"startTime": "2027-01-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-01-01T09:46:46Z",
"startServiceTime": "2027-01-01T09:46:46Z",
"departureTime": "2027-01-01T11:46:46Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT46M46S",
"travelDistanceMetersFromPreviousStandstill": 43612,
"minStartTravelTime": "2027-01-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H33M21S",
"travelTimeFromStartLocationToFirstVisit": "PT46M46S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT46M35S",
"totalTravelDistanceMeters": 87332,
"travelDistanceFromStartLocationToFirstVisitMeters": 43612,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 43720,
"endLocationArrivalTime": "2027-01-01T12:33:21Z"
}
}
]
}
]
},
"inputMetrics": {
"visits": 2,
"visitGroups": 0,
"vehicles": 2,
"mandatoryVisits": 2,
"optionalVisits": 0,
"vehicleShifts": 2,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT2H17M31S",
"travelTimeFromStartLocationToFirstVisit": "PT1H8M43S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT1H8M48S",
"totalTravelDistanceMeters": 127023,
"travelDistanceFromStartLocationToFirstVisitMeters": 63425,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 63598,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 2,
"workingTimeFairnessPercentage": 86.97
}
}
modelOutput
contains the itinerary for the vehicle shifts.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output.
5. Maximum travel time per visit
A vehicle shift’s maxTravelTimePerVisit
property defines the maximum amount of allowed travel time (in ISO 8601 duration format) per visit.
{
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxTravelTimePerVisit": "PT45M"
}
]
}
Travel time per visit includes the total travel time from the previous location, including:
-
Visits
-
Breaks with a location
The Max travel time per visit (hard)
constraint is triggered when the travel time from the previous location exceeds the limit specified in maxTravelTimePerVisit
.
Visits in distant locations may be left unassigned if no technician is ever close enough to make the trip in less time than the limit specified in maxTravelTimePerVisit
.
In the following example, there are 2 visits and 1 technician (Beth) with 1 shift.
Beth’s shift has a maxTravelTimePerVisit
of 45 minutes.
Visit B is assigned to Beth because it takes less than 45 minutes to reach.
Visit A is not assigned because it would take longer than 45 minutes to reach.

-
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": "Maximum travel time per visit example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"vehicleType": "VAN",
"shifts": [
{
"id": "Beth-02-01",
"startLocation": [33.70450, -84.34881],
"endLocation": [33.70450, -84.34881],
"minStartTime": "2027-01-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-01-01T17:00:00Z",
"maxTravelTimePerVisit": "PT45M"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.69831, -85.01223],
"serviceDuration": "PT2H"
},
{
"id": "Visit B",
"location": [33.91554, -84.45041],
"serviceDuration": "PT2H"
}
]
}
}
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",
"parentId": null,
"originId": "ID",
"name": "Maximum travel time per visit example",
"submitDateTime": "2025-07-24T09:59:21.889699336Z",
"startDateTime": "2025-07-24T09:59:32.978573637Z",
"activeDateTime": "2025-07-24T09:59:33.494204576Z",
"completeDateTime": "2025-07-24T10:00:03.803690032Z",
"shutdownDateTime": "2025-07-24T10:00:04.104819785Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-10000medium/-3697soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-02-01",
"startTime": "2027-01-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-01-01T09:30:42Z",
"startServiceTime": "2027-01-01T09:30:42Z",
"departureTime": "2027-01-01T11:30:42Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT30M42S",
"travelDistanceMetersFromPreviousStandstill": 30631,
"minStartTravelTime": "2027-01-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H1M37S",
"travelTimeFromStartLocationToFirstVisit": "PT30M42S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT30M55S",
"totalTravelDistanceMeters": 61596,
"travelDistanceFromStartLocationToFirstVisitMeters": 30631,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 30965,
"endLocationArrivalTime": "2027-01-01T12:01:37Z"
}
}
]
}
]
},
"inputMetrics": {
"visits": 2,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 2,
"optionalVisits": 0,
"vehicleShifts": 1,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT1H1M37S",
"travelTimeFromStartLocationToFirstVisit": "PT30M42S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT30M55S",
"totalTravelDistanceMeters": 61596,
"travelDistanceFromStartLocationToFirstVisitMeters": 30631,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 30965,
"totalUnassignedVisits": 1,
"totalAssignedVisits": 1,
"assignedMandatoryVisits": 1,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0
}
}
modelOutput
contains the itinerary for the vehicle shifts.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output.
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.