Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Vehicle resource constraints
  • Technician ratings

Field Service Routing

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with field service routing
    • Understanding the API
    • Constraints
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Technician costs
      • Technician ratings
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Visit requirements
      • Multi-vehicle visits
      • Priority visits and optional visits
      • Visit service level agreement (SLA)
    • Recommendations
      • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
    • Real-time planning
      • 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
    • Time zones and daylight-saving time (DST)
    • New and noteworthy
    • Upgrading to the latest versions
    • Feature requests
    • Reference guide

Technician ratings

Customers often provide feedback on how well technicians completed the service they requested.

Ratings are usually captured on a scale from 1 to 5 or 1 to 10, the higher the rating the better the perceived service.

Companies want to send their best (highest rated) technicians to ensure the quality of service is as high as possible.

Timefold’s Field Service Routing model can accept technician ratings as input data and assign the highest rated technicians to tasks as long as other constraints are also met.

Customer ratings are often highly subjective and often are not equal to the actual quality of the work the technician did.

Prerequisites

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

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

  2. From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, 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.

1. Specifying technician ratings

Technician ratings are added in vehicles:

{
  "vehicles": [
    {
      "id": "Beth",
      "technicianRating": 4.0
    }
  ]
}

technicianRating is a number (with or without decimals) that is equal to or greater than 0.

There are no other restrictions on technicianRating to keep it as flexible as possible for different rating scenarios.

When ratings don’t exist for every technician, you can define a default rating for all vehicles that don’t have a rating defined with the input.

defaultTechnicianRating property is added to the overrides in the model configuration in the input:

{
  "model": {
    "overrides": {
      "maximizeTechnicianRatingWeight": 1,
      "defaultTechnicianRating": 5
    }
  }
}

By default maximizeTechnicianRatingWeight is 0 and is not active in solving.

Setting maximizeTechnicianRatingWeight to 1 will give the constraint the same weight as other constraints with a value 1. With a rating of at least 1, the constraint will be invoked during solving.

defaultTechnicianRating sets the default rating for all technicians.

If no defaultTechnicianRating is defined in the model configuration and there are vehicles without a rating, the model will use a rating of 0.0 as the default.

maximizeTechnicianRatingWeight and defaultTechnicianRating can also be defined in configuration profiles.

To access configuration profiles:

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

  2. Select the Field Service Routing model.

  3. Select configuration profiles.

Select the profile you want to change or create a new profile.

Change Maximize technician rating and defaultTechnicianRating to the required value. The updated values will be used with any run that uses this configuration profile.

Learn more about configuration profiles.

2. Example

The constraint is deactivated by default and needs to be activated with an override of 1 for the maximizeTechnicianRatingWeight property in the model config.

The following example shows how to define the technician ratings in the input. It also activates the constraint to maximize the technician ratings and defines a default value for vehicles that don’t have a rating defined.

In this case Beth has a rating of 4.0. Carl has no technician rating defined, but the default rating in the model configuration is 5.0. If all else is the same or very similar between Carl and Beth, Carl will be assigned the visits as he has a higher rating.

  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/field-service-routing/v1/route-plans [email protected]
{
  "config": {
    "run": {
      "name": "Technician rating example"
    },
    "model": {
      "overrides": {
        "maximizeTechnicianRatingWeight": 1,
        "defaultTechnicianRating": 5
      }
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Beth",
        "technicianRating": 4.0,
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startLocation": [33.77301, -84.43838],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z",
            "cost": {
              "fixedCost": 100.0
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startLocation": [33.68786, -84.18487],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z",
            "cost": {
              "fixedCost": 100.0
            }
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT2H"
      },
      {
        "id": "Visit B",
        "location": [33.74699, -84.02504],
        "serviceDuration": "PT2H"
      }
    ]
  }
}
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": "Technician rating example",
    "submitDateTime": "2025-05-15T14:41:20.255255+02:00",
    "startDateTime": "2025-05-15T14:41:20.274576+02:00",
    "activeDateTime": "2025-05-15T14:41:20.3483+02:00",
    "completeDateTime": "2025-05-15T14:41:50.369955+02:00",
    "shutdownDateTime": "2025-05-15T14:41:50.385054+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-1134239soft",
    "tags": null,
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "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": 100.0,
              "overtime": null
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit B",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T09:19:25Z",
                "startServiceTime": "2027-02-01T09:19:25Z",
                "departureTime": "2027-02-01T11:19:25Z",
                "effectiveServiceDuration": "PT2H",
                "travelTimeFromPreviousStandstill": "PT19M25S",
                "travelDistanceMetersFromPreviousStandstill": 16179,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              },
              {
                "id": "Visit A",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T12:05:24Z",
                "startServiceTime": "2027-02-01T12:05:24Z",
                "departureTime": "2027-02-01T14:05:24Z",
                "effectiveServiceDuration": "PT2H",
                "travelTimeFromPreviousStandstill": "PT45M59S",
                "travelDistanceMetersFromPreviousStandstill": 38320,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT1H35M44S",
              "travelTimeFromStartLocationToFirstVisit": "PT19M25S",
              "travelTimeBetweenVisits": "PT45M59S",
              "travelTimeFromLastVisitToEndLocation": "PT30M20S",
              "totalTravelDistanceMeters": 79782,
              "travelDistanceFromStartLocationToFirstVisitMeters": 16179,
              "travelDistanceBetweenVisitsMeters": 38320,
              "travelDistanceFromLastVisitToEndLocationMeters": 25283,
              "endLocationArrivalTime": "2027-02-01T14:35:44Z",
              "technicianCosts": 100.0,
              "overtime": null
            }
          }
        ]
      }
    ]
  },
  "inputMetrics": {
    "visits": 2,
    "visitGroups": 0,
    "vehicles": 2,
    "mandatoryVisits": 2,
    "optionalVisits": 0,
    "vehicleShifts": 2
  },
  "kpis": {
    "totalTravelTime": "PT1H35M44S",
    "travelTimeFromStartLocationToFirstVisit": "PT19M25S",
    "travelTimeBetweenVisits": "PT45M59S",
    "travelTimeFromLastVisitToEndLocation": "PT30M20S",
    "totalTravelDistanceMeters": 79782,
    "travelDistanceFromStartLocationToFirstVisitMeters": 16179,
    "travelDistanceBetweenVisitsMeters": 38320,
    "travelDistanceFromLastVisitToEndLocationMeters": 25283,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 2,
    "assignedMandatoryVisits": 2,
    "assignedOptionalVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 0.0,
    "totalTechnicianCosts": 200.0,
    "totalOvertime": null,
    "averageTechnicianRating": 5.0
  }
}

modelOutput contains the visits assigned to Carl’s shift itinerary.

The "averageTechnicianRating of 5.0 is included in the KPIs.

Next

  • See the full API spec or try the online API.

  • Learn more about field service routing from our YouTube playlist.

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