Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Visit service constraints
  • Movable visits and multi-day schedules

Field Service Routing

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with field service routing
    • Understanding the API
    • User guide
      • Constraints
      • Time zones and daylight-saving time (DST)
      • Routing with Timefold’s maps service
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Route optimization
      • Technician costs
      • Technician ratings
    • 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
    • Reference guide

Movable visits and multi-day schedules

Visits in field service routing often have availability time windows that specify when the visit can occur.

Visits can have a single or multiple time windows, for instance:

  • A single time window on a single day: 09:00 to 17:00.

  • Multiple time windows on a single day: 09:00 to 11:00 and 13:00 to 17:00.

  • A single time window that spans multiple days: February 1st 2027 at 09:00 to February 2nd 2027 at 17:00.

  • Multiple time windows on multiple days: February 1st 2027 at 09:00 to 17:00 and February 2nd 2027 at 09:00 to 17:00.

When a visit has a time window or time windows that are on a single day, they are considered non-movable. The visit cannot be scheduled on another day.

When a visit has a time window or time windows that span multiple days they are considered movable. The visit can be scheduled on different days.

Movable visits can be scheduled at any time during their time window, however, it is often preferable to schedule them as close to the beginning of their time window as possible to avoid running out of options later in the time window.

Additionally, it can be important to spread movable visits out so that schedules retain some flexibility to schedule new non-movable visits and respond to situations that require Real-time planning.

This guide explains how to manage movable visits and multi-day schedules with the following examples:

  • Schedule movable visits to the earliest day

  • Balance movable and non-movable visits

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. Schedule movable visits to the earliest day

In the following example, Visit A is a movable visit with a time window that spans 3 days. Visit B is not a movable visit as it has a time window that spans a single day.

{
  "visits": [
    {
      "id": "Visit A",
      "location": [33.77301, -84.43838],
      "serviceDuration": "PT2H",
      "timeWindows": [
        {
          "minStartTime": "2027-02-01T09:00:00Z",
          "maxEndTime": "2027-02-03T17:00:00Z"
        }
      ]
    },
    {
      "id": "Visit B",
      "location": [33.76415, -84.36136],
      "serviceDuration": "PT2H",
      "timeWindows": [
        {
          "minStartTime": "2027-02-01T09:00:00Z",
          "maxEndTime": "2027-02-01T17:00:00Z"
        }
      ]
    }
  ]
}

Carl is the only available technician in the area where these visits are located.

{
  "id": "Carl",
  "shifts": [
    {
      "id": "Carl-2027-02-01",
      "startLocation": [33.74135, -84.36340],
      "minStartTime": "2027-02-01T09:00:00Z",
      "maxEndTime": "2027-02-01T13:00:00Z"
    },
    {
      "id": "Carl-2027-02-02",
      "startLocation": [33.74135, -84.36340],
      "minStartTime": "2027-02-02T09:00:00Z",
      "maxEndTime": "2027-02-02T17:00:00Z"
    },
    {
      "id": "Carl-2027-02-03",
      "startLocation": [33.74135, -84.36340],
      "minStartTime": "2027-02-03T09:00:00Z",
      "maxEndTime": "2027-02-03T17:00:00Z"
    }
  ]
}

Carl is available between 09:00 and 13:00 on Monday and between 09:00 and 17:00 on Tuesday and Wednesday.

The Prefer visits scheduled to the earliest day soft constraint adds a penalty for every day after a movable visits minStartTime, incentivizing Timefold to schedule the visit as early as possible.

Visit B, which has a non-movable time window on Monday the 1st, is scheduled on Monday.

Visit A, which has a movable time window that spans Monday, Tuesday, and Wednesday, is scheduled on Tuesday.

There was not enough time to schedule Visit A on Monday. Tuesday was the earliest Visit A could be scheduled.

