Resource-limited planning and optional visits
When there is more work to be done than people to complete the work, the type of planning involved is known as resource-limited planning.
When you are resource-limited planning in field service routing, you can introduce mandatory visits and optional visits to help prioritize which visits should be assigned as soon as possible and which visits can wait until later.
Mandatory visits are visits with a time window that falls within the current planning window.
Optional visits are visits with a time window that ends outside the current planning window.
You can also give individual visits priority levels to further control the order visits should be assigned.
This guide explains how to manage resource-limited planning and optional visits 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. Assign mandatory visits
In the following example, the planning window is for February 1st, 2027:
{
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-02T00:00:00Z"
}
}
There are 5 visits.
3 of the visits (Visit C, Visit D, and Visit E) have time windows that are in the planning window, making them mandatory visits. 2 of the visits (Visit A and Visit B) have time windows that end after the planning window, making them optional visits.
{
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
]
}
The Require scheduling mandatory visits
is a medium constraint that penalizes unassigned mandatory visits, incentivizing Timefold to schedule as many mandatory visits as possible.
There is only time for 3 of the visits to be scheduled during the planning window.
Because Visit C, Visit D, and Visit E have time windows that end on February 1st, 2027, during the planning window, they are scheduled. Visit A and Visit B, which have time windows that end after the planning window, are left unscheduled.

-
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": "Assign mandatory 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": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-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",
"name": "Assign mandatory visits example",
"submitDateTime": "2025-02-25T05:08:07.977085453Z",
"startDateTime": "2025-02-25T05:08:34.443085661Z",
"activeDateTime": "2025-02-25T05:08:34.775097095Z",
"completeDateTime": "2025-02-25T05:13:35.123701244Z",
"shutdownDateTime": "2025-02-25T05:13:35.313892244Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-1005088soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit D",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:16:24Z",
"startServiceTime": "2027-02-01T09:16:24Z",
"departureTime": "2027-02-01T11:16:24Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT16M24S",
"travelDistanceMetersFromPreviousStandstill": 15551,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:53:09Z",
"startServiceTime": "2027-02-01T11:53:09Z",
"departureTime": "2027-02-01T13:53:09Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT36M45S",
"travelDistanceMetersFromPreviousStandstill": 43211,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:57:34Z",
"startServiceTime": "2027-02-01T13:57:34Z",
"departureTime": "2027-02-01T15:57:34Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT4M25S",
"travelDistanceMetersFromPreviousStandstill": 2712,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H24M48S",
"travelTimeFromStartLocationToFirstVisit": "PT16M24S",
"travelTimeBetweenVisits": "PT41M10S",
"travelTimeFromLastVisitToEndLocation": "PT27M14S",
"totalTravelDistanceMeters": 89600,
"travelDistanceFromStartLocationToFirstVisitMeters": 15551,
"travelDistanceBetweenVisitsMeters": 45923,
"travelDistanceFromLastVisitToEndLocationMeters": 28126,
"endLocationArrivalTime": "2027-02-01T16:24:48Z",
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 3,
"optionalVisits": 2,
"vehicleShifts": 1
},
"kpis": {
"totalTravelTime": "PT1H24M48S",
"travelTimeFromStartLocationToFirstVisit": "PT16M24S",
"travelTimeBetweenVisits": "PT41M10S",
"travelTimeFromLastVisitToEndLocation": "PT27M14S",
"totalTravelDistanceMeters": 89600,
"travelDistanceFromStartLocationToFirstVisitMeters": 15551,
"travelDistanceBetweenVisitsMeters": 45923,
"travelDistanceFromLastVisitToEndLocationMeters": 28126,
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itinerary with all mandatory visits assigned and the optional visits unassigned.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output, including:
{
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1
}
2. Too many mandatory visits
When there are too many mandatory visits to assign during the planning window, mandatory visits will be left unassigned.
From the original example, Visit A’s time window has been changed to end within the current planning window, making it a mandatory visit.
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
This time, mandatory Visit D, Visit C, and Visit A are assigned. However, mandatory Visit E is not assigned, and optional Visit B is also not assigned.

-
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": "Too many mandatory 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": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-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",
"name": "Too many mandatory visits example",
"submitDateTime": "2025-02-25T05:24:25.71765674Z",
"startDateTime": "2025-02-25T05:24:37.019507815Z",
"activeDateTime": "2025-02-25T05:24:37.874868846Z",
"completeDateTime": "2025-02-25T05:29:38.112430598Z",
"shutdownDateTime": "2025-02-25T05:29:38.363738161Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-10000medium/-505022soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:26:12Z",
"startServiceTime": "2027-02-01T09:26:12Z",
"departureTime": "2027-02-01T11:26:12Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT26M12S",
"travelDistanceMetersFromPreviousStandstill": 29031,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:30:38Z",
"startServiceTime": "2027-02-01T11:30:38Z",
"departureTime": "2027-02-01T13:30:38Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT4M26S",
"travelDistanceMetersFromPreviousStandstill": 2704,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:54:10Z",
"startServiceTime": "2027-02-01T13:54:10Z",
"departureTime": "2027-02-01T15:54:10Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT23M32S",
"travelDistanceMetersFromPreviousStandstill": 24218,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H23M42S",
"travelTimeFromStartLocationToFirstVisit": "PT26M12S",
"travelTimeBetweenVisits": "PT27M58S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89937,
"travelDistanceFromStartLocationToFirstVisitMeters": 29031,
"travelDistanceBetweenVisitsMeters": 26922,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T16:23:42Z",
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 4,
"optionalVisits": 1,
"vehicleShifts": 1
},
"kpis": {
"totalTravelTime": "PT1H23M42S",
"travelTimeFromStartLocationToFirstVisit": "PT26M12S",
"travelTimeBetweenVisits": "PT27M58S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89937,
"travelDistanceFromStartLocationToFirstVisitMeters": 29031,
"travelDistanceBetweenVisitsMeters": 26922,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itinerary with the assigned visits.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output, including:
{
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1
}
The input metrics show there are 4 mandatory visits, however, the KPIs show that only 3 of the mandatory visits have been assigned.
The run score
is 0hard/-10000medium/-505022soft
which includes the medium penalty for leaving a mandatory visit unassigned.
3. Priority visits
In the previous example one of the mandatory visits was left unassigned. Some visits are more important than others and have a higher priority.
Visits can define their priority.
There are 10 built-in priorities.
"1" is the highest priority, and "10" is the lowest priority.
The default built-in priority is "6". These built-in priorities use exponential penalty weights, where priority "1" is 10 times more important than priority "2", priority "2" is 10 times more important than priority "3" and so on.
Optionally, you can override these weights or define custom priorities in the model configuration overrides.
The following example uses the input dataset from the previous example, but this time the visits have the following priorities:
-
Visit A: 5
-
Visit B: 10
-
Visit C: 5
-
Visit D: 10
-
Visit E: 1
{
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"priority": "5",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"priority": "10",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"priority": "5",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"priority": "10",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"priority": "1",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
]
}
Visit E has the highest priority (1) and is assigned as the first visit of the day. Visit C and Visit A both have a priority of 5 and are assigned.
Visit D is a mandatory visit, but its priority is lower than the other priority visits and is left unassigned. Visit B is an optional visit and is left unassigned.

