Schedule field service with breaks
Three technicians in Ottawa, each starting and ending at home: Kanata, Orléans and Alta Vista. Nine
jobs take 30 to 90 minutes each (duration), and some have to start inside a window. Two jobs need
the certification only the Kanata and Alta Vista technicians have (allowedVehicleIndices), one
is in Kanata and can only go to the Kanata technician, and one is in Orléans and goes to the Orléans
technician.
Every technician takes a 30 minute lunch break starting between 12:00 and 13:00
(breakRule.breakRequests), and no day may last more than six and a half hours from leaving home
to getting back (routeDurationLimit).
What to read in the answer:
routes[].breaks[]gives each break's start and length.transitions[].breakDurationshows which leg a break fell on. A break may also come before the vehicle starts or after it ends (the break rule allows both); such a break is inroutes[].breaks[]and on no leg.metrics.breakDurationandmetrics.visitDurationadd up the breaks on the route's legs and the time on site.
Send it
Save the body below as field-service-with-breaks.json and post it to /optimize. Set UNMAP_API_KEY to your API key.
curl -X POST "https://api.unmap.dev/optimize" \
-H "X-API-Key: $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
--data @field-service-with-breaks.jsonThe body uses straight-line travel at a fixed speed (useGeodesicDistances with geodesicMetersPerSecond), so it needs no road network. Delete those two fields to plan with driving times from the Canada road network instead. Times are UTC: 12:00:00Z is 08:00 in Ottawa in October. "timeout": "10s" is the longest solve on the Hobby plan, and the request stays inside Hobby's limits (25 shipments, 5 vehicles). The solver may return a different visit order between runs.
This body is a file in Unmap's conformance suite, which validates it and checks the solver's answers for feasibility. After you change it, send it with "solvingMode": "VALIDATE_ONLY" to check its fields, then solve it to check that your constraints fit together.
{
"useGeodesicDistances": true,
"geodesicMetersPerSecond": 11,
"timeout": "10s",
"model": {
"globalStartTime": "2026-10-13T12:00:00Z",
"globalEndTime": "2026-10-13T23:00:00Z",
"shipments": [
{
"label": "job-201",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.414, "longitude": -75.697 },
"duration": "3600s",
"timeWindows": [{ "startTime": "2026-10-13T13:00:00Z", "endTime": "2026-10-13T16:00:00Z" }]
}
]
},
{
"label": "job-202",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.401, "longitude": -75.688 },
"duration": "2700s"
}
]
},
{
"label": "job-203",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.392, "longitude": -75.755 },
"duration": "5400s"
}
],
"allowedVehicleIndices": [0, 2]
},
{
"label": "job-204",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.469, "longitude": -75.516 },
"duration": "1800s",
"timeWindows": [{ "startTime": "2026-10-13T17:00:00Z", "endTime": "2026-10-13T20:00:00Z" }]
}
],
"allowedVehicleIndices": [1]
},
{
"label": "job-205",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.309, "longitude": -75.907 },
"duration": "3600s"
}
],
"allowedVehicleIndices": [0]
},
{
"label": "job-206",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.274, "longitude": -75.736 },
"duration": "2700s",
"timeWindows": [{ "startTime": "2026-10-13T12:30:00Z", "endTime": "2026-10-13T15:00:00Z" }]
}
]
},
{
"label": "job-207",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.437, "longitude": -75.662 },
"duration": "1800s"
}
]
},
{
"label": "job-208",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.387, "longitude": -75.662 },
"duration": "4500s"
}
],
"allowedVehicleIndices": [0, 2]
},
{
"label": "job-209",
"deliveries": [
{
"arrivalLocation": { "latitude": 45.35, "longitude": -75.74 },
"duration": "2700s",
"timeWindows": [{ "startTime": "2026-10-13T18:00:00Z", "endTime": "2026-10-13T21:00:00Z" }]
}
]
}
],
"vehicles": [
{
"label": "tech-kanata",
"startLocation": { "latitude": 45.32, "longitude": -75.89 },
"endLocation": { "latitude": 45.32, "longitude": -75.89 },
"startTimeWindows": [{ "startTime": "2026-10-13T12:00:00Z", "endTime": "2026-10-13T12:30:00Z" }],
"endTimeWindows": [{ "endTime": "2026-10-13T21:30:00Z" }],
"breakRule": {
"breakRequests": [{ "earliestStartTime": "2026-10-13T16:00:00Z", "latestStartTime": "2026-10-13T17:00:00Z", "minDuration": "1800s" }]
},
"routeDurationLimit": { "maxDuration": "23400s" },
"costPerHour": 45,
"costPerKilometer": 0.4
},
{
"label": "tech-orleans",
"startLocation": { "latitude": 45.475, "longitude": -75.53 },
"endLocation": { "latitude": 45.475, "longitude": -75.53 },
"startTimeWindows": [{ "startTime": "2026-10-13T12:00:00Z", "endTime": "2026-10-13T12:30:00Z" }],
"endTimeWindows": [{ "endTime": "2026-10-13T21:30:00Z" }],
"breakRule": {
"breakRequests": [{ "earliestStartTime": "2026-10-13T16:00:00Z", "latestStartTime": "2026-10-13T17:00:00Z", "minDuration": "1800s" }]
},
"routeDurationLimit": { "maxDuration": "23400s" },
"costPerHour": 45,
"costPerKilometer": 0.4
},
{
"label": "tech-alta-vista",
"startLocation": { "latitude": 45.38, "longitude": -75.67 },
"endLocation": { "latitude": 45.38, "longitude": -75.67 },
"startTimeWindows": [{ "startTime": "2026-10-13T12:00:00Z", "endTime": "2026-10-13T12:30:00Z" }],
"endTimeWindows": [{ "endTime": "2026-10-13T21:30:00Z" }],
"breakRule": {
"breakRequests": [{ "earliestStartTime": "2026-10-13T16:00:00Z", "latestStartTime": "2026-10-13T17:00:00Z", "minDuration": "1800s" }]
},
"routeDurationLimit": { "maxDuration": "23400s" },
"costPerHour": 45,
"costPerKilometer": 0.4
}
]
}
}Next steps
- Optimize API: endpoints, field support, limits and pricing.
- Optimize errors: what a skipped shipment's reason means, and every error and warning.
- Optimize playground: run this request with your key and see the routes on a map.