Skills and skill levels

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:

Prerequisites

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.

The times displayed in the visualizations are approximates only.

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",
        "completeDateTime": "2024-08-20T05:15:07.698377551Z",
        "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.

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

"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": "f2142150-ff04-4d02-9bc3-3acf7c510ced",
        "name": "Matching skill levels with visits example",
        "submitDateTime": "2024-08-20T05:07:30.782484459Z",
        "startDateTime": "2024-08-20T05:07:36.233619326Z",
        "completeDateTime": "2024-08-20T05:08:37.165206321Z",
        "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 efficient than others and regularly complete visits quicker than other technicians.

skill multiplier

This can be expressed in the dataset by adding multiplier to the skills definition. multiplier is a float number, for instance, 0.8. The visit serviceDuration is multiplied by the multiplier to determine the effectiveServiceDuration of the visit.

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.
"skills": [
  {
    "name": "plumber",
    "multiplier": 0.8
  }
  • 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.8
              }
            ]
          }
        ]
      },
      {
        "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",
        "completeDateTime": "2024-08-20T07:43:37.791116599Z",
        "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:41:57Z",
                                "effectiveServiceDuration": "PT1H12M",
                                "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:11: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 1 hour and 12 minutes, 18 minutes less than the original 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.

Next