Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Visit service constraints
  • Skills

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
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Visit requirements
      • Multi-vehicle visits
      • Priority visits and optional visits
    • 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
    • Recommendations
      • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
    • Time zones and daylight-saving time (DST)
    • New and noteworthy
    • Upgrading to the latest versions
    • Feature requests
    • Reference guide

Skills

Technicians have different skills and levels of experience which have an impact on which visits they should be assigned. It wouldn’t make any sense to send a plumber to a visit that requires an electrician.

Similarly, even within a skill, it might be necessary to ensure the electrician with the highest skill level is assigned to the most complex visits, while more junior technicians are assigned to less complex visits.

In addition to skill level, technicians have varying levels of effectiveness and complete visits at different rates.

This guide describes how to specify the skill and skill level of the technicians and the required skills for visits with the following examples:

  • Matching skills with visits

  • Skill level

  • Skill multiplier

  • Skill multipliers with multiple skills

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. Matching skills with visits

Visits require technicians with specific skills to complete the work. For instance, if Visit A is a task for an electrician, and both Beth and Carl could be assigned to the visit, but Beth is an electrician and Carl is a plumber, it’s necessary to match Beth’s skill as an electrician with the visit.

Technician’s skills are added to the shifts they’re available to work.

{
  "vehicles": [
    {
      "id": "Beth",
      "shifts": [
        {
          "id": "Beth-2027-02-01",
          "startLocation": [33.68786, -84.18487],
          "minStartTime": "2027-02-01T09:00:00Z",
          "maxEndTime": "2027-02-01T17:00:00Z",
          "skills": [
            {
              "name": "electrician"
            }
          ]
        }
      ]
    },
    {
      "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",
          "skills": [
            {
              "name": "plumber"
            }
          ]
        }
      ]
    }
  ]
}

The skills arrays in the sample, show that Beth is an "electrician" and Carl is a "plumber".

requiredSkills for visits are declared as part of the visit:

{
  "visits": [
    {
      "id": "Visit A",
      "location": [33.77301, -84.43838],
      "serviceDuration": "PT1H30M",
      "requiredSkills": [
        {
          "name": "electrician"
        }
      ]
    }
  ]
}

The requiredSkill for Visit A is "electrician".

In addition to adding the technicians' skills to their shifts and the requiredSkills to the visits, skills must also be declared as part of the modelInput.

{
  "skills": [ "electrician", "plumber" ]
}

See the following example. Failure to declare skills will result in a validation error.

Declaring skills ensures only declared skills are considered in the solution and prevents similar, but different skills from being included, for instance, plumber and pulmber.

Timefold will match Beth’s skill "electrician" to the requiredSkill for Visit A and assign Beth to the visit.

  • 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": "Matching skills with visits example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startLocation": [33.68786, -84.18487],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z",
            "skills": [
              {
                "name": "electrician"
              }
            ]
          }
        ]
      },
      {
        "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",
            "skills": [
              {
                "name": "plumber"
              }
            ]
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT1H30M",
        "requiredSkills": [
          {
            "name": "electrician"
          }
        ]
      }
    ],
    "skills": [ "electrician", "plumber" ]
  }
}
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": "Matching skills with visits example",
    "submitDateTime": "2024-08-20T05:10:01.30884112Z",
    "startDateTime": "2024-08-20T05:10:06.930012466Z",
    "activeDateTime": "2024-08-20T05:10:07.030012466Z",
    "completeDateTime": "2024-08-20T05:15:07.698377551Z",
    "shutdownDateTime": "2024-08-20T05:15:07.798377551Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-3569soft",
    "tags": null,
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit A",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T09:29:57Z",
                "startServiceTime": "2027-02-01T09:29:57Z",
                "departureTime": "2027-02-01T10:59:57Z",
                "effectiveServiceDuration": "PT1H30M",
                "travelTimeFromPreviousStandstill": "PT29M57S",
                "travelDistanceMetersFromPreviousStandstill": 31492,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT59M29S",
              "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT29M32S",
              "totalTravelDistanceMeters": 65476,
              "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 33984,
              "endLocationArrivalTime": "2027-02-01T11:29:29Z"
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-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
            }
          }
        ]
      }
    ]
  },
  "kpis": {
    "totalTravelTime": "PT59M29S",
    "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT29M32S",
    "totalTravelDistanceMeters": 65476,
    "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 33984,
    "totalUnassignedVisits": 0
  }
}

