Visit assignment restrictions

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. Alternatively, the customer might have requested a specific technician.

Prerequisite

To run the examples in this guide, you need to authenticate with a valid API key:

  1. Log in to Timefold Platform: app.timefold.ai

  2. From the Dashboard, click your username, and from the drop-down menu select API Keys.

  3. Copy the default API key.

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.

Any solution that assigns the visit to a vehicle ID that is not "Beth" or "Carl" will be penalized for breaking a hard constraint.

Learn about the hard and soft constraints in the Field Service Routing model.

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",
    "completeDateTime": "2024-07-15T04:39:42.884169539Z",
    "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.

visit assignment required vehicles

Next