Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
    • Pick-up and Delivery Routing
  • Platform
Try models
  • Field Service Routing
  • Recommendations
  • Bulk time window recommendations

Field Service Routing

    • Introduction
    • Getting started: Hello world
    • User guide
      • Terminology
      • Use case guide
      • Scheduling API concepts
      • Integration
      • Constraints
      • Using the API
        • Using the OpenAPI spec
        • API tooling
      • Demo datasets
      • Input datasets
        • Model configuration
        • Model input
        • Planning window
        • Time zones and daylight-saving time (DST)
      • Routing with Timefold’s maps service
      • Input validation
      • Model response
      • Output datasets
        • Metadata
        • Model output
        • Input metrics
        • Key performance indicators (KPIs)
      • Key performance indicators (KPIs)
      • Metrics and optimization goals
      • Score analysis
      • Visualizations
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Route optimization
      • Technician costs
      • Technician ratings
      • Coverage area
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Multi-vehicle visits
      • Multi-day schedules and movable visits
      • Priority visits and optional visits
      • Visit service level agreement (SLA)
      • Duration added for first visit on location
      • Visit profit
      • Visit requirements and tags
        • Visit requirements
        • Tags
    • Manual intervention
    • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
      • Bulk time window recommendations
    • Real-time planning
      • Real-time planning: extended visit
      • Real-time planning: reassignment
      • Real-time planning: emergency visit
      • Real-time planning: no show
      • Real-time planning: technician ill
      • Real-time planning: pinning visits
      • Real-time planning: actual arrival and departure times
    • Real-time planning with patches
      • Real-time planning: extended visit (using patches)
      • Real-time planning: reassignment (using patches)
      • Real-time planning: emergency visit (using patches)
      • Real-time planning: no show (using patches)
      • Real-time planning: technician ill (using patches)
      • Real-time planning: pinning visits (using patches)
    • Scenarios
      • Configuring labor law compliance
      • Ferry Connections
      • Long-running visits
    • Changelog
    • Upgrade to the latest version
    • Feature requests

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
  • 2. Include dependencies
  • 3. Recommendations with pinned visits

1. Bulk recommendations for unsolved route plans

Learn how to configure an API Key to run the examples in this guide:
  1. Log in to Timefold Platform: app.timefold.ai.

  2. From the Dashboard, click your tenant, and from the drop-down menu select Manage tenant, then choose API Keys.

  3. 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.

Continuous planning includes four stages:

  1. Historic
    The historic stage includes schedules that have already been executed and cannot be changed.

  2. Published
    The published stage includes schedules that have been shared with your customers and technicians and should only be changed in special circumstances.

  3. 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.

  4. 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.

  1. Ann is assigned to both Visit A and Visit B in her shift.

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

  1. Ann is assigned to both Visit B and Visit A (in this order).

  2. Ann is assigned to both Visit A and Visit B.

  3. 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.

  • © 2026 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default