modelOutput contains the solution for the dataset with Beth assigned to Visit A.

2. Skill level

Some jobs are more complicated than others and require experienced technicians. For instance, a newly qualified plumber is unlikely to have the same level of experience as a veteran of twenty years.

Skill level is defined as a positive integer. 1 is the lowest skill level. The higher the number, the more skilled the technician. 1 is the default value if no level is provided.

{
  "id": "Carl-2027-02-01",
  "skills": [
    {
      "name": "plumber",
      "level": 3
    }
  ]
}

Visits can include a minLevel for a requiredSkill. If minLevel is not defined, the default value null will be used. This means any technician with any skill level could be assigned to the visit.

{
  "id": "Visit A",
  "requiredSkills": [
    {
      "name": "plumber",
      "minLevel": 2
    }
  ]
}

Timefold will assign a visit to a technician with the correct skill, for instance "plumber" at or above the minLevel specified for the visit.

skill level

In the following example, Timefold will assign Visit A to Carl because he has a skill level 3, and Beth only has a skill level 1.

  • 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": "Matching skill levels with visits example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startLocation": [33.68786, -84.18487],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z",
            "skills": [
              {
                "name": "plumber",
                "level": 1
              }
            ]
          }
        ]
      },
      {
        "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",
            "skills": [
              {
                "name": "plumber",
                "level": 3
              }
            ]
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT1H30M",
        "requiredSkills": [
          {
            "name": "plumber",
            "minLevel": 2
          }
        ]
      }
    ],
    "skills": [ "plumber" ]
  }
}
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": "Matching skill levels with visits example",
    "submitDateTime": "2024-08-20T05:07:30.782484459Z",
    "startDateTime": "2024-08-20T05:07:36.233619326Z",
    "activeDateTime": "2024-08-20T05:07:36.333619326Z",
    "completeDateTime": "2024-08-20T05:08:37.165206321Z",
    "shutdownDateTime": "2024-08-20T05:08:37.265206321Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-8969soft",
    "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
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit A",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T09:29:57Z",
                "startServiceTime": "2027-02-01T09:29:57Z",
                "departureTime": "2027-02-01T10:59:57Z",
                "effectiveServiceDuration": "PT1H30M",
                "travelTimeFromPreviousStandstill": "PT29M57S",
                "travelDistanceMetersFromPreviousStandstill": 31492,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT59M29S",
              "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT29M32S",
              "totalTravelDistanceMeters": 65476,
              "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 33984,
              "endLocationArrivalTime": "2027-02-01T11:29:29Z"
            }
          }
        ]
      }
    ]
  },
  "kpis": {
    "totalTravelTime": "PT59M29S",
    "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT29M32S",
    "totalTravelDistanceMeters": 65476,
    "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 33984,
    "totalUnassignedVisits": 0
  }
}

modelOutput contains the solution with Carl assigned to Visit A.

If a visit specifies a minLevel with a higher value than the skill level of all the available technicians, the visit will be left unassigned. To avoid assigning overqualified technicians, Timefold is incentivized to match the lowest matching skill level from the available technicians that meet or exceed the minLevel.

3. Skill multiplier

Some technicians, even at the same skill level, are more proficient than others and regularly complete visits quicker than other technicians.

skill multiplier

The multiplier is added to the skills definition. multiplier is a float number, for instance, 0.5. The visit serviceDuration is multiplied by the multiplier to determine the effectiveServiceDuration of the visit.

With a multiplier of 0.5 and a serviceDuration of 1 hour, the effectiveServiceDuration becomes 30 minutes.

