Fairness
Having balanced workloads across technicians is an important requirement in field service routing.
Technicians like to know they are receiving a fair share of the workload, and that one technician isn’t unfairly being assigned more or less work than other technicians.
Determining how the balanced workload should be achieved can be a complex task. The fairness computation makes use of the standard Timefold approach described in the solver documentation.
The fairness of a solution for the field service routing model is based on the assigned workload to a technician, the availability of a technician and their historical data if it is provided. The assigned workload includes the time from when a technician leaves their starting location and arrives back at their end location. All travel times, service times, wait times, and break times are included.
This guide describes how to implement fairness with the field service routing model.
1. Without historical data
In the following example, we have two technicians and only one visit. No historical data has been provided, and both technicians start from the same location (the depot) so travel distance isn’t a factor in deciding which technician to assign.
The visit is assigned to Ann, and the solution has a fairness score of 0%.
-
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": "Fairness basic 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"
}
]
},
{
"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"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT1H"
}
]
}
}
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": "Fairness basic example",
"submitDateTime": "2025-01-03T07:03:24.386813005Z",
"startDateTime": "2025-01-03T07:03:31.244090637Z",
"activeDateTime": "2025-01-03T07:03:31.531334782Z",
"completeDateTime": "2025-01-03T07:03:32.955960544Z",
"shutdownDateTime": "2025-01-03T07:03:33.21652227Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-69221soft",
"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:56Z",
"startServiceTime": "2027-02-01T09:29:56Z",
"departureTime": "2027-02-01T10:29:56Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT29M56S",
"travelDistanceMetersFromPreviousStandstill": 31493,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT59M28S",
"travelTimeFromStartLocationToFirstVisit": "PT29M56S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 65477,
"travelDistanceFromStartLocationToFirstVisitMeters": 31493,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T10:59:28Z"
}
}
]
},
{
"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
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT59M28S",
"travelTimeFromStartLocationToFirstVisit": "PT29M56S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 65477,
"travelDistanceFromStartLocationToFirstVisitMeters": 31493,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 0,
"workingTimeFairnessPercentage": 0.0
}
}
modelOutput
contains the itineraries for Ann and Beth. Ann was assigned the visit.
2. With historical data
Determining the time window, in which the workload should be balanced is the first step in achieving a fair solution. By default, the complete planning window is considered.
Historical data that includes both the technician’s hours previously worked and their historical availability can also be considered as part of the solution. This helps ensure fairness to technicians not just in the current planning window, but over an extended period of time.
It’s important to provide historical data for all employees to avoid skewing the results of the solution. When an employee has no historical data, for instance a new employee, you can provide an average from the other employees for the new employee to maintain the fairness of the solution. |
Historical data is added to Vehicles
in the dataset:
{
"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"
}
],
"historicalTimeUtilized": "PT2280M",
"historicalTimeCapacity": "PT2400M"
},
{
"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"
}
],
"historicalTimeUtilized": "PT1000M",
"historicalTimeCapacity": "PT2400M"
}
]
}
-
historicalTimeUtilized
is the amount of time the technician worked in the time period being considered. -
historicalTimeCapacity
is the technician’s availability in the time period being considered.
The example above shows that Ann and Beth both had an availability of 2400 minutes in the previous time window.
Ann’s historicalTimeUtilized
is 2280 minutes, and Beth’s historicalTimeUtilized
is 1000 minutes.
Ann worked 1280 more minutes than Beth.
When adding historicalTimeUtilized and historicalTimeCapacity , it’s important to use data from the same time period for all employees, otherwise the fairness calculations will be incorrect.
If they are not provided with the input a default duration of 0 will be used for both fields.
|
With the historical data included that shows Ann has worked more hours than Beth, this time Beth (not Ann) is assigned the visit, and the solution has a fairness score of 65.86%.
-
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": "Fairness with historical data 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"
}
],
"historicalTimeUtilized": "PT2280M",
"historicalTimeCapacity": "PT2400M"
},
{
"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"
}
],
"historicalTimeUtilized": "PT1000M",
"historicalTimeCapacity": "PT2400M"
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT1H"
}
]
}
}
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": "Fairness with historical data example",
"submitDateTime": "2025-01-03T07:23:34.778693353Z",
"startDateTime": "2025-01-03T07:23:41.729137453Z",
"activeDateTime": "2025-01-03T07:23:42.051974217Z",
"completeDateTime": "2025-01-03T07:28:42.126650472Z",
"shutdownDateTime": "2025-01-03T07:28:42.381767231Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-69330soft",
"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:29:56Z",
"startServiceTime": "2027-02-01T09:29:56Z",
"departureTime": "2027-02-01T10:29:56Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT29M56S",
"travelDistanceMetersFromPreviousStandstill": 31493,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT59M28S",
"travelTimeFromStartLocationToFirstVisit": "PT29M56S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 65477,
"travelDistanceFromStartLocationToFirstVisitMeters": 31493,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"endLocationArrivalTime": "2027-02-01T10:59:28Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT59M28S",
"travelTimeFromStartLocationToFirstVisit": "PT29M56S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT29M32S",
"totalTravelDistanceMeters": 65477,
"travelDistanceFromStartLocationToFirstVisitMeters": 31493,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 33984,
"totalUnassignedVisits": 0,
"workingTimeFairnessPercentage": 65.86
}
}
modelOutput
contains the itineraries for Ann and Beth. Beth was assigned the visit.
3. Working time fairness KPI
The working time fairness KPI is a measure of how well the workload is balanced across all technicians.
It is represented as a percentage, where 100%
means the workload is perfectly balanced and 0%
means the workload is completely unbalanced.
In the first example, Ann was assigned one visit, and Beth was not assigned any visits, the working time fairness percentage was 0%. This was an unfair solution.
In the second example, with historical data included, Beth was assign the visit and Ann was not assigned any visits, however, because the historical data included previous hours worked and available, the working time fairness percentage was 65.86%. This was a much fairer solution.
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.