Technician costs
Technicians can have different wages and overtime rates. When assigning a visit, Timefold minimizes the total cost of all vehicle shifts in the plan.
It is important to realize that optimizing for cost may compete with other optimization goals, such as minimizing travel time or fairness.
This guide explains how to manage shift cost with the following examples:
Prerequisite
To run the examples in this guide, you need to authenticate with a valid API key for this model:
-
Log in to Timefold Platform: app.timefold.ai
-
From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, then choose API Keys.
-
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. Shift cost and rates
A vehicle shift’s cost
property includes fixedCost
and the list of rates
which are useful for expressing different rates for different shift durations:
fixedCost
is the cost of having a vehicle available for a shift, even if the vehicle has no visits assigned during the shift.
The fixed cost is independent on the actual time worked.
Typically used for technicians who are full-time employees and have an employee type contract.
rates
is a list of different cost rates (or tariffs) applied to the vehicle shift in the order they are defined.
Every rate:
-
Starts from the end of the previous rate, or the start of the vehicle shift if there is no previous rate.
-
Has a
duration
defining the time period this rate applies to. -
Has an optional
activationCost
paid once when this rate has started. -
Defines a cost per time unit (
HOUR
orMINUTE
) that is applied to every started unit within this rate’s duration.HOUR
is the default. The unit also determines the granularity of cost calculation, the rate is paid for every unit that is started. For instance, in case ofHOUR
, every started hour is fully paid.
For example, to define a standard 8-hour cost as 10.0 per hour and an overtime rate for the next 4 hours which cost as twice as much, use the following rates
list:
[
{ "duration": "PT8H", "costPerUnit": 10.0, "unit": "HOUR" },
{ "duration": "PT4H", "costPerUnit": 20.0, "unit": "HOUR" }
]
The example above defines two rates (let’s call them rate1
and rate2
) which start and end as follows (assuming the shift starts at 08:00
):
-
rate1.start = 08:00
(shift start),rate1.end = 16:00
(rate1.start + rate1.duration
) -
rate2.start = 16:00
(rate1.end
),rate2.end = 20:00
(rate2.start + rate2.duration
)
If the vehicle shift duration exceeds the last defined rate’s duration, this last rate is used for the remaining shift duration as well.
Assuming a shift using the rates from the example above ends at 22:00
, rate2
is used for cost calculation of the shift time after 16:00
until 22:00
.
2. An employee scenario
A technician with an employee
type of contract usually has a fixed cost contract with a potentially higher rate for overtime.
The fixed cost is applied regardless of if the technician has been assigned any visits.
A vehicle shift’s cost
property includes fixedCost
and the list of rates
which are useful for expressing a different rate for overtime:
{
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"cost": {
"fixedCost": 80.0,
"rates": [
{
"duration": "PT8H",
"costPerUnit": 0.0
},
{
"duration": "PT4H",
"costPerUnit": 20.0,
"unit": "HOUR"
}
]
}
}
]
}
]
}
fixedCost
defines that the cost for this vehicle shift will always include 80.0
(regardless of if this shift is assigned any visits).
rates
defines two rates:
-
For the first 8 hours of the shift, the cost is zero (as the employee’s
fixedCost
covers the standard working hours). The combination of this first rate and thefixedCost
could be viewed as a10.0
per hour pre-paid rate for the first 8 hours of the shift regardless of the actual shift duration. -
For the next 4 hours (or longer), the cost is
20.0
per hour (twice the standard rate expressed byfixedCost
).
3. A contractor scenario
A technician with a contractor
type of contract is usually paid per hour with a potential fixed "activation" cost.
The activation cost is applied only if the technician has been assigned at least one visit.
A vehicle shift’s cost
property lists the rates
which are useful for expressing different contractor rates:
{
"vehicles": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"cost": {
"rates": [
{
"duration": "PT4H",
"activationCost": 40.0,
"costPerUnit": 0.0
},
{
"duration": "PT4H",
"costPerUnit": 20.0,
"unit": "HOUR"
}
]
}
}
]
}
]
}
In the above example, rates
defines two rates:
-
The first 4 hours of the shift are completely covered by the activation cost
40.0
. The activation cost is paid only if the shift has started (at least one visit assigned). -
For the next 4 hours (or longer), the cost is 20.0 per hour.
4. Optimizing cost vs. travel time
In the following example, Carl and Beth have shifts. Beth’s start location is very close to the visits that need to be assigned and both Beth and Carl have the same shift fixed costs.
Timefold assigns the visits to Beth because of the shorter total travel time.
-
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": "Equal shift cost example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"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": 10.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": 10.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": "2d614ade-a815-4aaa-a1e4-5c5af36cec5f",
"name": "Equal shift cost example",
"submitDateTime": "2025-01-20T15:42:21.546782222+01:00",
"startDateTime": "2025-01-20T15:42:21.550721323+01:00",
"activeDateTime": "2025-01-20T15:42:21.556205034+01:00",
"completeDateTime": "2025-01-20T15:42:26.831217635+01:00",
"shutdownDateTime": "2025-01-20T15:42:26.836983017+01:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-142647soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:45:59Z",
"startServiceTime": "2027-02-01T09:45:59Z",
"departureTime": "2027-02-01T11:45:59Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT45M59S",
"travelDistanceMetersFromPreviousStandstill": 38320,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T12:31:58Z",
"startServiceTime": "2027-02-01T12:31:58Z",
"departureTime": "2027-02-01T14:31:58Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT45M59S",
"travelDistanceMetersFromPreviousStandstill": 38320,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H31M58S",
"travelTimeFromStartLocationToFirstVisit": "PT45M59S",
"travelTimeBetweenVisits": "PT45M59S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 76640,
"travelDistanceFromStartLocationToFirstVisitMeters": 38320,
"travelDistanceBetweenVisitsMeters": 38320,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": "2027-02-01T14:31:58Z",
"technicianCosts": 10
}
}
]
},
{
"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,
"technicianCosts": 10
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT1H31M58S",
"travelTimeFromStartLocationToFirstVisit": "PT45M59S",
"travelTimeBetweenVisits": "PT45M59S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 76640,
"travelDistanceFromStartLocationToFirstVisitMeters": 38320,
"travelDistanceBetweenVisitsMeters": 38320,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"totalUnassignedVisits": 0,
"workingTimeFairnessPercentage": 0.0,
"totalTechnicianCosts": 20
}
}
modelOutput
contains the visits assigned to Carl’s shift itinerary and the KPI totalTechnicianCosts
equal to 20
as the sum of the costs for Beth’s and Carl’s shifts (Carl’s costs are fixed so he is paid even when he is not assigned any visits).
In the following example, Carl and Beth have shifts. Beth’s start location is very close to the visits that need to be assigned. However, Beth’s shift cost is twice as much as Carl’s.
Timefold assigns the visits to Carl because the high cost of assigning Beth outweighs the advantage of the shorter total travel time had the visit been assigned to Beth.
-
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": "Different shift cost example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.77301, -84.43838],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z",
"cost": {
"rates": [
{
"duration": "PT8H",
"activationCost": 20.0,
"costPerUnit": 0.0
},
{
"duration": "PT4H",
"costPerUnit": 40.0,
"unit": "HOUR"
}
]
}
}
]
},
{
"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": {
"rates": [
{
"duration": "PT8H",
"activationCost": 10.0,
"costPerUnit": 0.0
},
{
"duration": "PT4H",
"costPerUnit": 20.0,
"unit": "HOUR"
}
]
}
}
]
}
],
"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": "38c65824-7351-4305-9643-740b51c7622a",
"name": "Different shift cost example",
"submitDateTime": "2025-01-20T15:40:05.963077955+01:00",
"startDateTime": "2025-01-20T15:40:05.971077375+01:00",
"activeDateTime": "2025-01-20T15:40:05.977300766+01:00",
"completeDateTime": "2025-01-20T15:40:12.178949601+01:00",
"shutdownDateTime": "2025-01-20T15:40:12.182930452+01:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-146021soft",
"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": 0
}
}
]
},
{
"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": 10
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT1H35M44S",
"travelTimeFromStartLocationToFirstVisit": "PT19M25S",
"travelTimeBetweenVisits": "PT45M59S",
"travelTimeFromLastVisitToEndLocation": "PT30M20S",
"totalTravelDistanceMeters": 79782,
"travelDistanceFromStartLocationToFirstVisitMeters": 16179,
"travelDistanceBetweenVisitsMeters": 38320,
"travelDistanceFromLastVisitToEndLocationMeters": 25283,
"totalUnassignedVisits": 0,
"workingTimeFairnessPercentage": 0.0,
"totalTechnicianCosts": 10
}
}
modelOutput
contains the visits assigned to Carl’s shift itinerary and the KPI totalTechnicianCosts
equal to 10
for Carl’s shift.
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.
-
Read Skills to learn how to avoid scheduling overqualified technicians for a job.