{
  "id": "Carl-2027-02-01",
  "skills": [
    {
      "name": "plumber",
      "multiplier": 0.5
    }
  ]
}
  • 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": "Skill multiplier example"
    }
  },
  "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",
            "skills": [
              {
                "name": "plumber",
                "multiplier": 0.5
              }
            ]
          }
        ]
      },
      {
        "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",
            "skills": [
              {
                "name": "plumber",
                "multiplier": null
              }
            ]
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT1H30M",
        "requiredSkills": [
          {
            "name": "plumber"
          }
        ]
      }
    ],
    "skills": [ "plumber" ]
  }
}
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": "Skill multiplier example",
    "submitDateTime": "2024-08-20T07:43:15.458890029Z",
    "startDateTime": "2024-08-20T07:43:21.032131885Z",
    "activeDateTime": "2024-08-20T07:43:21.132131885Z",
    "completeDateTime": "2024-08-20T07:43:37.791116599Z",
    "shutdownDateTime": "2024-08-20T07:43:37.891116599Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-3569soft",
    "tags": null,
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Ann",
        "shifts": [
          {
            "id": "Ann-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit A",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T09:29:57Z",
                "startServiceTime": "2027-02-01T09:29:57Z",
                "departureTime": "2027-02-01T10:14:57Z",
                "effectiveServiceDuration": "PT45M",
                "travelTimeFromPreviousStandstill": "PT29M57S",
                "travelDistanceMetersFromPreviousStandstill": 31492,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT59M29S",
              "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT29M32S",
              "totalTravelDistanceMeters": 65476,
              "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 33984,
              "endLocationArrivalTime": "2027-02-01T10:44:29Z"
            }
          }
        ]
      },
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-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
            }
          }
        ]
      }
    ]
  },
  "kpis": {
    "totalTravelTime": "PT59M29S",
    "travelTimeFromStartLocationToFirstVisit": "PT29M57S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT29M32S",
    "totalTravelDistanceMeters": 65476,
    "travelDistanceFromStartLocationToFirstVisitMeters": 31492,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 33984,
    "totalUnassignedVisits": 0
  }
}

modelOutput contains the solution. Ann has been assigned Visit A which has an effectiveServiceDuration of 45 minutes, which is half the expected serviceDuration.

Skill level and multiplier can be used together: However, they have no effect on each other.

  • level determines which technicians can be assigned to a visit.

  • multiplier determines the effectiveServiceDuration of visits.

If multiplier for a skill is not defined, the default value null will be used, and the effectiveServiceDuration will not be affected by this skill.

null is used instead of 1.0 to avoid affecting the average duration calculation for visits in a visit group.

3.1. Skill multipliers with multiple skills

Some visits require multiple skills. For instance, a visit might need a technician who is a plumber and an electrician, another visit might need a plumber who also speaks Spanish.

For a technician who is both a plumber and an electrician, these skills and their multipliers are added to the employee:

{
  "id": "Carl-2027-02-01",
  "skills": [
    {
      "name": "plumber",
      "multiplier": 0.5
    },
    {
      "name": "electrician",
      "multiplier": 1.0
    }
  ]
}

For a visit with a service duration of 1 hour that requires a plumber and an electrician, the multipliers are averaged and then multiplied by the service duration to give an effective service duration. In this case the effective service duration would be 45 minutes.

The multiplier isn’t always required. For instance, when a visit requires a plumber who speaks Spanish, because the skill spanish, while essential, will not affect the speed of the work, the multiplier can be omitted and the default value null will be used.

{
  "id": "Carl-2027-02-01",
  "skills": [
    {
      "name": "plumber",
      "multiplier": 0.5
    },
    {
      "name": "spanish"
    }
  ]
}

In this case, where only one multiplier has been explicitly added, the average of the multipliers is 0.5 and a service duration of 1 hour would result in an effective service duration of 30 minutes.

Next

  • Understand the constraints of the Field Service Routing model.

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

  • Manage shift times with Time zones and daylight-saving time (DST) changes.

  • Learn about Shift hours and overtime

  • Use time windows to specify visit availability and limit when visits can be scheduled.

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