Real-time planning: reassignment (preview)
This guide describes features currently available as a preview feature. If you’d like early access to this feature, please Contact us. For information about the current supported feature, see Real-time planning. |
There are many situations where Real-time planning (preview) is necessary.
When a visit takes longer than expected other visits might need to be rescheduled or even assigned to another technician.
Consider the following shift schedule:
Carl has 3 visits scheduled for the day: Visit E, Visit B, and Visit D.
Ann has 2 visits scheduled for the day: Visit C and Visit A.
However, Visit E takes much longer than expected.
As a result, Visit B and Visit D need to be rescheduled and Visit D reassigned to Ann.
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. Batch schedule: reassignment
The original schedule was generated from the following input dataset during batch planning:
-
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: reassignment example"
}
},
"modelInput": {
"vehicles": [
{
"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"
}
]
},
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit E",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit B",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit D",
"location": [33.89351, -84.00649],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit A",
"location": [33.67590, -84.11845],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit C",
"location": [33.71517, -84.08527],
"serviceDuration": "PT1H30M"
}
]
}
}
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",
"parentId": null,
"originId": "ID",
"name": "Original shift plan: reassignment example",
"submitDateTime": "2025-09-11T06:29:45.145554453Z",
"startDateTime": "2025-09-11T06:30:17.589912379Z",
"activeDateTime": "2025-09-11T06:30:17.682930566Z",
"completeDateTime": "2025-09-11T06:30:48.160455421Z",
"shutdownDateTime": "2025-09-11T06:30:48.702420869Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-11836soft",
"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 E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:49:00Z",
"startServiceTime": "2027-02-01T09:49:00Z",
"departureTime": "2027-02-01T11:19:00Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT49M",
"travelDistanceMetersFromPreviousStandstill": 54141,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:59:33Z",
"startServiceTime": "2027-02-01T11:59:33Z",
"departureTime": "2027-02-01T13:29:33Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT40M33S",
"travelDistanceMetersFromPreviousStandstill": 42300,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit D",
"kind": "VISIT",
"arrivalTime": "2027-02-01T14:01:14Z",
"startServiceTime": "2027-02-01T14:01:14Z",
"departureTime": "2027-02-01T15:31:14Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT31M41S",
"travelDistanceMetersFromPreviousStandstill": 34290,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT2H39M7S",
"travelTimeFromStartLocationToFirstVisit": "PT49M",
"travelTimeBetweenVisits": "PT1H12M14S",
"travelTimeFromLastVisitToEndLocation": "PT37M53S",
"totalTravelDistanceMeters": 165335,
"travelDistanceFromStartLocationToFirstVisitMeters": 54141,
"travelDistanceBetweenVisitsMeters": 76590,
"travelDistanceFromLastVisitToEndLocationMeters": 34604,
"endLocationArrivalTime": "2027-02-01T16:09:07Z"
}
}
]
},
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:09:55Z",
"startServiceTime": "2027-02-01T09:09:55Z",
"departureTime": "2027-02-01T10:39:55Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT9M55S",
"travelDistanceMetersFromPreviousStandstill": 6094,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T10:52:12Z",
"startServiceTime": "2027-02-01T10:52:12Z",
"departureTime": "2027-02-01T12:22:12Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT12M17S",
"travelDistanceMetersFromPreviousStandstill": 8841,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT32M50S",
"travelTimeFromStartLocationToFirstVisit": "PT9M55S",
"travelTimeBetweenVisits": "PT12M17S",
"travelTimeFromLastVisitToEndLocation": "PT10M38S",
"totalTravelDistanceMeters": 23769,
"travelDistanceFromStartLocationToFirstVisitMeters": 6094,
"travelDistanceBetweenVisitsMeters": 8841,
"travelDistanceFromLastVisitToEndLocationMeters": 8834,
"endLocationArrivalTime": "2027-02-01T12:32:50Z"
}
}
]
}
],
"unassignedVisits": []
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 2,
"mandatoryVisits": 5,
"optionalVisits": 0,
"vehicleShifts": 2,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT3H11M57S",
"travelTimeFromStartLocationToFirstVisit": "PT58M55S",
"travelTimeBetweenVisits": "PT1H24M31S",
"travelTimeFromLastVisitToEndLocation": "PT48M31S",
"totalTravelDistanceMeters": 189104,
"travelDistanceFromStartLocationToFirstVisitMeters": 60235,
"travelDistanceBetweenVisitsMeters": 85431,
"travelDistanceFromLastVisitToEndLocationMeters": 43438,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 5,
"assignedMandatoryVisits": 5,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 2,
"workingTimeFairnessPercentage": 66.3
}
}
modelOutput
contains Carl’s and Ann’s shift itineraries.
2. Real-time planning update: reassignment
The plan needs to be updated to reflect the situation in the field.
Visit E took a total of 4 hours, which is 3 hours longer than expected.
The serviceDuration
for Visit E needs to be updated to 4 hours.
The following shows the required input and the patch operation that will change the original input:
-
Input
-
Patch
{
"id": "Visit E",
"location": [33.84475, -84.63649],
"serviceDuration": "PT4H",
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
{
"op": "replace",
"path": "/visits/[id=Visit E]/serviceDuration",
"value": "PT4H"
}
The departure times for visits that have already occurred and that technicians have already begin traveling to can be frozen by adding freezeDeparturesBeforeTime
and including the time when the freeze is implemented.
The following shows the required input and the patch operation that will change the original input:
-
Input
-
Patch
{
"modelInput": {
"freezeDeparturesBeforeTime": "2027-02-01T12:10:00Z"
}
}
{
"op": "add",
"path": "/freezeDeparturesBeforeTime",
"value": "2027-02-01T12:10:00Z"
}
In the original schedule Ann was due to finish Visit A at 12:22.
If the delay for Visit E is reported early enough, the freezeDeparturesBeforeTime
can be set prior to Ann finishing Visit A.
The updated schedule will keep visit A in place in the schedule and reschedule other visits as required.
Submit the full patch to modify the original input dataset to the following endpoint. Replace <ID> with the dataset ID:
v1/route-plans/<ID>/from-patch/
When submitted, solving is triggered 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: reassignment example"
}
},
"patch": [
{
"op": "add",
"path": "/freezeDeparturesBeforeTime",
"value": "2027-02-01T12:10:00Z"
},
{
"op": "replace",
"path": "/visits/[id=Visit E]/serviceDuration",
"value": "PT4H"
}
]
}
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",
"parentId": "ORIGIN-ID",
"originId": "ORIGIN-ID",
"name": "Real-time planning: reassignment example",
"submitDateTime": "2025-09-11T07:37:23.866151051Z",
"startDateTime": "2025-09-11T07:37:41.634466935Z",
"activeDateTime": "2025-09-11T07:37:41.680867654Z",
"completeDateTime": "2025-09-11T07:38:12.34045188Z",
"shutdownDateTime": "2025-09-11T07:38:12.891110645Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-12391soft",
"tags": [
"system.type:from-patch",
"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 E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:49:00Z",
"startServiceTime": "2027-02-01T09:49:00Z",
"departureTime": "2027-02-01T13:49:00Z",
"effectiveServiceDuration": "PT4H",
"travelTimeFromPreviousStandstill": "PT49M",
"travelDistanceMetersFromPreviousStandstill": 54141,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T14:29:33Z",
"startServiceTime": "2027-02-01T14:29:33Z",
"departureTime": "2027-02-01T15:59:33Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT40M33S",
"travelDistanceMetersFromPreviousStandstill": 42300,
"minStartTravelTime": "2027-02-01T12:10:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H57M18S",
"travelTimeFromStartLocationToFirstVisit": "PT49M",
"travelTimeBetweenVisits": "PT40M33S",
"travelTimeFromLastVisitToEndLocation": "PT27M45S",
"totalTravelDistanceMeters": 125661,
"travelDistanceFromStartLocationToFirstVisitMeters": 54141,
"travelDistanceBetweenVisitsMeters": 42300,
"travelDistanceFromLastVisitToEndLocationMeters": 29220,
"endLocationArrivalTime": "2027-02-01T16:27:18Z"
}
}
]
},
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:09:55Z",
"startServiceTime": "2027-02-01T09:09:55Z",
"departureTime": "2027-02-01T10:39:55Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT9M55S",
"travelDistanceMetersFromPreviousStandstill": 6094,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T10:52:12Z",
"startServiceTime": "2027-02-01T10:52:12Z",
"departureTime": "2027-02-01T12:22:12Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT12M17S",
"travelDistanceMetersFromPreviousStandstill": 8841,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit D",
"kind": "VISIT",
"arrivalTime": "2027-02-01T12:56:48Z",
"startServiceTime": "2027-02-01T12:56:48Z",
"departureTime": "2027-02-01T14:26:48Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT34M36S",
"travelDistanceMetersFromPreviousStandstill": 31646,
"minStartTravelTime": "2027-02-01T12:10:00Z"
}
],
"metrics": {
"totalTravelTime": "PT1H27M",
"travelTimeFromStartLocationToFirstVisit": "PT9M55S",
"travelTimeBetweenVisits": "PT46M53S",
"travelTimeFromLastVisitToEndLocation": "PT30M12S",
"totalTravelDistanceMeters": 72073,
"travelDistanceFromStartLocationToFirstVisitMeters": 6094,
"travelDistanceBetweenVisitsMeters": 40487,
"travelDistanceFromLastVisitToEndLocationMeters": 25492,
"endLocationArrivalTime": "2027-02-01T14:57:00Z"
}
}
]
}
],
"unassignedVisits": []
},
"inputMetrics": {
"visits": 5,
"visitGroups": 0,
"vehicles": 2,
"mandatoryVisits": 5,
"optionalVisits": 0,
"vehicleShifts": 2,
"visitsWithSla": 0
},
"kpis": {
"totalTravelTime": "PT3H24M18S",
"travelTimeFromStartLocationToFirstVisit": "PT58M55S",
"travelTimeBetweenVisits": "PT1H27M26S",
"travelTimeFromLastVisitToEndLocation": "PT57M57S",
"totalTravelDistanceMeters": 197734,
"travelDistanceFromStartLocationToFirstVisitMeters": 60235,
"travelDistanceBetweenVisitsMeters": 82787,
"travelDistanceFromLastVisitToEndLocationMeters": 54712,
"totalUnassignedVisits": 0,
"totalAssignedVisits": 5,
"assignedMandatoryVisits": 5,
"assignedOptionalVisits": 0,
"totalActivatedVehicles": 2,
"workingTimeFairnessPercentage": 88.77
}
}
modelOutput
contains Ann’s and Carl’s updated shift itineraries.
Visit E took longer than expected. Visit B is rescheduled, and Visit D is rescheduled and reassigned to Ann.
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.
-
Real-time planning with extended visits.
-
Real-time planning and emergency visits.