Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Field Service Routing
  • Real-time planning
  • Real-time planning: pinning visits

Field Service Routing

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with field service routing
    • Understanding the API
    • Constraints
    • Vehicle resource constraints
      • Shift hours and overtime
      • Lunch breaks and personal appointments
      • Fairness
      • Technician costs
    • Visit service constraints
      • Time windows and opening hours
      • Skills
      • Visit dependencies
      • Visit requirements
      • Multi-vehicle visits
      • Priority visits and optional visits
    • 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
    • Recommendations
      • Recommendations
      • Visit time window recommendations
      • Visit group time window recommendations
    • Time zones and daylight-saving time (DST)
    • New and noteworthy
    • Upgrading to the latest versions
    • Feature requests
    • Reference guide

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.

Prerequisites

To run the examples in this guide, you need to authenticate with a valid API key for this model:

  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. 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 pinning

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 freezeDeparturesBeforeTime can be pinned, but there cannot be any unpinned visits between the freezeDeparturesBeforeTime and pinned visits.

{
  "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.

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