Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
    • Pick-up and Delivery Routing
    • Task Scheduling
  • Platform
Try models
  • Field Service Routing
  • Real-time planning
  • Real-time planning: minimizing disruption on replanning

Field Service Routing

    • Introduction
    • Getting started: Hello world
    • User guide
      • Terminology
      • Use case guide
      • Scheduling API concepts
      • Integration
      • Constraints
      • Using the API
        • Using the OpenAPI spec
        • API tooling
      • Demo datasets
      • Input datasets
        • Model configuration
        • Model input
        • Planning window
        • Time zones and daylight-saving time (DST)
      • Routing with Timefold’s maps service
      • Input validation
      • Model response
      • Output datasets
        • Metadata
        • Model output
        • Input metrics
        • Key performance indicators (KPIs)
      • Key performance indicators (KPIs)
      • Metrics and optimization goals
      • Score analysis
      • Visualizations
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Route optimization
      • Technician costs
      • Technician ratings
      • Coverage area
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Multi-vehicle visits
      • Multi-day schedules and movable visits
      • Priority visits and optional visits
      • Visit service level agreement (SLA)
      • Duration added for first visit on location
      • Visit profit
      • Visit requirements and tags
        • Visit requirements
        • Tags
    • Manual intervention
    • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
      • Bulk time window recommendations
    • Real-time planning
      • Real-time planning: extended visit
      • Real-time planning: reassignment
      • Real-time planning: emergency visit
      • Real-time planning: focused replanning
      • Real-time planning: no show
      • Real-time planning: technician ill
      • Real-time planning: pinning visits
      • Real-time planning: minimizing disruption on replanning
      • Real-time planning: actual arrival and departure times
    • Scenarios
      • Configuring labor law compliance
      • Ferry connections
      • Long-running visits
    • Changelog
    • Upgrade to the latest version
    • Feature requests

Real-time planning: minimizing disruption on replanning

Real-time planning can be disruptive to technician’s schedules.

Real-time planning: pinning visits solves this by hard-pinning specific visits or blocks of the schedule so that they cannot be touched at all. This is the right tool when a visit absolutely must not move, for instance because a technician has already started traveling to it.

However, pinning is an all-or-nothing tool. For every other already-planned visit that is not pinned, Timefold is completely free to move it to a different vehicle or a different time, even when doing so only brings a marginal improvement to the schedule. In practice, a small optimization gain is often not worth telling a technician (or a customer) that their already-communicated appointment has changed again.

The Minimize disruption on replanning soft constraint addresses this middle ground. Instead of hard-blocking changes, it makes Timefold reluctant to move a visit away from its previous assignment, in proportion to how disruptive that change actually is. A visit can still be moved or delayed when there is a good enough reason for it, for example, to keep the solution feasible or because the improvement clearly outweighs the disruption.

This guide explains the constraint with the following example:

  • 1. Original assignment and what counts as a disruption

1. Original assignment and what counts as a disruption

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 Manage tenant, 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.

For every visit that is already assigned when a solve starts, Timefold records its current vehicle shift, its position in that shift’s itinerary, and its startServiceTime. This is called the visit’s original assignment for that solve.

Visits that have no original assignment, for instance a brand new emergency visit that has never been planned before, are entirely unaffected by this constraint: there is nothing for them to be disrupted from.

Once solving has produced a new plan, the Minimize disruption on replanning constraint compares each visit’s new assignment against its original assignment:

  • A change of vehicle shift is penalized with a flat penalty of 100.

  • A change of vehicle is penalized with a flat penalty of 100. In the case of a visit that is moved to a different vehicle’s shift, both penalties apply, for a total of 200.

  • A change of startServiceTime is penalized in proportion to the difference, one point per second between the original and the new startServiceTime.

These penalties are combined and multiplied by minimizeDisruptionOnReplanningWeight.

Every soft constraint has a weight that can be configured to change the relative importance of the constraint compared to other constraints.

Learn about constraint weights.

When this constraint is violated, its constraint match justification includes the visit’s original vehicle shift ID, its original itinerary position, and its original startServiceTime, as well as the visit’s current vehicle shift ID, itinerary position and startServiceTime, so you can see exactly what changed.

1.1. Batch schedule

