Skip to content

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[].breakDuration shows 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 in routes[].breaks[] and on no leg.
  • metrics.breakDuration and metrics.visitDuration add 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.json

The 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