Visit requirements
Sometimes a specific technician is required to handle a customer visit. For instance, the visit might be one in a series of related customer visits that all need to be performed by the same technician.
There are also times a specific technician is preferred, for instance, if the customer had a positive experience with a technician and has requested them for a new visit.
If the experience was bad, a customer might want to exclude a specific technician from visiting them again.
This guide explains visit requirements 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. Vehicles required by a visit
For a customer visit that must be handled by specific technicians, for instance, Beth or Carl, specify the list of required vehicle IDs (not vehicle shifts) in the visit’s requiredVehicles
attribute:
{
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT1H30M",
"requiredVehicles": [ "Beth", "Carl" ]
}
]
}
If the requiredVehicles
list is empty or not specified, the visit can be assigned to any vehicle’s shift.
The Require visit vehicle match required vehicles
hard constraint is invoked when a solution assigns a visit to a vehicle not in the visit’s list of required vehicles.
The constraint adds a hard penalty to the run score based on the effective service duration of the visit.
Visits will not be scheduled if they break this constraint.
Below is an example dataset with three vehicles and one visit that can only be assigned to Beth’s or Carl’s shifts:
-
Input
-
Output
Try this example in Timefold Platform by saving the JSON into a file called required-vehicles-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": "Required vehicles example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T12:30:00Z"
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T12:30:00Z",
"maxEndTime": "2027-02-01T16:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT1H30M",
"requiredVehicles": [ "Beth", "Carl" ]
}
]
}
}
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": "Required vehicles example",
"submitDateTime": "2024-07-15T04:39:34.792750944Z",
"startDateTime": "2024-07-15T04:39:40.233674574Z",
"activeDateTime": "2024-07-15T04:39:40.333674574Z",
"completeDateTime": "2024-07-15T04:39:42.884169539Z",
"shutdownDateTime": "2024-07-15T04:39:42.984169539Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-8soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:00:04Z",
"startServiceTime": "2027-02-01T09:00:04Z",
"departureTime": "2027-02-01T10:30:04Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT4S",
"travelDistanceMetersFromPreviousStandstill": 25336,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT8S",
"travelTimeFromStartLocationToFirstVisit": "PT4S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT4S",
"totalTravelDistanceMeters": 50672,
"travelDistanceFromStartLocationToFirstVisitMeters": 25336,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 25336,
"endLocationArrivalTime": "2027-02-01T10:30:08Z"
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T12:30:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT8S",
"travelTimeFromStartLocationToFirstVisit": "PT4S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT4S",
"totalTravelDistanceMeters": 50672,
"travelDistanceFromStartLocationToFirstVisitMeters": 25336,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 25336,
"totalUnassignedVisits": 0
}
}
modelOutput
contains the itineraries with Visit A assigned to Beth.
In this example, Ann’s shift startLocation
is very close to Visit A, so assigning the visit to Ann would result in minimal travel time and is a more optimal schedule.
However, because Visit A requires Beth or Carl, Beth is assigned instead of Ann.
This example illustrates that requirements for specific vehicles might result in a suboptimal schedule compared to a solution where any technician could be assigned the visit.
2. Vehicles preferred by a visit
When a customer visit prefers a specific technician (or technicians) to handle the visit, for instance, Beth or Carl, specify the list of required vehicle IDs (not vehicle shifts) in the visit’s preferredVehicles
attribute:
{
"visits": [
{
"id": "Visit A",
"location": [33.68786, -84.18487],
"serviceDuration": "PT1H30M",
"preferredVehicles": [ "Beth", "Carl" ]
}
]
}
If the preferredVehicles
list is empty or not specified, the visit can be assigned to any vehicle’s shift.
The Prefer visit vehicle match preferred vehicles
soft constraint is invoked for visits that specify preferred vehicles.
To optimize for meeting customers' preferences, the constraint adds a soft penalty to the run score derived from the effective service duration of the visit.
Visits can still be scheduled even if doing so breaks this constraint, but Timefold is incentivized to use the route plan with the best score.
Below is an example dataset with three vehicles and one visit that would prefer Beth or Carl attend the visit:
-
Input
-
Output
Try this example in Timefold Platform by saving the JSON into a file called preferred-vehicles-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": "Preferred vehicles example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T12:30:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.68786, -84.18487],
"serviceDuration": "PT1H30M",
"preferredVehicles": [ "Beth", "Carl" ]
}
]
}
}
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": "Preferred vehicles example",
"submitDateTime": "2025-05-06T06:49:42.714207787Z",
"startDateTime": "2025-05-06T06:49:52.950624789Z",
"activeDateTime": "2025-05-06T06:49:58.242247685Z",
"completeDateTime": "2025-05-06T07:19:58.346135957Z",
"shutdownDateTime": "2025-05-06T07:20:03.561854806Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-3825soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null,
"technicianCosts": null,
"overtime": null
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:29:43Z",
"startServiceTime": "2027-02-01T09:29:43Z",
"departureTime": "2027-02-01T10:59:43Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT29M43S",
"travelDistanceMetersFromPreviousStandstill": 34030,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT59M31S",
"travelTimeFromStartLocationToFirstVisit": "PT29M43S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M48S",
"totalTravelDistanceMeters": 65532,
"travelDistanceFromStartLocationToFirstVisitMeters": 34030,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 31502,
"endLocationArrivalTime": "2027-02-01T11:29:31Z",
"technicianCosts": null,
"overtime": null
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T12:30:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null,
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 1,
"visitGroups": 0,
"vehicles": 3,
"mandatoryVisits": 1,
"optionalVisits": 0,
"vehicleShifts": 3
},
"kpis": {
"totalTravelTime": "PT59M31S",
"travelTimeFromStartLocationToFirstVisit": "PT29M43S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M48S",
"totalTravelDistanceMeters": 65532,
"travelDistanceFromStartLocationToFirstVisitMeters": 34030,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 31502,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 1,
"assignedMandatoryVisits": 1,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itineraries with Visit A assigned to Beth.
3. Vehicles prohibited by a visit
It is also possible that a customer doesn’t want to be served by a specific technician (or technicians).
This can be based on experiences or other reasons that excluded the technician from the visit.
To specify the list of vehicle IDs (not vehicle shifts) that are prohibited from handling the visit, use the visit’s prohibitedVehicles
attribute:
{
"visits": [
{
"id": "Visit A",
"location": [33.68786, -84.18487],
"serviceDuration": "PT1H30M",
"prohibitedVehicles": [ "Carl" ]
}
]
}
If the prohibitedVehicles
list is empty or not specified, the visit can be assigned to any vehicle’s shift.
The Require visit vehicle not match prohibited vehicles
hard constraint is invoked when a solution schedules a visit to a technician prohibited by that visit.
The constraint adds a hard penalty to the run score derived from the effective service duration of the visit.
Visits will not be scheduled if they break this constraint.
Below is an example dataset with three vehicles and one visit that cannot be assigned to Carl’s shift:
-
Input
-
Output
Try this example in Timefold Platform by saving the JSON into a file called prohibited-vehicles-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": "Prohibited vehicles example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.77301, -84.43899],
"minStartTime": "2027-02-01T12:30:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.68786, -84.18487],
"serviceDuration": "PT1H30M",
"prohibitedVehicles": [ "Carl" ]
}
]
}
}
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": "Prohibited vehicles example",
"submitDateTime": "2025-05-13T14:57:24.342067+02:00",
"startDateTime": "2025-05-13T14:57:24.363099+02:00",
"activeDateTime": "2025-05-13T14:57:24.425335+02:00",
"completeDateTime": "2025-05-13T14:57:54.44669+02:00",
"shutdownDateTime": "2025-05-13T14:57:54.462331+02:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-54577soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:30:24Z",
"startServiceTime": "2027-02-01T09:30:24Z",
"departureTime": "2027-02-01T11:00:24Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT30M24S",
"travelDistanceMetersFromPreviousStandstill": 25336,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H48S",
"travelTimeFromStartLocationToFirstVisit": "PT30M24S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT30M24S",
"totalTravelDistanceMeters": 50672,
"travelDistanceFromStartLocationToFirstVisitMeters": 25336,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 25336,
"endLocationArrivalTime": "2027-02-01T11:30:48Z",
"technicianCosts": null,
"overtime": null
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null,
"technicianCosts": null,
"overtime": null
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T12:30:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": null,
"technicianCosts": null,
"overtime": null
}
}
]
}
]
},
"inputMetrics": {
"visits": 1,
"visitGroups": 0,
"vehicles": 3,
"mandatoryVisits": 1,
"optionalVisits": 0,
"vehicleShifts": 3
},
"kpis": {
"totalTravelTime": "PT1H48S",
"travelTimeFromStartLocationToFirstVisit": "PT30M24S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT30M24S",
"totalTravelDistanceMeters": 50672,
"travelDistanceFromStartLocationToFirstVisitMeters": 25336,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 25336,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 1,
"assignedMandatoryVisits": 1,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalTechnicianCosts": null,
"totalOvertime": null
}
}
modelOutput
contains the itineraries with Visit A assigned to Ann.
Next
-
See the full API spec or try the online API.
-
Learn more about field service routing from our YouTube playlist.
-
Learn about Shift hours and overtime.
-
Send technicians with the right skills to visits.