prefer visits scheduled to the earliest day
  • 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": "Prefer visits scheduled to the earliest day example"
    }
  },
  "modelInput": {
    "vehicles": [
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startLocation": [33.74135, -84.36340],
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T13:00:00Z"
          },
          {
            "id": "Carl-2027-02-02",
            "startLocation": [33.74135, -84.36340],
            "minStartTime": "2027-02-02T09:00:00Z",
            "maxEndTime": "2027-02-02T17:00:00Z"
          },
          {
            "id": "Carl-2027-02-03",
            "startLocation": [33.74135, -84.36340],
            "minStartTime": "2027-02-03T09:00:00Z",
            "maxEndTime": "2027-02-03T17:00:00Z"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit A",
        "location": [33.77301, -84.43838],
        "serviceDuration": "PT2H",
        "timeWindows": [
          {
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-03T17:00:00Z"
          }
        ]
      },
      {
        "id": "Visit B",
        "location": [33.76415, -84.36136],
        "serviceDuration": "PT2H",
        "timeWindows": [
          {
            "minStartTime": "2027-02-01T09:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z"
          }
        ]
      }
    ]
  }
}
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",
    "parentId": null,
    "originId": "OriginID",
    "name": "Prefer visits scheduled to the earliest day example",
    "submitDateTime": "2025-07-10T07:11:27.419183993Z",
    "startDateTime": "2025-07-10T07:11:40.19852569Z",
    "activeDateTime": "2025-07-10T07:12:05.932533716Z",
    "completeDateTime": "2025-07-10T07:12:36.223987233Z",
    "shutdownDateTime": "2025-07-10T07:12:36.587864829Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-8013soft",
    "tags": [
      "system.type:from-request",
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Carl",
        "shifts": [
          {
            "id": "Carl-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit B",
                "kind": "VISIT",
                "arrivalTime": "2027-02-01T09:18:39Z",
                "startServiceTime": "2027-02-01T09:18:39Z",
                "departureTime": "2027-02-01T11:18:39Z",
                "effectiveServiceDuration": "PT2H",
                "travelTimeFromPreviousStandstill": "PT18M39S",
                "travelDistanceMetersFromPreviousStandstill": 3945,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT37M22S",
              "travelTimeFromStartLocationToFirstVisit": "PT18M39S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT18M43S",
              "totalTravelDistanceMeters": 7908,
              "travelDistanceFromStartLocationToFirstVisitMeters": 3945,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 3963,
              "endLocationArrivalTime": "2027-02-01T11:37:22Z"
            }
          },
          {
            "id": "Carl-2027-02-02",
            "startTime": "2027-02-02T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit A",
                "kind": "VISIT",
                "arrivalTime": "2027-02-02T09:39:23Z",
                "startServiceTime": "2027-02-02T09:39:23Z",
                "departureTime": "2027-02-02T11:39:23Z",
                "effectiveServiceDuration": "PT2H",
                "travelTimeFromPreviousStandstill": "PT39M23S",
                "travelDistanceMetersFromPreviousStandstill": 9148,
                "minStartTravelTime": "2027-02-01T00:00:00Z"
              }
            ],
            "metrics": {
              "totalTravelTime": "PT1H19M31S",
              "travelTimeFromStartLocationToFirstVisit": "PT39M23S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT40M8S",
              "totalTravelDistanceMeters": 18522,
              "travelDistanceFromStartLocationToFirstVisitMeters": 9148,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 9374,
              "endLocationArrivalTime": "2027-02-02T12:19:31Z"
            }
          },
          {
            "id": "Carl-2027-02-03",
            "startTime": "2027-02-03T09:00:00Z",
            "itinerary": [],
            "metrics": {
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0
            }
          }
        ]
      }
    ]
  },
  "inputMetrics": {
    "visits": 2,
    "visitGroups": 0,
    "vehicles": 1,
    "mandatoryVisits": 2,
    "optionalVisits": 0,
    "vehicleShifts": 3,
    "visitsWithSla": 0
  },
  "kpis": {
    "totalTravelTime": "PT1H56M53S",
    "travelTimeFromStartLocationToFirstVisit": "PT58M2S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT58M51S",
    "totalTravelDistanceMeters": 26430,
    "travelDistanceFromStartLocationToFirstVisitMeters": 13093,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 13337,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 2,
    "assignedMandatoryVisits": 2,
    "assignedOptionalVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 100.0,
    "averageTechnicianRating": 0.0
  }
}

modelOutput contains the itinerary with Visit A and Visit B scheduled to the earliest possible times within their time windows.

inputMetrics provides a breakdown of the inputs in the input dataset.

KPIs provides the KPIs for the output including:

{
 "totalTravelTime": "PT1H56M53S",
  "travelTimeFromStartLocationToFirstVisit": "PT58M2S",
  "travelTimeFromLastVisitToEndLocation": "PT58M51S",
  "totalTravelDistanceMeters": 26430,
  "travelDistanceFromStartLocationToFirstVisitMeters": 13093,
  "travelDistanceFromLastVisitToEndLocationMeters": 13337,
  "totalAssignedVisits": 2,
  "assignedMandatoryVisits": 2,
  "totalActivatedVehicles": 1,
  "workingTimeFairnessPercentage": 100.0
}

2. Balance movable and non-movable visits

Vehicle shifts can set a ratio to control the percentage of the vehicle shift that can be assigned movable visits.

{
  "id": "Carl-2027-02-02",
  "startLocation": [33.74135, -84.36340],
  "minStartTime": "2027-02-02T09:00:00Z",
  "maxEndTime": "2027-02-02T17:00:00Z",
  "movableOccupationRatioThreshold": 0.5
}

movableOccupationRatioThreshold sets the ratio to control the percentage of the vehicle shift that can be assigned movable visits.

The value must be a double. The minimum value is 0.0, and the maximum value is 1.0.

When the value is 0.0, zero percent of the shift can be used for movable visits.

When the value is 0.5, 50 percent of the shift can be used for movable visits.

When the value is 1.0, 100 percent of the shift can be used for movable visits.

Breaks are deducted from the shift so that only the effective shift duration is considered.

With an 8-hour shift that includes a 1-hour lunch break, the effective shift duration is 7 hours.

A movableOccupationRatioThreshold of 0.5 on such a shift would result in 3.5 hours being available for movable visits (and the travel time to those visits).

If the vehicle shift occupancy grows above the threshold, the Balance movable and non-movable visits constraint penalizes the solution score for the exceeding travel and service duration of movable visits assigned to this vehicle shift.

Next

  • See the full API spec or try the online API.

  • Learn more about field service routing from our YouTube playlist.

  • Learn more about Time windows and opening hours.

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