Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Vehicle resource constraints
  • Technician coverage area

Field Service Routing

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with field service routing
    • Understanding the API
    • User guide
      • User guide
      • Terms
      • Constraints
      • Planning window
      • Model configuration
      • Configuration overrides
      • Time zones and daylight-saving time (DST)
      • Routing with Timefold’s maps service
      • Validation
      • Model response
      • Key performance indicators (KPIs)
    • Vehicle resource constraints
      • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Route optimization
      • Technician coverage area
      • Technician costs
      • Technician ratings
    • Visit service constraints
      • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Visit requirements
      • Multi-vehicle visits
      • Movable visits and multi-day schedules
      • Priority visits and optional visits
      • Visit service level agreement (SLA)
    • Recommendations
      • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
    • 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
    • Scenarios
      • Long-running visits
    • Changelog
    • Upgrade to the latest version
    • Feature requests

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:

  • Area coverage with polygons

  • Area coverage defined by multiple polygons

Prerequisites

Learn how to configure an API Key to run the examples in this guide:
  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. 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.

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