The original schedule was generated from the following input dataset during the regular batch scheduling. Visit E prefers Beth, but at this point Beth only has a short one-hour shift late in the day, which is too short to fit Visit E’s 90-minute service. The visit is assigned to Ann instead:

  • 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": "Original shift plan: minimize disruption 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.70474, -84.06508],
            "minStartTime": "2027-02-01T16:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit E",
        "location": [33.90719, -84.28149],
        "serviceDuration": "PT1H30M",
        "preferredVehicles": ["Beth"]
      }
    ]
  }
}
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>
{
  "metadata": {
    "id": "ID",
    "originId": "ID",
    "name": "Original shift plan: minimize disruption example",
    "submitDateTime": "2026-08-12T16:17:17.796942793+02:00",
    "startDateTime": "2026-08-12T16:17:17.959065606+02:00",
    "activeDateTime": "2026-08-12T16:17:17.959860319+02:00",
    "completeDateTime": "2026-08-12T16:17:48.025174188+02:00",
    "shutdownDateTime": "2026-08-12T16:17:48.025189344+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-3964soft",
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Ann",
        "shifts": [
          {
            "id": "Ann-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit E",
                "arrivalTime": "2027-02-01T09:31:10Z",
                "startServiceTime": "2027-02-01T09:31:10Z",
                "departureTime": "2027-02-01T11:01:10Z",
                "effectiveServiceDuration": "PT1H30M",
                "travelTimeFromPreviousStandstill": "PT31M10S",
                "travelDistanceMetersFromPreviousStandstill": 25971,
                "minStartTravelTime": "2027-02-01T00:00:00Z",
                "kind": "VISIT"
              }
            ],
            "metrics": {
              "totalServiceDuration": "PT1H30M",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT1H2M20S",
              "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT31M10S",
              "totalTravelDistanceMeters": 51942,
              "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 25971,
              "endLocationArrivalTime": "2027-02-01T11:32:20Z",
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 1,
          "assignedVisits": 1,
          "totalShiftDuration": "PT2H32M20S",
          "totalServiceDuration": "PT1H30M",
          "totalTravelTime": "PT1H2M20S",
          "totalTravelDistanceMeters": 51942,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      },
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startTime": "2027-02-01T16:00:00Z",
            "itinerary": [],
            "metrics": {
              "totalServiceDuration": "PT0S",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0,
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 0,
          "assignedVisits": 0,
          "totalShiftDuration": "PT0S",
          "totalServiceDuration": "PT0S",
          "totalTravelTime": "PT0S",
          "totalTravelDistanceMeters": 0,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      }
    ],
    "unassignedVisits": []
  },
  "inputMetrics": {
    "vehicles": 2,
    "vehicleShifts": 2,
    "visits": 1,
    "mandatoryVisits": 1,
    "optionalVisits": 0,
    "pinnedVisits": 0,
    "visitsWithSla": 0,
    "visitGroups": 0,
    "visitDependencies": 0,
    "excludedVisits": 0,
    "movableVisits": 0
  },
  "kpis": {
    "averageTravelTimePerVisit": "PT1H2M20S",
    "totalTravelTime": "PT1H2M20S",
    "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT31M10S",
    "averageTravelDistanceMetersPerVisit": 51942,
    "totalTravelDistanceMeters": 51942,
    "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 25971,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 1,
    "assignedMandatoryVisits": 1,
    "unassignedMandatoryVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 0.0,
    "totalOvertime": "PT0S",
    "availableOvertime": "PT0S"
  }
}

modelOutput contains Ann’s and Beth’s shift itineraries. Ann is assigned Visit E, with a startServiceTime of 09:31:10.

The exact arrivalTime, travel distances, and score in this guide’s example output depend on the map data used to solve it. Treat them as illustrative, and expect the actual values from your own environment to differ.

1.2. Real-time planning update: a new shift becomes available for the preferred vehicle

A few hours later, Beth’s afternoon frees up and she is available to work a longer shift, starting well before Visit E’s original startServiceTime and comfortably long enough to fit it:

{
  "id": "Beth-2027-02-01-B",
  "startLocation": [33.70474, -84.06508],
  "minStartTime": "2027-02-01T09:30:00Z",
  "maxEndTime": "2027-02-01T17:00:00Z"
}

