Movable stops and multi-day schedules
Stops in pick-up and delivery routing often have availability time windows that specify when the stops can occur.
Stops can have a single or multiple time windows, for instance:
-
A single time window on a single day: 09:00 to 17:00.
-
Multiple time windows on a single day: 09:00 to 11:00 and 13:00 to 17:00.
-
A single time window that spans multiple days: February 1st 2027 at 09:00 to February 2nd 2027 at 17:00.
-
Multiple time windows on multiple days: February 1st 2027 at 09:00 to 17:00 and February 2nd 2027 at 09:00 to 17:00.
When a stop has a time window or time windows that are on a single day, they are considered non-movable. The stop cannot be scheduled on another day.
When a stop has a time window or time windows that span multiple days they are considered movable. The stop can be scheduled on different days.
Movable stops can be scheduled at any time during their time window, however, it is often preferable to schedule them as close to the beginning of their time window as possible to avoid running out of options later in the time window.
This guide explains how to manage movable stops and multi-day schedules with the following example:
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. Schedule movable stops to the earliest day
The Prefer stops scheduled to the earliest day soft constraint adds a penalty for every day after a movable stops' minStartTime that the stop is scheduled, incentivizing Timefold to schedule the stop as early as possible.
In the following example, the stops in Job A are movable stops with time windows on 3 days. The stops in Job B are not movable stops as they have time windows on a single day.
{
"jobs": [
{
"id": "Job A",
"stops": [
{
"id": "A1",
"location": [33.78592, -84.46136],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17:00:00Z"
}
]
},
{
"id": "A2",
"location": [33.72757, -83.96354],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17: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-01T17:00:00Z"
}
]
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobB_dep1",
"precedingStop": "B1"
}
]
}
]
}
]
}
Carl is the only available driver in the area where these stops are located.
{
"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"
},
{
"id": "Carl Tue",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-02T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
},
{
"id": "Carl Wed",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-03T09:00:00Z",
"maxEndTime": "2027-02-03T17:00:00Z"
}
]
}
Carl is available between 09:00 and 13:00 on Monday and between 09:00 and 17:00 on Tuesday and Wednesday.
The stops in Job B, which have non-movable time windows on Monday the 1st, are schedule on Monday.
There is no time left to schedule the stops in Job A on Monday, but they have movable time windows for Monday, Tuesday, and Wednesday, and they are scheduled to the earliest available time which is on Tuesday.
-
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": "Prefer stops to the earliest day 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"
},
{
"id": "Carl Tue",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-02T09:00:00Z",
"maxEndTime": "2027-02-02T17:00:00Z"
},
{
"id": "Carl Wed",
"startLocation": [33.68786, -84.18487],
"endLocation": [33.68786, -84.18487],
"minStartTime": "2027-02-03T09:00:00Z",
"maxEndTime": "2027-02-03T17: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-03T17:00:00Z"
}
]
},
{
"id": "A2",
"location": [33.72757, -83.96354],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17: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-01T17:00:00Z"
}
]
},
{
"id": "B2",
"location": [33.48594, -84.26560],
"duration": "PT20M",
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-01T17:00:00Z"
}
],
"stopDependencies": [
{
"id": "jobB_dep1",
"precedingStop": "B1"
}
]
}
]
}
]
}
}
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": "Run-2025-07-24T08:22:57.963848066+02:00",
"submitDateTime": "2025-07-24T08:22:57.963848066+02:00",
"startDateTime": "2025-07-24T08:22:58.000506429+02:00",
"activeDateTime": "2025-07-24T08:22:58.075518595+02:00",
"completeDateTime": "2025-07-24T08:23:33.725346361+02:00",
"shutdownDateTime": "2025-07-24T08:23:33.742346344+02:00",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-19442soft",
"tags": [
"system.profile:default",
"system.type:from-input"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"drivers": [
{
"id": "Carl",
"shifts": [
{
"id": "Carl Mon",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "B1",
"arrivalTime": "2027-02-01T10:02:40Z",
"startServiceTime": "2027-02-01T10:02:40Z",
"departureTime": "2027-02-01T10:22:40Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT1H2M40S",
"travelDistanceMetersFromPreviousStandstill": 52218
},
{
"id": "B2",
"arrivalTime": "2027-02-01T11:48:03Z",
"startServiceTime": "2027-02-01T11:48:03Z",
"departureTime": "2027-02-01T12:08:03Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT1H25M23S",
"travelDistanceMetersFromPreviousStandstill": 71155
}
],
"metrics": {
"totalTravelTime": "PT2H56M27S",
"travelTimeFromStartLocationToFirstStop": "PT1H2M40S",
"travelTimeBetweenStops": "PT1H25M23S",
"travelTimeFromLastStopToEndLocation": "PT28M24S",
"totalTravelDistanceMeters": 147038,
"travelDistanceFromStartLocationToFirstStopMeters": 52218,
"travelDistanceBetweenStopsMeters": 71155,
"travelDistanceFromLastStopToEndLocationMeters": 23665,
"endLocationArrivalTime": "2027-02-01T12:36:27Z",
"overtime": "PT0S"
}
},
{
"id": "Carl Tue",
"startTime": "2027-02-02T09:00:00Z",
"itinerary": [
{
"id": "A1",
"arrivalTime": "2027-02-02T09:33:21Z",
"startServiceTime": "2027-02-02T09:33:21Z",
"departureTime": "2027-02-02T09:53:21Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT33M21S",
"travelDistanceMetersFromPreviousStandstill": 27795
},
{
"id": "A2",
"arrivalTime": "2027-02-02T10:49:07Z",
"startServiceTime": "2027-02-02T10:49:07Z",
"departureTime": "2027-02-02T11:09:07Z",
"effectiveServiceDuration": "PT20M",
"kind": "STOP",
"travelTimeFromPreviousStandstill": "PT55M46S",
"travelDistanceMetersFromPreviousStandstill": 46477
}
],
"metrics": {
"totalTravelTime": "PT1H54M15S",
"travelTimeFromStartLocationToFirstStop": "PT33M21S",
"travelTimeBetweenStops": "PT55M46S",
"travelTimeFromLastStopToEndLocation": "PT25M8S",
"totalTravelDistanceMeters": 95216,
"travelDistanceFromStartLocationToFirstStopMeters": 27795,
"travelDistanceBetweenStopsMeters": 46477,
"travelDistanceFromLastStopToEndLocationMeters": 20944,
"endLocationArrivalTime": "2027-02-02T11:34:15Z",
"overtime": "PT0S"
}
},
{
"id": "Carl Wed",
"startTime": "2027-02-03T09:00:00Z",
"itinerary": [],
"metrics": {
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstStop": "PT0S",
"travelTimeBetweenStops": "PT0S",
"travelTimeFromLastStopToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstStopMeters": 0,
"travelDistanceBetweenStopsMeters": 0,
"travelDistanceFromLastStopToEndLocationMeters": 0,
"overtime": "PT0S"
}
}
]
}
]
},
"inputMetrics": {
"stops": 4,
"drivers": 1,
"driverShifts": 3
},
"kpis": {
"totalTravelTime": "PT4H50M42S",
"travelTimeFromStartLocationToFirstStop": "PT1H36M1S",
"travelTimeBetweenStops": "PT2H21M9S",
"travelTimeFromLastStopToEndLocation": "PT53M32S",
"totalTravelDistanceMeters": 242254,
"travelDistanceFromStartLocationToFirstStopMeters": 80013,
"travelDistanceBetweenStopsMeters": 117632,
"travelDistanceFromLastStopToEndLocationMeters": 44609,
"totalUnassignedStops": 0,
"totalAssignedStops": 4,
"totalActivatedDrivers": 1,
"totalOvertime": "PT0S"
}
}
Next
-
See the full API spec or try the online API.