Bulk time window recommendations
| This feature is in preview. The API may change in a future release. |
Bulk recommendations provide time window recommendations for multiple individual visits that are not a part of a visit group. Unlike visit group recommendations where all visits must be assigned to separate technicians, bulk recommendations let the optimizer decide whether the visits are best served by the same technician or by different technicians.
This guide explains bulk recommendations with the following examples:
1. Bulk recommendations for unsolved route plans
Learn how to configure an API Key to run the examples in this guide:
In the examples, replace |
Continuous planning includes four stages:
-
Historic
The historic stage includes schedules that have already been executed and cannot be changed. -
Published
The published stage includes schedules that have been shared with your customers and technicians and should only be changed in special circumstances. -
Draft
The draft stage includes schedules that can change without impacting customers or technicians. At some point, draft schedules will be solved and moved to the published stage. -
Unplanned
The unplanned stage is too far away to start planning schedules.
When customers request multiple visits during a time period that is still in the draft phase, you can use bulk recommendations to provide time windows to choose from. The customer’s chosen time window can be added to the draft schedule, which will be optimized before being published.
1.1. The recommendation input dataset
A recommendation input dataset includes the following:
1.1.1. Max number of recommendations per time window
The maximum number of recommendations to make per time window is specified in the input dataset.
The recommendation will consider the technicians whose vehicle shifts overlap with the specified time window and make the number of recommendations specified in maxNumberOfRecommendationsPerTimeWindow.
{
"maxNumberOfRecommendationsPerTimeWindow": 3
}
1.1.2. Fit visit IDs
The IDs of the visits to make the recommendations for.
The provided fitVisitIds must match IDs from visits in the modelInput.
Between 1 and 20 visits can be specified.
{
"fitVisitIds": ["Visit A", "Visit B"]
}
1.1.3. Time windows
The time windows to be considered for the recommendation. For instance, a full working day from 09:00 to 17:00 on February 1, 2027.
Time windows can cover an entire day or even longer, but as time windows get longer, you reduce the number of recommendations and the customer’s options.
For bulk recommendations, it is not required to define the time windows in the recommendation request:
-
If not provided, the time windows defined by the visits will be used instead.
-
If both the time windows in the recommendation request and the visit’s time windows are provided, the time windows in the recommendation request will be used for all visits to make recommendation for.
It is highly recommended to provide time windows either in the recommendation request or with the visits. This way, performance is not decreased by attempting to search visit assignments out of the expected time window.
{
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
1.1.4. Model input
The model input must include the technicians' vehicles and shifts and all visits specified in fitVisitIds.
To learn more about vehicle shifts and visits, see Shift hours and overtime.
{
"modelInput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H"
},
{
"id": "Visit B",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H"
}
]
}
}
1.2. Submit the input dataset
Submit the recommendations input dataset to the API endpoint: /v1/route-plans/recommendations/bulk-recommend-time-windows.
If you want individual constraint match justifications included in the response, you can specify an optional boolean includeJustifications query parameter:
/v1/route-plans/recommendations/bulk-recommend-time-windows?includeJustifications=true.
The HTTP response provides the recommendations.
-
Input
-
Output (without justifications)
-
Output (with justifications)
Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
|
To get recommendations without constraint match justifications (default):
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/recommendations/bulk-recommend-time-windows [email protected]
To get recommendations with constraint match justifications:
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/recommendations/bulk-recommend-time-windows?includeJustifications=true [email protected]
{
"maxNumberOfRecommendationsPerTimeWindow": 3,
"fitVisitIds": ["Visit A", "Visit B"],
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
],
"modelInput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H"
},
{
"id": "Visit B",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H"
}
]
}
}
{
"recommendations": [
{
"scoreDiff": "0hard/20000medium/-7902soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-362soft",
"constraintName": "Balance time utilization",
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"arrivalTime": "2027-02-01T09:31:10Z",
"startServiceTime": "2027-02-01T09:31:10Z",
"departureTime": "2027-02-01T10:31:10Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT31M10S",
"travelDistanceMetersFromPreviousStandstill": 25971,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit A",
"arrivalTime": "2027-02-01T11:11:22Z",
"startServiceTime": "2027-02-01T11:11:22Z",
"departureTime": "2027-02-01T12:11:22Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"endLocationArrivalTime": "2027-02-01T13:05:40Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H2M50S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"averageTravelDistanceMetersPerVisit": 52359,
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
},
{
"scoreDiff": "0hard/20000medium/-7902soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-362soft",
"constraintName": "Balance time utilization",
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"arrivalTime": "2027-02-01T09:54:18Z",
"startServiceTime": "2027-02-01T09:54:18Z",
"departureTime": "2027-02-01T10:54:18Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT54M18S",
"travelDistanceMetersFromPreviousStandstill": 45245,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit B",
"arrivalTime": "2027-02-01T11:34:30Z",
"startServiceTime": "2027-02-01T11:34:30Z",
"departureTime": "2027-02-01T12:34:30Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT54M18S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT31M10S",
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 45245,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 25971,
"endLocationArrivalTime": "2027-02-01T13:05:40Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H2M50S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT54M18S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT31M10S",
"averageTravelDistanceMetersPerVisit": 52359,
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 45245,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 25971,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
},
{
"scoreDiff": "0hard/20000medium/-8931soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-387soft",
"constraintName": "Balance time utilization",
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-8544soft",
"constraintName": "Minimize travel time",
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"arrivalTime": "2027-02-01T10:06:04Z",
"startServiceTime": "2027-02-01T10:06:04Z",
"departureTime": "2027-02-01T11:06:04Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT1H6M4S",
"travelDistanceMetersFromPreviousStandstill": 55061,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit B",
"arrivalTime": "2027-02-01T11:46:16Z",
"startServiceTime": "2027-02-01T11:46:16Z",
"departureTime": "2027-02-01T12:46:16Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H22M24S",
"travelTimeFromStartLocationToFirstVisit": "PT1H6M4S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT36M8S",
"totalTravelDistanceMeters": 118671,
"travelDistanceFromStartLocationToFirstVisitMeters": 55061,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 30109,
"endLocationArrivalTime": "2027-02-01T13:22:24Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H11M12S",
"totalTravelTime": "PT2H22M24S",
"travelTimeFromStartLocationToFirstVisit": "PT1H6M4S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT36M8S",
"averageTravelDistanceMetersPerVisit": 59336,
"totalTravelDistanceMeters": 118671,
"travelDistanceFromStartLocationToFirstVisitMeters": 55061,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 30109,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
}
]
}
{
"recommendations": [
{
"scoreDiff": "0hard/20000medium/-7902soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-362soft",
"constraintName": "Balance time utilization",
"matchesDiff": [
{
"score": "0hard/0medium/-362soft",
"justification": {
"unfairnessScore": 362,
"description": "Unfairness score for the time utilization of vehicles '362'."
}
},
{
"score": "0hard/0medium/0soft",
"justification": {
"unfairnessScore": 0,
"description": "Unfairness score for the time utilization of vehicles '0'."
}
}
],
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchesDiff": [
{
"score": "0hard/0medium/-5670soft",
"justification": {
"visitId": "Visit A",
"travelTime": "PT1H34M30S",
"description": "Visit (Visit A) has travel time of 'PT1H34M30S'."
}
},
{
"score": "0hard/0medium/-1870soft",
"justification": {
"visitId": "Visit B",
"travelTime": "PT31M10S",
"description": "Visit (Visit B) has travel time of 'PT31M10S'."
}
}
],
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchesDiff": [
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit A",
"description": "Mandatory visit (Visit A) has been left unassigned."
}
},
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit B",
"description": "Mandatory visit (Visit B) has been left unassigned."
}
}
],
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"arrivalTime": "2027-02-01T09:31:10Z",
"startServiceTime": "2027-02-01T09:31:10Z",
"departureTime": "2027-02-01T10:31:10Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT31M10S",
"travelDistanceMetersFromPreviousStandstill": 25971,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit A",
"arrivalTime": "2027-02-01T11:11:22Z",
"startServiceTime": "2027-02-01T11:11:22Z",
"departureTime": "2027-02-01T12:11:22Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"endLocationArrivalTime": "2027-02-01T13:05:40Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H2M50S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"averageTravelDistanceMetersPerVisit": 52359,
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
},
{
"scoreDiff": "0hard/20000medium/-7902soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-362soft",
"constraintName": "Balance time utilization",
"matchesDiff": [
{
"score": "0hard/0medium/-362soft",
"justification": {
"unfairnessScore": 362,
"description": "Unfairness score for the time utilization of vehicles '362'."
}
},
{
"score": "0hard/0medium/0soft",
"justification": {
"unfairnessScore": 0,
"description": "Unfairness score for the time utilization of vehicles '0'."
}
}
],
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchesDiff": [
{
"score": "0hard/0medium/-4282soft",
"justification": {
"visitId": "Visit B",
"travelTime": "PT1H11M22S",
"description": "Visit (Visit B) has travel time of 'PT1H11M22S'."
}
},
{
"score": "0hard/0medium/-3258soft",
"justification": {
"visitId": "Visit A",
"travelTime": "PT54M18S",
"description": "Visit (Visit A) has travel time of 'PT54M18S'."
}
}
],
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchesDiff": [
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit A",
"description": "Mandatory visit (Visit A) has been left unassigned."
}
},
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit B",
"description": "Mandatory visit (Visit B) has been left unassigned."
}
}
],
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"arrivalTime": "2027-02-01T09:54:18Z",
"startServiceTime": "2027-02-01T09:54:18Z",
"departureTime": "2027-02-01T10:54:18Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT54M18S",
"travelDistanceMetersFromPreviousStandstill": 45245,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit B",
"arrivalTime": "2027-02-01T11:34:30Z",
"startServiceTime": "2027-02-01T11:34:30Z",
"departureTime": "2027-02-01T12:34:30Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT54M18S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT31M10S",
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 45245,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 25971,
"endLocationArrivalTime": "2027-02-01T13:05:40Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H2M50S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT54M18S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT31M10S",
"averageTravelDistanceMetersPerVisit": 52359,
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 45245,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 25971,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
},
{
"scoreDiff": "0hard/20000medium/-8931soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-387soft",
"constraintName": "Balance time utilization",
"matchesDiff": [
{
"score": "0hard/0medium/-387soft",
"justification": {
"unfairnessScore": 387,
"description": "Unfairness score for the time utilization of vehicles '387'."
}
},
{
"score": "0hard/0medium/0soft",
"justification": {
"unfairnessScore": 0,
"description": "Unfairness score for the time utilization of vehicles '0'."
}
}
],
"matchCountDiff": 0
},
{
"score": "0hard/0medium/-8544soft",
"constraintName": "Minimize travel time",
"matchesDiff": [
{
"score": "0hard/0medium/-4580soft",
"justification": {
"visitId": "Visit B",
"travelTime": "PT1H16M20S",
"description": "Visit (Visit B) has travel time of 'PT1H16M20S'."
}
},
{
"score": "0hard/0medium/-3964soft",
"justification": {
"visitId": "Visit A",
"travelTime": "PT1H6M4S",
"description": "Visit (Visit A) has travel time of 'PT1H6M4S'."
}
}
],
"matchCountDiff": 2
},
{
"score": "0hard/20000medium/0soft",
"constraintName": "Require scheduling mandatory visits",
"matchesDiff": [
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit A",
"description": "Mandatory visit (Visit A) has been left unassigned."
}
},
{
"score": "0hard/10000medium/0soft",
"justification": {
"visitId": "Visit B",
"description": "Mandatory visit (Visit B) has been left unassigned."
}
}
],
"matchCountDiff": -2
}
],
"timeWindow": {
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
},
"vehicleShifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit A",
"arrivalTime": "2027-02-01T10:06:04Z",
"startServiceTime": "2027-02-01T10:06:04Z",
"departureTime": "2027-02-01T11:06:04Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT1H6M4S",
"travelDistanceMetersFromPreviousStandstill": 55061,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit B",
"arrivalTime": "2027-02-01T11:46:16Z",
"startServiceTime": "2027-02-01T11:46:16Z",
"departureTime": "2027-02-01T12:46:16Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H22M24S",
"travelTimeFromStartLocationToFirstVisit": "PT1H6M4S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT36M8S",
"totalTravelDistanceMeters": 118671,
"travelDistanceFromStartLocationToFirstVisitMeters": 55061,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 30109,
"endLocationArrivalTime": "2027-02-01T13:22:24Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H11M12S",
"totalTravelTime": "PT2H22M24S",
"travelTimeFromStartLocationToFirstVisit": "PT1H6M4S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT36M8S",
"averageTravelDistanceMetersPerVisit": 59336,
"totalTravelDistanceMeters": 118671,
"travelDistanceFromStartLocationToFirstVisitMeters": 55061,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 30109,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
}
]
}
1.3. The recommendations
In this example, there is a maximum of two recommendations per time window. The optimizer evaluates whether it is more efficient for a single technician to handle both visits or for two technicians to handle one visit each.
-
Ann is assigned to both Visit A and Visit B in her shift.
-
Ann is assigned to Visit A and Beth is assigned to Visit B in their respective shifts.
Each recommendation includes the following:
1.3.1. ScoreDiff
The score diff is based on the difference before the visits have been scheduled and after all visits have been scheduled.
{
"scoreDiff": "0hard/20000medium/-7902soft"
}
1.3.2. Constraint scores without justifications
The score for each constraint that has changed, for instance:
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchCountDiff": 2
}
1.3.3. Constraint scores with justifications
The score and justification for each constraint that has changed, for instance:
{
"score": "0hard/0medium/-7540soft",
"constraintName": "Minimize travel time",
"matchesDiff": [
{
"score": "0hard/0medium/-5670soft",
"justification": {
"visitId": "Visit A",
"travelTime": "PT1H34M30S",
"description": "Visit (Visit A) has travel time of 'PT1H34M30S'."
}
},
{
"score": "0hard/0medium/-1870soft",
"justification": {
"visitId": "Visit B",
"travelTime": "PT31M10S",
"description": "Visit (Visit B) has travel time of 'PT31M10S'."
}
}
],
"matchCountDiff": 2
}
1.3.4. The time window
The time window the recommendation is for:
{
"timeWindow": {
"startTime": "2027-02-01T09:00:00Z",
"endTime": "2027-02-01T17:00:00Z"
}
}
1.3.5. The vehicle shifts
The vehicle shifts the recommendation applies to along with the itinerary, metrics, and KPIs of each shift.
When all visits are assigned to a single technician, there is one entry in vehicleShifts.
When the visits are split across multiple technicians, there is one entry per technician.
For the first recommendation (Ann handles both visits), there is a single vehicle shift with two visits in the itinerary:
{
"vehicleShifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"arrivalTime": "2027-02-01T09:31:10Z",
"startServiceTime": "2027-02-01T09:31:10Z",
"departureTime": "2027-02-01T10:31:10Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT31M10S",
"travelDistanceMetersFromPreviousStandstill": 25971,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Visit A",
"arrivalTime": "2027-02-01T11:11:22Z",
"startServiceTime": "2027-02-01T11:11:22Z",
"departureTime": "2027-02-01T12:11:22Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT40M12S",
"travelDistanceMetersFromPreviousStandstill": 33501,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"endLocationArrivalTime": "2027-02-01T13:05:40Z",
"overtime": "PT0S",
"availableOvertime": "PT0S"
}
}
],
"dependentVehicleShifts": [],
"kpis": {
"averageTravelTimePerVisit": "PT1H2M50S",
"totalTravelTime": "PT2H5M40S",
"travelTimeFromStartLocationToFirstVisit": "PT31M10S",
"travelTimeBetweenVisits": "PT40M12S",
"travelTimeFromLastVisitToEndLocation": "PT54M18S",
"averageTravelDistanceMetersPerVisit": 52359,
"totalTravelDistanceMeters": 104717,
"travelDistanceFromStartLocationToFirstVisitMeters": 25971,
"travelDistanceBetweenVisitsMeters": 33501,
"travelDistanceFromLastVisitToEndLocationMeters": 45245,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"unassignedMandatoryVisits": 0,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 0.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S"
}
}
1.4. Implementing the recommendation
The recommendation output for this example includes three recommendations for the 09:00–17:00 time window:
-
Ann is assigned to both Visit B and Visit A (in this order).
-
Ann is assigned to both Visit A and Visit B.
-
Beth is assigned to both Visit A and Visit B.
| The recommendations include specific times, however, the plan is still a draft and those times will likely change as additional visits are added to the plan. |
If the customer agrees to one of the time windows, it must be added to the recommendation input dataset (the draft plan).
In this instance, the customer accepted the first recommendation:
-
Both Visit B and Visit A are assigned to Ann on February 1st between 09:00 and 17:00.
First, add the visits to Ann’s itinerary in the order they appear in the recommended itinerary:
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT"
},
{
"id": "Visit A",
"kind": "VISIT"
}
]
}
]
}
Next, add the time window to both visits:
{
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Visit B",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
]
}
If the customer had accepted the second recommendation instead, Visit A followed by Visit B would be added to Ann’s itinerary, each with the same time window.
| It is important to decide how to manage concurrent requests. If multiple requests are made for recommendations, which recommendation is added to the input dataset and what happens to other recommendations? |
2. Include dependencies
The recommendations output only includes vehicle shifts that are directly impacted by the recommendations. However, there are times when recommendations also change vehicle shifts that are not part of the recommendations. For instance, some jobs require multiple visits in a specific order to complete the work (learn more about this topic in Visit dependencies).
To include all visit dependencies in the recommendations output, include "includeDependencies": "ALL" in the recommendations input dataset:
{
"maxNumberOfRecommendationsPerTimeWindow": 2,
"fitVisitIds": ["Visit A", "Visit B"],
"includeDependencies": "ALL"
}
"includeDependencies" can be set to ALL or NONE.
The default value is NONE.
When set to ALL, the response includes a dependentVehicleShifts list alongside vehicleShifts.
This list contains shifts that were rescheduled to accommodate the recommended visits but are not directly assigned any of the recommended visits.
3. Recommendations with pinned visits
When a plan has been published, it will often include visits that can’t be moved to accommodate new visits on the schedule.
Recommendations can reschedule visits to accommodate new visits, however, visits can be pinned to prevent them from being moved.
To pin visits, add "pinningRequested": true to each visit, and add the "minStartTravelTime" from the previous output dataset to each visit.
{
"visits": [
{
"id": "Visit D",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H",
"minStartTravelTime": "2027-02-01T00:00:00Z",
"pinningRequested": true
}
]
}
Any visit a technician is scheduled to make before a pinned visit must also be pinned.
Learn more about pinning in the Real-time planning: pinning visits guide.
Next
-
See the full API spec or try the online API.
-
Learn more about field service routing from our YouTube playlist.
-
Learn about Visit time window recommendations.
-
Learn about Visit group time window recommendations for multiple visits that are a part of a visit group.