Without minimizeDisruptionOnReplanningWeight set, Visit E’s preference for Beth would now win: Timefold would move Visit E from Ann’s shift onto Beth’s new shift, pulling its startServiceTime forward to whenever Beth can reach it, even though nothing about Visit E itself changed and the schedule that was already communicated to Ann and the customer would need to be revised for a marginal gain.

Activate the constraint by setting minimizeDisruptionOnReplanningWeight in config.model.overrides. This time, there is a preferred vehicle set for Visit E, so the penalty for minimizing disruptions needs to be higher than the preferred vehicle constraint’s penalty. Let’s set the weight to 100:

{
  "config": {
    "model": {
      "overrides": {
        "minimizeDisruptionOnReplanningWeight": 100
      }
    }
  }
}

Instead of resubmitting the full updated dataset, you can make this change with the Patch feature, which submits only the change as a JSON Patch document and generates a new revision of the plan. The Patch feature is currently available as a preview feature. If you’d like early access to it, please contact us.

Add the new shift for Beth:

  • Input

  • Patch

{
  "id": "Beth-2027-02-01-B",
  "startLocation": [33.70474, -84.06508],
  "minStartTime": "2027-02-01T09:30:00Z",
  "maxEndTime": "2027-02-01T17:00:00Z"
}
{
  "op": "add",
  "path": "/vehicles/[id=Beth]/shifts/-",
  "value": {
    "id": "Beth-2027-02-01-B",
    "startLocation": [
      33.70474,
      -84.06508
    ],
    "minStartTime": "2027-02-01T09:30:00Z",
    "maxEndTime": "2027-02-01T17:00:00Z"
  }
}

See /from-patch for more information about the /from-patch/ endpoint and the patch payload format.

Because minimizeDisruptionOnReplanningWeight is part of config, not modelInput, it is set directly alongside the patch, not as a patch operation:

{
  "config": {
    "model": {
      "overrides": {
        "minimizeDisruptionOnReplanningWeight": 100
      }
    }
  }
}

Because this request continues the existing dataset, Timefold automatically carries forward Ann’s and Beth’s previous assignments as the original assignment for this solve. Unlike pinning, there is no need to add itinerary or minStartTravelTime to Visit E yourself for this to work.

Submit the patch to generate a new revision of the plan.

  • Patch

  • Output