-
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": "Assign high priority 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": "PT2H",
"priority": "5",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"priority": "10",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"priority": "5",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"priority": "10",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"priority": "1",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-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",
"name": "Assign high priority visits example",
"submitDateTime": "2025-02-25T09:12:20.49667869Z",
"startDateTime": "2025-02-25T09:13:54.643876572Z",
"activeDateTime": "2025-02-25T09:13:55.099440057Z",
"completeDateTime": "2025-02-25T09:18:55.501439836Z",
"shutdownDateTime": "2025-02-25T09:18:55.73524306Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-1medium/-505022soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:26:12Z",
"startServiceTime": "2027-02-01T09:26:12Z",
"departureTime": "2027-02-01T11:26:12Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT26M12S",
"travelDistanceMetersFromPreviousStandstill": 29031,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:30:38Z",
"startServiceTime": "2027-02-01T11:30:38Z",
"departureTime": "2027-02-01T13:30:38Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT4M26S",
"travelDistanceMetersFromPreviousStandstill": 2704,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:54:10Z",
"startServiceTime": "2027-02-01T13:54:10Z",
"departureTime": "2027-02-01T15:54:10Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT23M32S",
"travelDistanceMetersFromPreviousStandstill": 24218,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H23M42S",
"travelTimeFromStartLocationToFirstVisit": "PT26M12S",
"travelTimeBetweenVisits": "PT27M58S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89937,
"travelDistanceFromStartLocationToFirstVisitMeters": 29031,
"travelDistanceBetweenVisitsMeters": 26922,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T16:23:42Z",
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 4,
"optionalVisits": 1,
"vehicleShifts": 1
},
"kpis": {
"totalTravelTime": "PT1H23M42S",
"travelTimeFromStartLocationToFirstVisit": "PT26M12S",
"travelTimeBetweenVisits": "PT27M58S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 89937,
"travelDistanceFromStartLocationToFirstVisitMeters": 29031,
"travelDistanceBetweenVisitsMeters": 26922,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itinerary with the highest priority visits assigned.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output, including:
{
"totalUnassignedVisits": 2,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1
}
4. Assign mandatory visits and optional visits
When the planning window includes enough time to assign mandatory and optional visits, the Require scheduling mandatory visits
constraint will assign as many mandatory visits as possible, then the soft Prefer scheduling optional visits
constraint will assign as many optional visits as possible.
In the following example, there are 5 visits. The planning window is for all day February 1st, 2027 and all day February 2nd 2027:
{
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-03T00:00:00Z"
}
}
Carl has two shifts that cover the planning window:
{
"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"
},
{
"id": "Carl-2027-02-02",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-02T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
}
3 of the visits (Visit C, Visit D, and Visit E) have time windows that are in the planning window, making them mandatory visits. 2 of the visits (Visit A and Visit B) have time windows that end after the planning window, making them optional visits.
The mandatory visits are prioritized over optional visits.
{
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
]
}

