Real-time planning: pinning stops (using patches)
| This guide describes functionality that relies on the Patch feature, which is currently only available as a preview feature. If you’d like early access to this feature, please Contact us. For information about real-time planning without the Patch functionality, see Real-time planning. |
Real-time planning allows you to modify an existing solution based on new information or changes in constraints. Real-time planning usually doesn’t require you to start the optimization from scratch, but rather takes an existing plan. However, in some cases, it is important to keep certain jobs and their stops assigned to specific drivers or at certain times and prevent them from being rescheduled.
Pinning stops prevents them from being rescheduled and helps to maintain stability in the schedule.
The pick-up and delivery model provides multiple ways of pinning stops to give you flexibility and control over the scheduling process:
-
Pinning with
freezeTimeto pin all stops before a specific time. -
Pinning whole driver shifts to prevent any changes to the entire shift itinerary.
-
Pinning individual stops to fix specific stops in the schedule and their shift.
1. Pinning with freezeTime
Learn how to configure an API Key to run the examples in this guide:
In the examples, replace |
Real-time planning can be disruptive to driver’s schedules depending on the frequency of incoming changes and updates.
To minimize disruption, you can pin all stops before freezeTime, including stops that drivers are already traveling to.
Pinning additional stops gives you greater control over specific stops after the time specified in freezeTime.
1.1. Batch schedule
The original schedule was generated from the following input dataset during the regular batch scheduling:
-
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/pickup-delivery-routing/v1/route-plans [email protected]
{
"config": {
"run": {
"name": "Original shift plan: pinning example"
}
},
"modelInput": {
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"startLocation": [33.77284, -84.42989],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth Mon",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"jobs": [
{
"id": "A",
"stops": [
{
"id": "A1",
"name": "A1",
"location": [33.77911, -84.49644],
"duration": "PT20M"
},
{
"id": "A2",
"name": "A2",
"location": [33.65979, -84.46366],
"duration": "PT20M"
}
]
},
{
"id": "B",
"stops": [
{
"id": "B1",
"name": "B1",
"location": [33.74648, -84.46461],
"duration": "PT20M"
},
{
"id": "B2",
"name": "B2",
"location": [33.65207, -84.46496],
"duration": "PT20M"
}
]
},
{
"id": "C",
"stops": [
{
"id": "C1",
"name": "C1",
"location": [33.72757, -83.96354],
"duration": "PT20M"
},
{
"id": "C2",
"name": "C2",
"location": [33.78592, -84.06508],
"duration": "PT20M"
}
]
}
]
}
}
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/pickup-delivery-routing/v1/route-plans/<ID>
{
"metadata": {
"id": "ID",
"originId": "ID",
"name": "Original shift plan: pinning example",
"submitDateTime": "2026-01-08T15:03:15.968089+01:00",
"startDateTime": "2026-01-08T15:03:16.00922+01:00",
"activeDateTime": "2026-01-08T15:03:16.010266+01:00",
"completeDateTime": "2026-01-08T15:03:46.016915+01:00",
"shutdownDateTime": "2026-01-08T15:03:46.016917+01:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-4705soft",
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "A1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:07:26Z",
"startServiceTime": "2027-02-01T09:07:26Z",
"departureTime": "2027-02-01T09:27:26Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT7M26S",
"travelDistanceMetersFromPreviousStandstill": 6190,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
},
{
"id": "B1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:33:02Z",
"startServiceTime": "2027-02-01T09:33:02Z",
"departureTime": "2027-02-01T09:53:02Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT5M36S",
"travelDistanceMetersFromPreviousStandstill": 4671,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
},
{
"id": "A2",
"kind": "STOP",
"arrivalTime": "2027-02-01T10:04:36Z",
"startServiceTime": "2027-02-01T10:04:36Z",
"departureTime": "2027-02-01T10:24:36Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT11M34S",
"travelDistanceMetersFromPreviousStandstill": 9640,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
},
{
"id": "B2",
"kind": "STOP",
"arrivalTime": "2027-02-01T10:25:38Z",
"startServiceTime": "2027-02-01T10:25:38Z",
"departureTime": "2027-02-01T10:45:38Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT1M2S",
"travelDistanceMetersFromPreviousStandstill": 867,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
}
],
"metrics": {
"totalTravelTime": "PT42M13S",
"travelTimeFromStartLocationToFirstStop": "PT7M26S",
"travelTimeBetweenStops": "PT18M12S",
"travelTimeFromLastStopToEndLocation": "PT16M35S",
"totalTravelDistanceMeters": 35183,
"travelDistanceFromStartLocationToFirstStopMeters": 6190,
"travelDistanceBetweenStopsMeters": 15178,
"travelDistanceFromLastStopToEndLocationMeters": 13815,
"endLocationArrivalTime": "2027-02-01T11:02:13Z"
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "C1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:11:40Z",
"startServiceTime": "2027-02-01T09:11:40Z",
"departureTime": "2027-02-01T09:31:40Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT11M40S",
"travelDistanceMetersFromPreviousStandstill": 9729,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
},
{
"id": "C2",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:45:22Z",
"startServiceTime": "2027-02-01T09:45:22Z",
"departureTime": "2027-02-01T10:05:22Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT13M42S",
"travelDistanceMetersFromPreviousStandstill": 11411,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": []
}
],
"metrics": {
"totalTravelTime": "PT36M12S",
"travelTimeFromStartLocationToFirstStop": "PT11M40S",
"travelTimeBetweenStops": "PT13M42S",
"travelTimeFromLastStopToEndLocation": "PT10M50S",
"totalTravelDistanceMeters": 30167,
"travelDistanceFromStartLocationToFirstStopMeters": 9729,
"travelDistanceBetweenStopsMeters": 11411,
"travelDistanceFromLastStopToEndLocationMeters": 9027,
"endLocationArrivalTime": "2027-02-01T10:16:12Z"
}
}
]
}
],
"unassignedJobs": []
},
"inputMetrics": {
"jobs": 3,
"stops": 6,
"drivers": 2,
"driverShifts": 2
},
"kpis": {
"totalTravelTime": "PT1H18M25S",
"totalTravelDistanceMeters": 65350,
"totalActivatedDrivers": 2,
"totalUnassignedJobs": 0,
"totalAssignedJobs": 3,
"assignedMandatoryJobs": 3,
"totalUnassignedStops": 0,
"totalAssignedStops": 6,
"assignedMandatoryStops": 6,
"travelTimeFromStartLocationToFirstStop": "PT19M6S",
"travelTimeBetweenStops": "PT31M54S",
"travelTimeFromLastStopToEndLocation": "PT27M25S",
"travelDistanceFromStartLocationToFirstStopMeters": 15919,
"travelDistanceBetweenStopsMeters": 26589,
"travelDistanceFromLastStopToEndLocationMeters": 22842
}
}
modelOutput contains Ann’s and Beth’s shift itineraries.
1.2. Real-time planning update
Early in the shift, a high priority job ("priority": "1") needs to be included in the plan for the same day.
The following shows the required input and the patch operation that will change the original input:
-
Input
-
Patch
{
"id": "D",
"priority": "1",
"stops": [
{
"id": "D1",
"name": "D1",
"location": [33.48594, -84.26560],
"duration": "PT20M"
},
{
"id": "D2",
"name": "D2",
"location": [34.11110, -84.43002],
"duration": "PT20M"
}
]
}
{
"op": "add",
"path": "/jobs/-",
"value": {
"id": "D",
"priority": "1",
"stops": [
{
"id": "D1",
"name": "D1",
"location": [33.48594, -84.26560],
"duration": "PT20M"
},
{
"id": "D2",
"name": "D2",
"location": [34.11110, -84.43002],
"duration": "PT20M"
}
]
}
}
The departure times for stops that have already occurred and that drivers have already begun traveling to can be frozen by adding freezeTime and including the time when the freeze is implemented.
The call for the high priority job came in a little before 10:00 and the freezeTime is set to 10:00.
The following shows the required input and the patch operation that will change the original input:
-
Input
-
Patch
{
"modelInput": {
"freezeTime": "2027-02-01T10:00:00Z"
}
}
{
"op": "add",
"path": "/freezeTime",
"value": "2027-02-01T10:00:00Z"
}
Submit the following 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/pickup-delivery-routing/v1/route-plans/<ID>/from-patch [email protected]
{
"config": {
"run": {
"name": "Real-time planning: pinning example"
}
},
"patch": [
{
"op": "add",
"path": "/freezeTime",
"value": "2027-02-01T10:00:00Z"
},
{
"op": "add",
"path": "/jobs/-",
"value": {
"id": "D",
"priority": "1",
"stops": [
{
"id": "D1",
"name": "D1",
"location": [33.48594, -84.26560],
"duration": "PT20M"
},
{
"id": "D2",
"name": "D2",
"location": [34.11110, -84.43002],
"duration": "PT20M"
}
]
}
}
]
}
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/pickup-delivery-routing/v1/route-plans/<ID>
{
"metadata": {
"id": "ID",
"originId": "ID",
"name": "Original shift plan: pinning example",
"submitDateTime": "2026-01-21T16:09:11.2355+01:00",
"startDateTime": "2026-01-21T16:09:11.337001+01:00",
"activeDateTime": "2026-01-21T16:09:11.337811+01:00",
"completeDateTime": "2026-01-21T16:09:41.353568+01:00",
"shutdownDateTime": "2026-01-21T16:09:41.35357+01:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-13422soft",
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "A1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:07:26Z",
"startServiceTime": "2027-02-01T09:07:26Z",
"departureTime": "2027-02-01T09:27:26Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT7M26S",
"travelDistanceMetersFromPreviousStandstill": 6190,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": [],
"pinned": true
},
{
"id": "B1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:33:02Z",
"startServiceTime": "2027-02-01T09:33:02Z",
"departureTime": "2027-02-01T09:53:02Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT5M36S",
"travelDistanceMetersFromPreviousStandstill": 4671,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": [],
"pinned": true
},
{
"id": "A2",
"kind": "STOP",
"arrivalTime": "2027-02-01T10:04:36Z",
"startServiceTime": "2027-02-01T10:04:36Z",
"departureTime": "2027-02-01T10:24:36Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT11M34S",
"travelDistanceMetersFromPreviousStandstill": 9640,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": [],
"pinned": true
},
{
"id": "B2",
"kind": "STOP",
"arrivalTime": "2027-02-01T10:25:38Z",
"startServiceTime": "2027-02-01T10:25:38Z",
"departureTime": "2027-02-01T10:45:38Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT1M2S",
"travelDistanceMetersFromPreviousStandstill": 867,
"minStartTravelTime": "2027-02-01T10:00:00Z",
"load": []
},
{
"id": "D1",
"kind": "STOP",
"arrivalTime": "2027-02-01T11:16:59Z",
"startServiceTime": "2027-02-01T11:16:59Z",
"departureTime": "2027-02-01T11:36:59Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT31M21S",
"travelDistanceMetersFromPreviousStandstill": 26123,
"minStartTravelTime": "2027-02-01T10:00:00Z",
"load": []
},
{
"id": "D2",
"kind": "STOP",
"arrivalTime": "2027-02-01T13:02:22Z",
"startServiceTime": "2027-02-01T13:02:22Z",
"departureTime": "2027-02-01T13:22:22Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT1H25M23S",
"travelDistanceMetersFromPreviousStandstill": 71155,
"minStartTravelTime": "2027-02-01T10:00:00Z",
"load": []
}
],
"metrics": {
"totalTravelTime": "PT3H7M30S",
"travelTimeFromStartLocationToFirstStop": "PT7M26S",
"travelTimeBetweenStops": "PT2H14M56S",
"travelTimeFromLastStopToEndLocation": "PT45M8S",
"totalTravelDistanceMeters": 156259,
"travelDistanceFromStartLocationToFirstStopMeters": 6190,
"travelDistanceBetweenStopsMeters": 112456,
"travelDistanceFromLastStopToEndLocationMeters": 37613,
"endLocationArrivalTime": "2027-02-01T14:07:30Z"
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "C1",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:11:40Z",
"startServiceTime": "2027-02-01T09:11:40Z",
"departureTime": "2027-02-01T09:31:40Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT11M40S",
"travelDistanceMetersFromPreviousStandstill": 9729,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": [],
"pinned": true
},
{
"id": "C2",
"kind": "STOP",
"arrivalTime": "2027-02-01T09:45:22Z",
"startServiceTime": "2027-02-01T09:45:22Z",
"departureTime": "2027-02-01T10:05:22Z",
"effectiveServiceDuration": "PT20M",
"travelTimeFromPreviousStandstill": "PT13M42S",
"travelDistanceMetersFromPreviousStandstill": 11411,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"load": [],
"pinned": true
}
],
"metrics": {
"totalTravelTime": "PT36M12S",
"travelTimeFromStartLocationToFirstStop": "PT11M40S",
"travelTimeBetweenStops": "PT13M42S",
"travelTimeFromLastStopToEndLocation": "PT10M50S",
"totalTravelDistanceMeters": 30167,
"travelDistanceFromStartLocationToFirstStopMeters": 9729,
"travelDistanceBetweenStopsMeters": 11411,
"travelDistanceFromLastStopToEndLocationMeters": 9027,
"endLocationArrivalTime": "2027-02-01T10:16:12Z"
}
}
]
}
],
"unassignedJobs": []
},
"inputMetrics": {
"jobs": 4,
"stops": 8,
"drivers": 2,
"driverShifts": 2
},
"kpis": {
"totalTravelTime": "PT3H43M42S",
"totalTravelDistanceMeters": 186426,
"totalActivatedDrivers": 2,
"totalUnassignedJobs": 0,
"totalAssignedJobs": 4,
"assignedMandatoryJobs": 4,
"totalUnassignedStops": 0,
"totalAssignedStops": 8,
"assignedMandatoryStops": 8,
"travelTimeFromStartLocationToFirstStop": "PT19M6S",
"travelTimeBetweenStops": "PT2H28M38S",
"travelTimeFromLastStopToEndLocation": "PT55M58S",
"travelDistanceFromStartLocationToFirstStopMeters": 15919,
"travelDistanceBetweenStopsMeters": 123867,
"travelDistanceFromLastStopToEndLocationMeters": 46640
}
}
modelOutput contains Ann’s and Beth’s updated itineraries.
Due to the freezeTime at 10:00, some of the stops were pinned and couldn’t be rescheduled.
The flag pinned of an output itinerary item indicates whether it was pinned during planning or not.
1.3. Finer control over pinning with freezeTime
In the above examples, setting the freezeTime meant that the following stops were pinned:
-
All stops that had already been completed or started.
-
All stops that drivers were already traveling to.
While pinning the stops that drivers were already traveling to is a safe option in terms of not disrupting the drivers' schedules, it may not always be necessary.
Imagine a scenario where Ann just finished Stop A at 09:00 and her next stop (Stop B) starts at 14:00. It will take Ann one hour to travel to Stop B, so she has plenty of time.
If the planning update happens at 11:00 (freezeTime is set to 11:00), Stop B would be pinned because Ann is already traveling to it, even though she might have enough time to handle a high priority stop at 12:00.
To avoid automatic pinning of stops that drivers are already traveling to, set the pinNextStopDuringFreeze property to NEVER.
This property can be set globally in the modelInput section of the route plan input, or for a specific driver shift (if both are set, the setting for the driver shift takes precedence):
{
"modelInput": {
"freezeTime": "2027-02-01T11:00:00Z",
"pinNextStopDuringFreeze": "ALWAYS",
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"pinNextStopDuringFreeze": "NEVER"
}
]
}
]
}
}
To summarize, the pinNextStopDuringFreeze property values have the following meaning:
-
ALWAYS(default): All stops that are completed, started or that drivers might already be traveling to before thefreezeTimeare pinned. -
NEVER: All stops that are completed or started before thefreezeTimeare pinned.
2. Pinning whole driver shifts
In some cases, it may be useful to pin the whole driver shift itinerary, for instance, if the driver shift already ended (imagine a multi-day schedule).
Pick-up and delivery routing will automatically pin all driver shifts that have maxEndTime or maxLastStopDepartureTime before or equal to the freezeTime.
Alternatively, pinning whole individual driver shifts can be used as a convenient shortcut to pin all stops in the driver shift itinerary. Note that pinning the whole driver shift also means that no new stops can be added to the driver shift itinerary (as opposed to pinning just the individual stops).
You can pin the whole driver shift by setting pinned to true for the driver shift:
{
"modelInput": {
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"pinned": true
}
]
}
]
}
}
Driver shift pinning can be combined with stop pinning and pinning by freezeTime.
3. Pinning individual stops
Sometimes it is necessary to pin specific stops in the schedule and their shift. Maybe the timing of a stop has already been finalized and communicated with a client or a driver has already loaded specific equipment onto their vehicle, which is needed for a specific client. In that case, the relevant stops should be pinned to the time and shift they were originally scheduled for.
Pinning stops is configured in the input itinerary of the updated dataset.
This can be achieved by setting startServiceTime as well as setting pin to true for an item on a driver shift itinerary.
As a result, the solver will not modify the start service time for a stop pinned that way. It might, however, modify the itinerary before and after the stop.
|
Setting |
Take the batch schedule from above as an example. Assume the delivery of Job A (i.e. Stop A2) has been communicated according to the original schedule and shouldn’t be changed during real-time planning. For that, we add the relevant information to the input itinerary of Ann:
-
Input
-
Patch
{
"modelInput": {
"drivers": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann Mon",
"itinerary": [
{
"id": "A1",
"kind": "STOP"
},
{
"id": "B1",
"kind": "STOP"
},
{
"id": "A2",
"kind": "STOP",
"pin": true,
"startServiceTime": "2027-02-01T10:04:36Z"
},
{
"id": "B2",
"kind": "STOP"
}
]
}
]
}
]
}
}
[
{
"op": "add",
"path": "/drivers/[id=Ann]/shifts/[id=Ann Mon]/itinerary/[id=A2]/pin",
"value": true
},
{
"op": "add",
"path": "/drivers/[id=Ann]/shifts/[id=Ann Mon]/itinerary/[id=A2]/startServiceTime",
"value": "2027-02-01T10:04:36Z"
}
]
Ignoring any freezeTime settings, the only pinned stop in this case will be A2.
The rest of the stops aren’t pinned and can be moved around as needed during real-time planning.
Next
-
See the full API spec or try the online API.
-
Use Time windows to specify stop availability and limit when stops can be scheduled.
-
Learn about real-time planning.