Priority stops and optional stops
When you are planning in pick-up and delivery routing, you can introduce mandatory stops and optional stops to help prioritize which jobs should be assigned as soon as possible and which jobs can wait until later.
Mandatory stops are stops with a time window that falls within the current planning window.
Optional stops are stops with a time window that ends outside the current planning window.
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 Pick-up and Delivery Routing model.
In the examples, replace <API_KEY> with the API Key you just copied.
1. Mandatory and optional stops
In the following example, the planning window is for February 1st, 2027:
{
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-02T00:00:00Z"
}
}
There are 2 jobs.
Job A has stops with time windows that are within the planning window, making them mandatory stops.
Job B has stops with time windows that end after the current planning window, making them optional stops.
{
"jobs": [
{
"id": "Job A",
"stops": [
{
"id": "A1",
"location": [33.78592, -84.46136],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "A2",
"location": [33.72757, -83.96354],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobA_dep1",
"precedingStop": "A1"
}
]
}
]
},
{
"id": "Job B",
"stops": [
{
"id": "B1",
"location": [34.11110, -84.43002],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobB_dep1",
"precedingStop": "B1"
}
]
}
]
}
]
}
Carl is working between 09:00 and 13:00 and only has time to complete one job.
Job A is a mandatory job and is assigned to Carl. Because Job B is an optional job, it is left unassigned.
-
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": "Assign mandatory and optional stops example"
}
},
"modelInput": {
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T13:00:00Z"
}
]
}
],
"jobs": [
{
"id": "Job A",
"stops": [
{
"id": "A1",
"location": [33.78592, -84.46136],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
]
},
{
"id": "A2",
"location": [33.72757, -83.96354],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobA_dep1",
"precedingStop": "A1"
}
]
}
]
},
{
"id": "Job B",
"stops": [
{
"id": "B1",
"location": [34.11110, -84.43002],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
]
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobB_dep1",
"precedingStop": "B1"
}
]
}
]
}
],
"planningWindow":
{
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-02T00: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/pickup-delivery-routing/v1/route-plans/<ID>
{
"metadata": {
"id": "ID",
"parentId": null,
"originId": "ID",
"name": "Assign mandatory and optional stops example",
"submitDateTime": "2025-10-15T06:25:52.212079318Z",
"startDateTime": "2025-10-15T06:25:59.044143469Z",
"activeDateTime": "2025-10-15T06:25:59.12949123Z",
"completeDateTime": "2025-10-15T06:26:29.566385636Z",
"shutdownDateTime": "2025-10-15T06:26:29.566389856Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-20000medium/-6852soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "A1",
"arrivalTime": "2027-02-01T09:31:38Z",
"startServiceTime": "2027-02-01T09:31:38Z",
"departureTime": "2027-02-01T09:51:38Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT31M38S",
"travelDistanceMetersFromPreviousStandstill": 33381
},
{
"id": "A2",
"arrivalTime": "2027-02-01T10:45:22Z",
"startServiceTime": "2027-02-01T10:45:22Z",
"departureTime": "2027-02-01T11:05:22Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT53M44S",
"travelDistanceMetersFromPreviousStandstill": 62124
}
],
"metrics": {
"totalTravelTime": "PT1H54M12S",
"travelTimeFromStartLocationToFirstStop": "PT31M38S",
"travelTimeBetweenStops": "PT53M44S",
"travelTimeFromLastStopToEndLocation": "PT28M50S",
"totalTravelDistanceMeters": 127024,
"travelDistanceFromStartLocationToFirstStopMeters": 33381,
"travelDistanceBetweenStopsMeters": 62124,
"travelDistanceFromLastStopToEndLocationMeters": 31519,
"endLocationArrivalTime": "2027-02-01T11:34:12Z",
"overtime": "PT0S"
}
}
]
}
]
},
"inputMetrics": {
"jobs": 2,
"stops": 4,
"drivers": 1,
"driverShifts": 1
},
"kpis": {
"totalTravelTime": "PT1H54M12S",
"totalTravelDistanceMeters": 127024,
"totalActivatedDrivers": 1,
"totalUnassignedJobs": "1",
"totalAssignedJobs": "1",
"totalUnassignedStops": 2,
"totalAssignedStops": 2,
"totalOvertime": "PT0S",
"travelTimeFromStartLocationToFirstStop": "PT31M38S",
"travelTimeBetweenStops": "PT53M44S",
"travelTimeFromLastStopToEndLocation": "PT28M50S",
"travelDistanceFromStartLocationToFirstStopMeters": 33381,
"travelDistanceBetweenStopsMeters": 62124,
"travelDistanceFromLastStopToEndLocationMeters": 31519
},
"run": {
"id": "b98f0072-c050-4ff2-ae93-3302833e98aa",
"parentId": null,
"originId": "b98f0072-c050-4ff2-ae93-3302833e98aa",
"name": "Assign mandatory and optional stops example",
"submitDateTime": "2025-10-15T06:25:52.212079318Z",
"startDateTime": "2025-10-15T06:25:59.044143469Z",
"activeDateTime": "2025-10-15T06:25:59.12949123Z",
"completeDateTime": "2025-10-15T06:26:29.566385636Z",
"shutdownDateTime": "2025-10-15T06:26:29.566389856Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-20000medium/-6852soft",
"tags": [
"system.type:from-request",
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
}
}
Next
-
See the full API spec or try the online API.