The mandatory visits in this example all have time windows on February 1st, and they are assigned to February 1st. The optional visits are assigned to February 2nd. However, mandatory visits can occur on any day in the planning window. If Visit C and Visit E had time windows on February 2nd, the route plan could include optional visits on February 1st. |

-
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": "Assign mandatory and optional 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"
},
{
"id": "Carl-2027-02-02",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-02T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-04T17:00:00Z"
}
]
},
{
"id": "Visit C",
"location": [33.88664, -84.28118],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit D",
"location": [33.71030, -84.05439],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit E",
"location": [33.87673, -84.26024],
"serviceDuration": "PT2H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-03T00: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": "Assign mandatory and optional visits example",
"submitDateTime": "2025-02-25T07:02:34.131265202Z",
"startDateTime": "2025-02-25T07:02:44.418846349Z",
"activeDateTime": "2025-02-25T07:02:44.684333186Z",
"completeDateTime": "2025-02-25T07:07:45.028612891Z",
"shutdownDateTime": "2025-02-25T07:07:45.305145223Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-12800soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit D",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:16:24Z",
"startServiceTime": "2027-02-01T09:16:24Z",
"departureTime": "2027-02-01T11:16:24Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT16M24S",
"travelDistanceMetersFromPreviousStandstill": 15551,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:53:09Z",
"startServiceTime": "2027-02-01T11:53:09Z",
"departureTime": "2027-02-01T13:53:09Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT36M45S",
"travelDistanceMetersFromPreviousStandstill": 43211,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:57:34Z",
"startServiceTime": "2027-02-01T13:57:34Z",
"departureTime": "2027-02-01T15:57:34Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT4M25S",
"travelDistanceMetersFromPreviousStandstill": 2712,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H24M48S",
"travelTimeFromStartLocationToFirstVisit": "PT16M24S",
"travelTimeBetweenVisits": "PT41M10S",
"travelTimeFromLastVisitToEndLocation": "PT27M14S",
"totalTravelDistanceMeters": 89600,
"travelDistanceFromStartLocationToFirstVisitMeters": 15551,
"travelDistanceBetweenVisitsMeters": 45923,
"travelDistanceFromLastVisitToEndLocationMeters": 28126,
"endLocationArrivalTime": "2027-02-01T16:24:48Z",
"technicianCosts": null,
"overtime": null
}
},
{
"id": "Carl-2027-02-02",
"startTime": "2027-02-02T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-02T09:29:56Z",
"startServiceTime": "2027-02-02T09:29:56Z",
"departureTime": "2027-02-02T11:29:56Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT29M56S",
"travelDistanceMetersFromPreviousStandstill": 31493,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-02T12:13:32Z",
"startServiceTime": "2027-02-02T12:13:32Z",
"departureTime": "2027-02-02T14:13:32Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT43M36S",
"travelDistanceMetersFromPreviousStandstill": 49957,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H35M12S",
"travelTimeFromStartLocationToFirstVisit": "PT29M56S",
"travelTimeBetweenVisits": "PT43M36S",
"travelTimeFromLastVisitToEndLocation": "PT21M40S",
"totalTravelDistanceMeters": 102672,
"travelDistanceFromStartLocationToFirstVisitMeters": 31493,
"travelDistanceBetweenVisitsMeters": 49957,
"travelDistanceFromLastVisitToEndLocationMeters": 21222,
"endLocationArrivalTime": "2027-02-02T14:35:12Z",
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 1,
"mandatoryVisits": 3,
"optionalVisits": 2,
"vehicleShifts": 2
},
"kpis": {
"totalTravelTime": "PT3H",
"travelTimeFromStartLocationToFirstVisit": "PT46M20S",
"travelTimeBetweenVisits": "PT1H24M46S",
"travelTimeFromLastVisitToEndLocation": "PT48M54S",
"totalTravelDistanceMeters": 192272,
"travelDistanceFromStartLocationToFirstVisitMeters": 47044,
"travelDistanceBetweenVisitsMeters": 95880,
"travelDistanceFromLastVisitToEndLocationMeters": 49348,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 5,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 2,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itineraries with all mandatory and optional visits assigned.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output, including:
{
"totalUnassignedVisits": 0,
"totalAssignedVisits": 5,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 2,
"totalActivatedVehicles": 1
}
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.
-
Schedule Lunch breaks and personal appointments.
-
Use Time windows to specify visit availability and limit when visits can be scheduled.