Technician coverage area
Technicians can cover different areas during their shifts. For instance, a technician may only be able to cover a specific city or region.
In Timefold, a technician’s coverage area can be defined using either a single polygon or multiple polygons.
The area definition uses a subset of the GeoJSON format, namely Polygon
and MultiPolygon
geometry types.
This guide explains how to define technician coverage areas with the following examples:
Prerequisites
Learn how to configure an API Key to run the examples in this guide:
-
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. Area coverage with polygons
A vehicle’s requiredArea
property can be used to define the area that a technician can cover during their shift.
If the property is not set, the technician can cover any visit location.
The following example shows a technician with a coverage area defined by a single polygon:
{
"requiredArea": {
"type": "Polygon",
"coordinates": [
[
[ -85.0, 34.0 ],
[ -84.2, 34.0 ],
[ -84.2, 33.0 ],
[ -85.0, 33.0 ],
[ -85.0, 34.0 ]
]
]
}
}
-
The
coordinates
array must contain at least one array of coordinates describing the polygon’s outer boundary. -
Any additional arrays of coordinates would describe holes in the polygon (not used in this example).
-
Coordinates are expressed as
[longitude, latitude]
pairs.Compatibility with the GeoJSON specification means that the order of coordinates is different from the common [latitude, longitude]
format used when defining locations throughout the rest of the model input. -
Each array of coordinates must contain at least four coordinate pairs.
-
The first and the last points in the
coordinates
array must be the same to close the polygon.
Please see GeoJSON specification for more details about the Polygon
type.
In the following example, Carl and Beth have shifts. Both Beth and Carl have their coverage area defined which covers visits A and B, but not visit C.
Timefold assigns the visits A and B and leaves C unassigned.
-
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 area coverage - Polygon example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"requiredArea": {
"type": "Polygon",
"coordinates": [
[
[ -85.0, 34.0 ],
[ -84.2, 34.0 ],
[ -84.2, 33.0 ],
[ -85.0, 33.0 ],
[ -85.0, 34.0 ]
]
]
},
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.77301, -84.43838],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Carl",
"requiredArea": {
"type": "Polygon",
"coordinates": [
[
[ -84.2, 34.0 ],
[ -83.0, 34.0 ],
[ -83.0, 33.0 ],
[ -84.2, 33.0 ],
[ -84.2, 34.0 ]
]
]
},
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T13:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H"
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H"
},
{
"id": "Visit C",
"location": [33.74699, -82.02504],
"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",
"originId": "ID",
"name": "Technician area coverage - Polygon example",
"submitDateTime": "2025-08-26T14:28:14.283386221+02:00",
"startDateTime": "2025-08-26T14:28:14.293630972+02:00",
"activeDateTime": "2025-08-26T14:28:14.354997846+02:00",
"completeDateTime": "2025-08-26T14:28:44.387274353+02:00",
"shutdownDateTime": "2025-08-26T14:28:44.415800066+02:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-10000medium/-45536soft",
"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:00:00Z",
"startServiceTime": "2027-02-01T09:00:00Z",
"departureTime": "2027-02-01T11:00:00Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT0S",
"travelDistanceMetersFromPreviousStandstill": 0,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": "2027-02-01T11:00:00Z"
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T13:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:21:37Z",
"startServiceTime": "2027-02-01T13:21:37Z",
"departureTime": "2027-02-01T15:21:37Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT21M37S",
"travelDistanceMetersFromPreviousStandstill": 21413,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT43M17S",
"travelTimeFromStartLocationToFirstVisit": "PT21M37S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT21M40S",
"totalTravelDistanceMeters": 42635,
"travelDistanceFromStartLocationToFirstVisitMeters": 21413,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 21222,
"endLocationArrivalTime": "2027-02-01T15:43:17Z"
}
}
]
}
],
"unassignedVisits": [
"Visit C"
]
},
"inputMetrics": {
"visits": 3,
"visitGroups": 0,
"vehicles": 2,
"mandatoryVisits": 3,
"optionalVisits": 0,
"vehicleShifts": 2,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT43M17S",
"travelTimeFromStartLocationToFirstVisit": "PT21M37S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT21M40S",
"totalTravelDistanceMeters": 42635,
"travelDistanceFromStartLocationToFirstVisitMeters": 21413,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 21222,
"totalUnassignedVisits": 1,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 2,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 2,
"workingTimeFairnessPercentage": 53.74
}
}
2. Area coverage defined by multiple polygons
The following example shows a technician with a coverage area defined by a multiple polygons, or a MultiPolygon
:
{
"requiredArea": {
"type": "MultiPolygon",
"coordinates": [
[
[
[ -85.0, 34.0 ],
[ -84.2, 34.0 ],
[ -84.2, 33.0 ],
[ -85.0, 33.0 ],
[ -85.0, 34.0 ]
]
],
[
[
[ -82.0, 34.0 ],
[ -83.0, 34.0 ],
[ -83.0, 33.0 ],
[ -82.0, 33.0 ],
[ -82.0, 34.0 ]
]
]
]
}
}
-
The
coordinates
array contains multiple arrays, each representing a polygon. -
Each of the polygon arrays must follow the same rules as described in the Area coverage with polygons section above.
-
Coordinates are expressed as
[longitude, latitude]
pairs.Compatibility with the GeoJSON specification means that the order of coordinates is different from the common [latitude, longitude]
format used when defining locations throughout the rest of the model input.
Please see GeoJSON specification for more details about the MultiPolygon
type.
In the following example, Carl and Beth have shifts. Carl’s area covers visit B. Beth’s area is defined using multiple polygons, the first of them covering visit A and the second polygon covering visit C.
Timefold assigns all visits A, B and C because they are located within a technician’s coverage area.
-
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 area coverage - MultiPolygon example"
}
},
"modelInput": {
"vehicles": [
{
"id": "Beth",
"requiredArea": {
"type": "MultiPolygon",
"coordinates": [
[
[
[ -85.0, 34.0 ],
[ -84.2, 34.0 ],
[ -84.2, 33.0 ],
[ -85.0, 33.0 ],
[ -85.0, 34.0 ]
]
],
[
[
[ -82.0, 34.0 ],
[ -83.0, 34.0 ],
[ -83.0, 33.0 ],
[ -82.0, 33.0 ],
[ -82.0, 34.0 ]
]
]
]
},
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.77301, -84.43838],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Carl",
"requiredArea": {
"type": "Polygon",
"coordinates": [
[
[ -84.2, 34.0 ],
[ -83.0, 34.0 ],
[ -83.0, 33.0 ],
[ -84.2, 33.0 ],
[ -84.2, 34.0 ]
]
]
},
"shifts": [
{
"id": "Carl-2027-02-01",
"startLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T13:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H"
},
{
"id": "Visit B",
"location": [33.74699, -84.02504],
"serviceDuration": "PT2H"
},
{
"id": "Visit C",
"location": [33.74699, -82.02504],
"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",
"originId": "ID",
"name": "Technician area coverage - MultiPolygon example",
"submitDateTime": "2025-08-26T14:36:16.147678868+02:00",
"startDateTime": "2025-08-26T14:36:16.152680762+02:00",
"activeDateTime": "2025-08-26T14:36:16.174221454+02:00",
"completeDateTime": "2025-08-26T14:36:46.191592255+02:00",
"shutdownDateTime": "2025-08-26T14:36:46.228568528+02:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-45448soft",
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:00:00Z",
"startServiceTime": "2027-02-01T09:00:00Z",
"departureTime": "2027-02-01T10:00:00Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT0S",
"travelDistanceMetersFromPreviousStandstill": 0,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T10:00:00Z",
"startServiceTime": "2027-02-01T10:00:00Z",
"departureTime": "2027-02-01T12:00:00Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT0S",
"travelDistanceMetersFromPreviousStandstill": 0,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": "2027-02-01T12:00:00Z"
}
}
]
},
{
"id": "Carl",
"shifts": [
{
"id": "Carl-2027-02-01",
"startTime": "2027-02-01T13:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:21:37Z",
"startServiceTime": "2027-02-01T13:21:37Z",
"departureTime": "2027-02-01T15:21:37Z",
"effectiveServiceDuration": "PT2H",
"travelTimeFromPreviousStandstill": "PT21M37S",
"travelDistanceMetersFromPreviousStandstill": 21413,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT43M17S",
"travelTimeFromStartLocationToFirstVisit": "PT21M37S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT21M40S",
"totalTravelDistanceMeters": 42635,
"travelDistanceFromStartLocationToFirstVisitMeters": 21413,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 21222,
"endLocationArrivalTime": "2027-02-01T15:43:17Z"
}
}
]
}
],
"unassignedVisits": []
},
"inputMetrics": {
"visits": 3,
"visitGroups": 0,
"vehicles": 2,
"mandatoryVisits": 3,
"optionalVisits": 0,
"vehicleShifts": 2,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT43M17S",
"travelTimeFromStartLocationToFirstVisit": "PT21M37S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT21M40S",
"totalTravelDistanceMeters": 42635,
"travelDistanceFromStartLocationToFirstVisitMeters": 21413,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 21222,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 3,
"assignedMandatoryVisits": 3,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 2,
"workingTimeFairnessPercentage": 71.06
}
}
Next
-
See the full API spec or try the online API.
-
Learn more about field service routing from our YouTube playlist.
-
Read Skills to learn how to avoid scheduling overqualified technicians for a job.