Try this example in Timefold Platform by saving this JSON into a file called patch.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/<ID>/from-patch [email protected]
{
  "config": {
    "run": {
      "name": "Real-time planning: minimize disruption"
    },
    "model": {
      "overrides": {
        "minimizeDisruptionOnReplanningWeight": 1
      }
    }
  },
  "patch": [
    {
      "op": "add",
      "path": "/vehicles/[id=Beth]/shifts/-",
      "value": {
        "id": "Beth-2027-02-01-B",
        "startLocation": [33.70474, -84.06508],
        "minStartTime": "2027-02-01T09:30: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>
{
  "metadata": {
    "id": "ID",
    "originId": "ID",
    "name": "Real-time planning: minimize disruption",
    "submitDateTime": "2026-08-17T22:42:52.813622844+02:00",
    "startDateTime": "2026-08-17T22:42:53.109675321+02:00",
    "activeDateTime": "2026-08-17T22:42:53.111022788+02:00",
    "completeDateTime": "2026-08-17T22:43:23.20204323+02:00",
    "shutdownDateTime": "2026-08-17T22:43:23.202066006+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-3964soft",
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Ann",
        "shifts": [
          {
            "id": "Ann-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit E",
                "arrivalTime": "2027-02-01T09:31:10Z",
                "startServiceTime": "2027-02-01T09:31:10Z",
                "departureTime": "2027-02-01T11:01:10Z",
                "effectiveServiceDuration": "PT1H30M",
                "travelTimeFromPreviousStandstill": "PT31M10S",
                "travelDistanceMetersFromPreviousStandstill": 25971,
                "minStartTravelTime": "2027-02-01T00:00:00Z",
                "kind": "VISIT"
              }
            ],
            "metrics": {
              "totalServiceDuration": "PT1H30M",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT1H2M20S",
              "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT31M10S",
              "totalTravelDistanceMeters": 51942,
              "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 25971,
              "endLocationArrivalTime": "2027-02-01T11:32:20Z",
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 1,
          "assignedVisits": 1,
          "totalShiftDuration": "PT2H32M20S",
          "totalServiceDuration": "PT1H30M",
          "totalTravelTime": "PT1H2M20S",
          "totalTravelDistanceMeters": 51942,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      },
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startTime": "2027-02-01T16:00:00Z",
            "itinerary": [],
            "metrics": {
              "totalServiceDuration": "PT0S",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0,
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          },
          {
            "id": "Beth-2027-02-01-B",
            "startTime": "2027-02-01T09:30:00Z",
            "itinerary": [],
            "metrics": {
              "totalServiceDuration": "PT0S",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0,
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 0,
          "assignedVisits": 0,
          "totalShiftDuration": "PT0S",
          "totalServiceDuration": "PT0S",
          "totalTravelTime": "PT0S",
          "totalTravelDistanceMeters": 0,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      }
    ],
    "unassignedVisits": []
  },
  "inputMetrics": {
    "vehicles": 2,
    "vehicleShifts": 3,
    "visits": 1,
    "mandatoryVisits": 1,
    "optionalVisits": 0,
    "pinnedVisits": 0,
    "visitsWithSla": 0,
    "visitGroups": 0,
    "visitDependencies": 0,
    "excludedVisits": 0,
    "movableVisits": 0
  },
  "kpis": {
    "averageTravelTimePerVisit": "PT1H2M20S",
    "totalTravelTime": "PT1H2M20S",
    "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT31M10S",
    "averageTravelDistanceMetersPerVisit": 51942,
    "totalTravelDistanceMeters": 51942,
    "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 25971,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 1,
    "assignedMandatoryVisits": 1,
    "unassignedMandatoryVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 0.0,
    "totalOvertime": "PT0S",
    "availableOvertime": "PT0S"
  }
}

modelOutput shows that Visit E stays with Ann, at its original startServiceTime of 09:31:10. Beth’s new shift, Beth-2027-02-01-B, remains empty despite Visit E preferring Beth: the 100 (constraint weight) times 100 vehicle-change penalty from Minimize disruption on replanning far outweighs the reward the preferred-vehicle constraint would have given for reassigning it.

If you are not using the Patch feature, resubmitting the full updated input dataset as a new dataset has the same effect, but because a fresh dataset has no previous solve to continue from, Visit E’s original assignment must be added back manually, the same way it would need to be for pinning individual visits, just without setting pin to true:

  • 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": "Real-time planning: minimize disruption"
    },
    "model": {
      "overrides": {
        "minimizeDisruptionOnReplanningWeight": 100
      }
    }
  },
  "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",
            "itinerary": [
              {
                "id": "Visit E",
                "kind": "VISIT",
                "startServiceTime": "2027-02-01T09:35:42Z"
              }
            ]
          }
        ]
      },
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startLocation": [33.70474, -84.06508],
            "minStartTime": "2027-02-01T16:00:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z"
          },
          {
            "id": "Beth-2027-02-01-B",
            "startLocation": [33.70474, -84.06508],
            "minStartTime": "2027-02-01T09:30:00Z",
            "maxEndTime": "2027-02-01T17:00:00Z"
          }
        ]
      }
    ],
    "visits": [
      {
        "id": "Visit E",
        "location": [33.90719, -84.28149],
        "serviceDuration": "PT1H30M",
        "preferredVehicles": ["Beth"],
        "minStartTravelTime": "2027-02-01T00: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>
{
  "metadata": {
    "id": "ID",
    "originId": "ID",
    "name": "Real-time planning: minimize disruption",
    "submitDateTime": "2026-08-17T22:42:52.813622844+02:00",
    "startDateTime": "2026-08-17T22:42:53.109675321+02:00",
    "activeDateTime": "2026-08-17T22:42:53.111022788+02:00",
    "completeDateTime": "2026-08-17T22:43:23.20204323+02:00",
    "shutdownDateTime": "2026-08-17T22:43:23.202066006+02:00",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-3964soft",
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "vehicles": [
      {
        "id": "Ann",
        "shifts": [
          {
            "id": "Ann-2027-02-01",
            "startTime": "2027-02-01T09:00:00Z",
            "itinerary": [
              {
                "id": "Visit E",
                "arrivalTime": "2027-02-01T09:31:10Z",
                "startServiceTime": "2027-02-01T09:31:10Z",
                "departureTime": "2027-02-01T11:01:10Z",
                "effectiveServiceDuration": "PT1H30M",
                "travelTimeFromPreviousStandstill": "PT31M10S",
                "travelDistanceMetersFromPreviousStandstill": 25971,
                "minStartTravelTime": "2027-02-01T00:00:00Z",
                "kind": "VISIT"
              }
            ],
            "metrics": {
              "totalServiceDuration": "PT1H30M",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT1H2M20S",
              "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT31M10S",
              "totalTravelDistanceMeters": 51942,
              "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 25971,
              "endLocationArrivalTime": "2027-02-01T11:32:20Z",
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 1,
          "assignedVisits": 1,
          "totalShiftDuration": "PT2H32M20S",
          "totalServiceDuration": "PT1H30M",
          "totalTravelTime": "PT1H2M20S",
          "totalTravelDistanceMeters": 51942,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      },
      {
        "id": "Beth",
        "shifts": [
          {
            "id": "Beth-2027-02-01",
            "startTime": "2027-02-01T16:00:00Z",
            "itinerary": [],
            "metrics": {
              "totalServiceDuration": "PT0S",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0,
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          },
          {
            "id": "Beth-2027-02-01-B",
            "startTime": "2027-02-01T09:30:00Z",
            "itinerary": [],
            "metrics": {
              "totalServiceDuration": "PT0S",
              "totalBreakDuration": "PT0S",
              "totalWaitingTime": "PT0S",
              "totalTravelTime": "PT0S",
              "travelTimeFromStartLocationToFirstVisit": "PT0S",
              "travelTimeBetweenVisits": "PT0S",
              "travelTimeFromLastVisitToEndLocation": "PT0S",
              "totalTravelDistanceMeters": 0,
              "travelDistanceFromStartLocationToFirstVisitMeters": 0,
              "travelDistanceBetweenVisitsMeters": 0,
              "travelDistanceFromLastVisitToEndLocationMeters": 0,
              "overtime": "PT0S",
              "availableOvertime": "PT0S"
            }
          }
        ],
        "metrics": {
          "activatedShifts": 0,
          "assignedVisits": 0,
          "totalShiftDuration": "PT0S",
          "totalServiceDuration": "PT0S",
          "totalTravelTime": "PT0S",
          "totalTravelDistanceMeters": 0,
          "totalBreakTime": "PT0S",
          "totalWaitingTime": "PT0S",
          "totalOvertime": "PT0S",
          "availableOvertime": "PT0S"
        }
      }
    ],
    "unassignedVisits": []
  },
  "inputMetrics": {
    "vehicles": 2,
    "vehicleShifts": 3,
    "visits": 1,
    "mandatoryVisits": 1,
    "optionalVisits": 0,
    "pinnedVisits": 0,
    "visitsWithSla": 0,
    "visitGroups": 0,
    "visitDependencies": 0,
    "excludedVisits": 0,
    "movableVisits": 0
  },
  "kpis": {
    "averageTravelTimePerVisit": "PT1H2M20S",
    "totalTravelTime": "PT1H2M20S",
    "travelTimeFromStartLocationToFirstVisit": "PT31M10S",
    "travelTimeBetweenVisits": "PT0S",
    "travelTimeFromLastVisitToEndLocation": "PT31M10S",
    "averageTravelDistanceMetersPerVisit": 51942,
    "totalTravelDistanceMeters": 51942,
    "travelDistanceFromStartLocationToFirstVisitMeters": 25971,
    "travelDistanceBetweenVisitsMeters": 0,
    "travelDistanceFromLastVisitToEndLocationMeters": 25971,
    "totalUnassignedVisits": 0,
    "totalAssignedVisits": 1,
    "assignedMandatoryVisits": 1,
    "unassignedMandatoryVisits": 0,
    "totalActivatedVehicles": 1,
    "workingTimeFairnessPercentage": 0.0,
    "totalOvertime": "PT0S",
    "availableOvertime": "PT0S"
  }
}

modelOutput shows the same result: Visit E stays with Ann at its original startServiceTime.

Next

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

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

  • Learn about real-time planning.

  • Real-time planning with pinned visits.

  • Learn about constraints and constraint weights.

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