Real-time planning: pinning visits
Real-time planning can be disruptive to technician’s schedules. Real-time planning is also not always possible, for instance:
-
Customers might only be available to provide access to the property at certain times, and the visit can only be completed during a specific time window.
-
A technician might have equipment or materials for a specific visit.
Visits that have already been assigned and scheduled can be pinned to prevent them from being rescheduled.
1. Pinning with freezeDeparturesBeforeTime
In the Real-time planning: emergency visit example, Ann had three visits scheduled for the day, while Beth had one. An emergency visit, Visit M, needed to be added to the schedule. The emergency visit was close to Ann’s existing route, and so the real-time plan reassigned Ann’s final visit of the day, Visit E, to Beth, and Ann was assigned the emergency Visit M.
However, this may not always be the best solution.
If Ann already has the material for Visit E loaded in her van, the visit cannot be assigned to another technician.
In this case, Visit E can be pinned so that it remains assigned to Ann, and the emergency visit, Visit M, will be assigned to Beth.
Real-time planning pins all visits before freezeDeparturesBeforeTime
, including visits that technicians are already traveling to.
Pinning additional visits gives you greater control over specific visits after the time specified in freezeDeparturesBeforeTime
.
1.1. Batch schedule: pinning
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/field-service-routing/v1/route-plans [email protected]
{
"config": {
"run": {
"name": "Original shift plan: pinning 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-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit E",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit B",
"location": [33.67590, -84.11845],
"serviceDuration": "PT1H30M"
},
{
"id": "Visit C",
"location": [33.76156, -84.27259],
"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>
{
"run": {
"id": "ID",
"name": "Original shift plan: pinning example",
"submitDateTime": "2024-12-11T04:47:09.396739074Z",
"startDateTime": "2024-12-11T04:47:16.153390108Z",
"activeDateTime": "2024-12-11T04:47:16.463899915Z",
"completeDateTime": "2024-12-11T04:52:16.530666888Z",
"shutdownDateTime": "2024-12-11T04:52:16.857617623Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-157696soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:18:16Z",
"startServiceTime": "2027-02-01T09:18:16Z",
"departureTime": "2027-02-01T10:48:16Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT18M16S",
"travelDistanceMetersFromPreviousStandstill": 15033,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:30:36Z",
"startServiceTime": "2027-02-01T11:30:36Z",
"departureTime": "2027-02-01T13:00:36Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT42M20S",
"travelDistanceMetersFromPreviousStandstill": 44028,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:41:09Z",
"startServiceTime": "2027-02-01T13:41:09Z",
"departureTime": "2027-02-01T15:11:09Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT40M33S",
"travelDistanceMetersFromPreviousStandstill": 42302,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT2H8M57S",
"travelTimeFromStartLocationToFirstVisit": "PT18M16S",
"travelTimeBetweenVisits": "PT1H22M53S",
"travelTimeFromLastVisitToEndLocation": "PT27M48S",
"totalTravelDistanceMeters": 130583,
"travelDistanceFromStartLocationToFirstVisitMeters": 15033,
"travelDistanceBetweenVisitsMeters": 86330,
"travelDistanceFromLastVisitToEndLocationMeters": 29220,
"endLocationArrivalTime": "2027-02-01T15:38:57Z"
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:10:37Z",
"startServiceTime": "2027-02-01T09:10:37Z",
"departureTime": "2027-02-01T10:40:37Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT10M37S",
"travelDistanceMetersFromPreviousStandstill": 8843,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT21M15S",
"travelTimeFromStartLocationToFirstVisit": "PT10M37S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT10M38S",
"totalTravelDistanceMeters": 17677,
"travelDistanceFromStartLocationToFirstVisitMeters": 8843,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 8834,
"endLocationArrivalTime": "2027-02-01T10:51:15Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT2H30M12S",
"travelTimeFromStartLocationToFirstVisit": "PT28M53S",
"travelTimeBetweenVisits": "PT1H22M53S",
"travelTimeFromLastVisitToEndLocation": "PT38M26S",
"totalTravelDistanceMeters": 148260,
"travelDistanceFromStartLocationToFirstVisitMeters": 23876,
"travelDistanceBetweenVisitsMeters": 86330,
"travelDistanceFromLastVisitToEndLocationMeters": 38054,
"totalUnassignedVisits": 0
}
}
modelOutput
contains Ann’s and Beth’s shift itineraries.
1.2. Real-time planning update: pinning
A little before midday, an emergency visit needs to be included in the plan for the same day.
Update the input dataset with the emergency visit (Visit M) included.
Because Visit M is a critical priority, it includes "priority": "1"
.
The already planned visits need to be updated with the minStartTravelTime
from the most recent planning output dataset (batch or real-time) to each visit.
Visit M has not already been planned and does not include a minStartTravelTime
value.
Because Visit E must be completed by Ann, add "pinningRequested": true
to Visit E.
Sequences of visits after the |
{
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z",
"pinningRequested": true
},
{
"id": "Visit B",
"location": [33.67590, -84.11845],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"location": [33.76156, -84.27259],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit M",
"location": [33.89104, -84.64711],
"serviceDuration": "PT1H30M",
"priority": "1"
}
]
}
Add the itinerary to Ann’s and Beth’s shifts from the previous output, including the visit IDs and the visit kind:
{
"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 C",
"kind": "VISIT"
},
{
"id": "Visit A",
"kind": "VISIT"
},
{
"id": "Visit E",
"kind": "VISIT"
}
]
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT"
}
]
}
]
}
]
}
Freeze visits that have already occurred and that technicians have begun traveling to by adding freezeDeparturesBeforeTime
:
{
"modelInput": {
"freezeDeparturesBeforeTime": "2027-02-01T12:00:00Z"
}
}
The call for the emergency visit came in a little before 12:00 and the freezeDeparturesBeforeTime
is set to 12:00.
Finally, resubmit the updated input dataset:
-
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: pinning"
}
},
"modelInput": {
"freezeDeparturesBeforeTime": "2027-02-01T12:00:00Z",
"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 C",
"kind": "VISIT"
},
{
"id": "Visit A",
"kind": "VISIT"
},
{
"id": "Visit E",
"kind": "VISIT"
}
]
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startLocation": [33.70474, -84.06508],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT"
}
]
}
]
}
],
"visits": [
{
"id": "Visit A",
"location": [33.84475, -84.63649],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"location": [33.90719, -84.28149],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z",
"pinningRequested": true
},
{
"id": "Visit B",
"location": [33.67590, -84.11845],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit C",
"location": [33.76156, -84.27259],
"serviceDuration": "PT1H30M",
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit M",
"location": [33.89104, -84.64711],
"serviceDuration": "PT1H30M",
"priority": "1"
}
]
}
}
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",
"name": "Real-time planning: pinning",
"submitDateTime": "2024-12-11T07:17:44.905391784Z",
"startDateTime": "2024-12-11T07:17:55.039550854Z",
"activeDateTime": "2024-12-11T07:17:55.744761354Z",
"completeDateTime": "2024-12-11T07:21:09.771021593Z",
"shutdownDateTime": "2024-12-11T07:21:10.102426256Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-297231soft",
"tags": null,
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Ann",
"shifts": [
{
"id": "Ann-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit C",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:18:16Z",
"startServiceTime": "2027-02-01T09:18:16Z",
"departureTime": "2027-02-01T10:48:16Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT18M16S",
"travelDistanceMetersFromPreviousStandstill": 15033,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit A",
"kind": "VISIT",
"arrivalTime": "2027-02-01T11:30:36Z",
"startServiceTime": "2027-02-01T11:30:36Z",
"departureTime": "2027-02-01T13:00:36Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT42M20S",
"travelDistanceMetersFromPreviousStandstill": 44028,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit E",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:41:09Z",
"startServiceTime": "2027-02-01T13:41:09Z",
"departureTime": "2027-02-01T15:11:09Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT40M33S",
"travelDistanceMetersFromPreviousStandstill": 42302,
"minStartTravelTime": "2027-02-01T00:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT2H8M57S",
"travelTimeFromStartLocationToFirstVisit": "PT18M16S",
"travelTimeBetweenVisits": "PT1H22M53S",
"travelTimeFromLastVisitToEndLocation": "PT27M48S",
"totalTravelDistanceMeters": 130583,
"travelDistanceFromStartLocationToFirstVisitMeters": 15033,
"travelDistanceBetweenVisitsMeters": 86330,
"travelDistanceFromLastVisitToEndLocationMeters": 29220,
"endLocationArrivalTime": "2027-02-01T15:38:57Z"
}
}
]
},
{
"id": "Beth",
"shifts": [
{
"id": "Beth-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Visit B",
"kind": "VISIT",
"arrivalTime": "2027-02-01T09:10:37Z",
"startServiceTime": "2027-02-01T09:10:37Z",
"departureTime": "2027-02-01T10:40:37Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT10M37S",
"travelDistanceMetersFromPreviousStandstill": 8843,
"minStartTravelTime": "2027-02-01T00:00:00Z"
},
{
"id": "Visit M",
"kind": "VISIT",
"arrivalTime": "2027-02-01T13:03:05Z",
"startServiceTime": "2027-02-01T13:03:05Z",
"departureTime": "2027-02-01T14:33:05Z",
"effectiveServiceDuration": "PT1H30M",
"travelTimeFromPreviousStandstill": "PT1H3M5S",
"travelDistanceMetersFromPreviousStandstill": 69591,
"minStartTravelTime": "2027-02-01T12:00:00Z"
}
],
"metrics": {
"totalTravelTime": "PT2H17M24S",
"travelTimeFromStartLocationToFirstVisit": "PT10M37S",
"travelTimeBetweenVisits": "PT1H3M5S",
"travelTimeFromLastVisitToEndLocation": "PT1H3M42S",
"totalTravelDistanceMeters": 150664,
"travelDistanceFromStartLocationToFirstVisitMeters": 8843,
"travelDistanceBetweenVisitsMeters": 69591,
"travelDistanceFromLastVisitToEndLocationMeters": 72230,
"endLocationArrivalTime": "2027-02-01T15:36:47Z"
}
}
]
}
]
},
"kpis": {
"totalTravelTime": "PT4H26M21S",
"travelTimeFromStartLocationToFirstVisit": "PT28M53S",
"travelTimeBetweenVisits": "PT2H25M58S",
"travelTimeFromLastVisitToEndLocation": "PT1H31M30S",
"totalTravelDistanceMeters": 281247,
"travelDistanceFromStartLocationToFirstVisitMeters": 23876,
"travelDistanceBetweenVisitsMeters": 155921,
"travelDistanceFromLastVisitToEndLocationMeters": 101450,
"totalUnassignedVisits": 0
}
}
modelOutput
contains Ann’s and Beth’s updated itineraries. Ann is still assigned to Visit E, and Beth is assigned to Visit M.
2. Pinning without freezeDeparturesBeforeTime
In some cases, even batch planning may need to take into account existing visit assignments (pinned visits) without considering all parts of the plan as a history that cannot be modified.
For instance, if visits for only one technician need to be pinned, but other visits being attended by other technicians do not need to be pinned.
You can pin specific visits without setting the freezeDeparturesBeforeTime
.
This will result in only the specific visits that have been pinned being frozen in the plan.
This scenario also requires that if there is a pinned visit C assigned to a vehicle shift, all visits in this vehicle shift’s itinerary before C must be pinned as well. |
For a schedule with visits A, B, C, and D assigned to the same vehicle shift, if Visit C needs to be pinned, then Visit A and Visit B must also be pinned.
Next
-
Understand the constraints of the Field Service Routing model.
-
See the full API spec or try the online API.
-
Manage shift times with Time zones and daylight-saving time (DST) changes.
-
Use Time windows to specify visit availability and limit when visits can be scheduled.
-
Learn about real-time planning.
-
Real-time planning with extended visits.
-
Real-time planning and emergency visits.