How to Schedule Technician Visits with Service Durations and Time Windows

API or service
Route Planner API
Task
Visits, durations, windows, breaks, and skills → technician schedules
Examples
URLAI
Difficulty
Intermediate
Time
12 min

Schedule technician visits with durations and time windows

A field-service team receives a day of customer visits. Each visit takes a known amount of time, many customers booked an arrival window, some jobs need a gas-certified technician, and every technician needs a lunch break. The dispatcher needs one feasible schedule per technician: which visits, in which order, and when each one starts.

The Route Planner API solves this in one request. Send technicians as agents with working hours, breaks, and capabilities. Send visits as jobs with a duration, booked time_windows, and requirements. The response returns an ordered schedule with start times for each technician.

This guide plans eight HVAC visits in Denver, Colorado, for two technicians who start and finish at a shop at 3400 Larimer Street.

Task flow: workday, visits, booked windows, breaks, and skills → relative seconds → Route Planner API → schedule per technician → clock times for dispatch.

Convert the workday into relative seconds

Route Planner times are plain numbers of seconds. The API does not read clock times, dates, or time zones. Choose one reference moment, call it 0, and convert every time to seconds after it. This guide uses 08:00 local time as 0:

Clock time (local) Relative seconds
08:00 0
10:00 7200
11:30 12600
12:00 14400
13:00 18000
15:00 25200
17:00 32400

Then map each business rule to a request field:

Business rule Field Value in this guide
Technician works 08:00–17:00 agents[].time_windows [[0, 32400]]
30-minute lunch that starts between 11:30 and 13:00 agents[].breaks [{"duration": 1800, "time_windows": [[12600, 18000]]}]
Technician holds a gas certification agents[].capabilities ["gas-certified"]
Visit takes 90 minutes on site jobs[].duration 5400
Customer booked a 08:00–10:00 arrival window jobs[].time_windows [[0, 7200]]
Furnace and boiler work needs a certified technician jobs[].requirements ["gas-certified"]

Keep these rules in mind:

  • A job time window limits when the technician arrives and starts the work. The visit may continue after the window ends. If the work must finish by the end of the window, subtract the duration from the window end.
  • A break time window limits when the break starts. The break duration is in seconds.
  • A job or agent can have up to 10 time windows. Use several windows for split availability, for example [[0, 7200], [21600, 25200]].
  • A job without time_windows can be scheduled at any time within the technician's working hours.
  • All durations and windows use seconds. 45 minutes is 2700, 1 hour is 3600, and 90 minutes is 5400.
  • Coordinates in start_location, end_location, and location use [longitude, latitude] order.

Convert times in your application with the time zone of the service area. When a schedule crosses a daylight saving time change, compute the seconds from real timestamps rather than by adding hours to a clock time.

Plan eight visits for two technicians

Both technicians start and end at the shop at 3400 Larimer Street, Denver. Only tech-anna has the gas-certified capability, so that technician is the only one eligible for the three gas jobs. Visit coordinates come from geocoding the customer addresses with the Geocoding API.

Job ID Customer address On site Booked arrival window Requirement
furnace-repair-clarkson 1550 Clarkson St, Denver, CO 80218 90 min 08:00–10:00 gas-certified
boiler-check-grove 1801 Grove St, Denver, CO 80204 60 min 08:00–12:00 gas-certified
water-heater-38th 4520 W 38th Ave, Denver, CO 80212 75 min 13:00–15:00 gas-certified
ac-tuneup-32nd 2420 W 32nd Ave, Denver, CO 80211 60 min 08:00–12:00 —
inspection-downing 2840 S Downing St, Denver, CO 80210 60 min 10:00–14:00 —
thermostat-pearl 1150 S Pearl St, Denver, CO 80210 45 min 12:00–17:00 —
duct-cleaning-12th 2100 E 12th Ave, Denver, CO 80206 90 min any time —
filter-service-holly 3900 Holly St, Denver, CO 80207 45 min 15:00–17:00 —
POST https://api.geoapify.com/v1/routeplanner?apiKey=YOUR_API_KEY
Content-Type: application/json
{
  "mode": "drive",
  "agents": [
    {
      "id": "tech-anna",
      "start_location": [-104.975422, 39.765989],
      "end_location": [-104.975422, 39.765989],
      "time_windows": [[0, 32400]],
      "capabilities": ["gas-certified"],
      "breaks": [{"duration": 1800, "time_windows": [[12600, 18000]]}]
    },
    {
      "id": "tech-ben",
      "start_location": [-104.975422, 39.765989],
      "end_location": [-104.975422, 39.765989],
      "time_windows": [[0, 32400]],
      "breaks": [{"duration": 1800, "time_windows": [[12600, 18000]]}]
    }
  ],
  "jobs": [
    {"id": "furnace-repair-clarkson", "location": [-104.977447, 39.740816], "duration": 5400, "time_windows": [[0, 7200]], "requirements": ["gas-certified"]},
    {"id": "boiler-check-grove", "location": [-105.026924, 39.74528], "duration": 3600, "time_windows": [[0, 14400]], "requirements": ["gas-certified"]},
    {"id": "water-heater-38th", "location": [-105.045475, 39.769297], "duration": 4500, "time_windows": [[18000, 25200]], "requirements": ["gas-certified"]},
    {"id": "ac-tuneup-32nd", "location": [-105.016386, 39.761943], "duration": 3600, "time_windows": [[0, 14400]]},
    {"id": "inspection-downing", "location": [-104.973382, 39.666064], "duration": 3600, "time_windows": [[7200, 21600]]},
    {"id": "thermostat-pearl", "location": [-104.980263, 39.696234], "duration": 2700, "time_windows": [[14400, 32400]]},
    {"id": "duct-cleaning-12th", "location": [-104.962097, 39.734967], "duration": 5400},
    {"id": "filter-service-holly", "location": [-104.922206, 39.771178], "duration": 2700, "time_windows": [[25200, 32400]]}
  ]
}

The request uses mode: "drive" because the technicians drive a service van with a car-sized footprint. Use a truck mode, such as light_truck, when the vehicle needs truck routing.

Stable id values on agents and jobs let you match the response to your dispatch records without relying on array positions.

Read the schedule and convert it to clock times

The API returned one feature per technician and no properties.issues, so all eight visits were assigned. The following response is reduced to the schedule fields.

Reduced response:

{
  "features": [
    {
      "properties": {
        "agent_id": "tech-anna",
        "start_time": 841,
        "end_time": 28385,
        "distance": 35841,
        "actions": [
          {"index": 0, "type": "start", "start_time": 841, "duration": 0, "waypoint_index": 0},
          {"index": 1, "type": "job", "job_id": "furnace-repair-clarkson", "start_time": 1157, "duration": 5400, "waypoint_index": 1},
          {"index": 2, "type": "job", "job_id": "boiler-check-grove", "start_time": 6978, "duration": 3600, "waypoint_index": 2},
          {"index": 3, "type": "job", "job_id": "ac-tuneup-32nd", "start_time": 10800, "duration": 3600, "waypoint_index": 3},
          {"index": 4, "type": "break", "start_time": 14400, "duration": 1800},
          {"index": 5, "type": "break", "start_time": 16469, "duration": 1531},
          {"index": 6, "type": "job", "job_id": "water-heater-38th", "start_time": 18000, "duration": 4500, "waypoint_index": 4},
          {"index": 7, "type": "break", "start_time": 23250, "duration": 1950},
          {"index": 8, "type": "job", "job_id": "filter-service-holly", "start_time": 25200, "duration": 2700, "waypoint_index": 5},
          {"index": 9, "type": "end", "start_time": 28385, "duration": 0, "waypoint_index": 6}
        ]
      }
    },
    {
      "properties": {
        "agent_id": "tech-ben",
        "start_time": 2156,
        "end_time": 17832,
        "distance": 31809,
        "actions": [
          {"index": 0, "type": "start", "start_time": 2156, "duration": 0, "waypoint_index": 0},
          {"index": 1, "type": "job", "job_id": "duct-cleaning-12th", "start_time": 2544, "duration": 5400, "waypoint_index": 1},
          {"index": 2, "type": "job", "job_id": "inspection-downing", "start_time": 8687, "duration": 3600, "waypoint_index": 2},
          {"index": 3, "type": "break", "start_time": 12600, "duration": 1800},
          {"index": 4, "type": "job", "job_id": "thermostat-pearl", "start_time": 14400, "duration": 2700, "waypoint_index": 3},
          {"index": 5, "type": "end", "start_time": 17832, "duration": 0, "waypoint_index": 4}
        ]
      }
    }
  ]
}

Add each start_time to the reference moment (08:00) to get a clock time. On-site work ends at start_time + duration:

Technician Clock time Action Booked window
tech-anna 08:14 Leaves the shop —
08:19–09:49 furnace-repair-clarkson 08:00–10:00
09:56–10:56 boiler-check-grove 08:00–12:00
11:00–12:00 ac-tuneup-32nd 08:00–12:00
12:00–12:30 Lunch break (1800 s) starts 11:30–13:00
12:34–13:00 Waits for the next window —
13:00–14:15 water-heater-38th 13:00–15:00
14:27–15:00 Waits for the next window —
15:00–15:45 filter-service-holly 15:00–17:00
15:53 Back at the shop —
tech-ben 08:35 Leaves the shop —
08:42–10:12 duct-cleaning-12th any time
10:24–11:24 inspection-downing 10:00–14:00
11:30–12:00 Lunch break (1800 s) starts 11:30–13:00
12:00–12:45 thermostat-pearl 12:00–17:00
12:57 Back at the shop —

Check these details before dispatching the plan:

  • Departure time. A technician's plan can start later than the working window. tech-anna starts at 841 (08:14), not at 0. Use the start action time as the departure time.
  • Waiting time appears as break actions. In this response, tech-anna has three break actions, but only one break was requested. The actions at 16469 and 23250 have no waypoint_index, and their durations do not match the requested 1800 seconds. They fill the time between arriving early and the opening of the next booked window, for example 12:34–13:00 before the 13:00 window of water-heater-38th. Show only the break that matches your requested duration and window as lunch. Label the others as waiting time.
  • Arrival inside the window. Each job start_time falls inside its booked window. Work may finish after the window ends.
  • Skills. All three gas-certified jobs went to tech-anna, the only technician with that capability.
  • Workload. The optimizer finds a feasible, efficient plan. It does not split the day equally: tech-ben returns at 12:57. To balance work, shorten the technicians' time_windows, add more jobs, or assign work in separate requests by territory.

If a visit cannot fit any technician's hours, breaks, and skills, its zero-based index appears in properties.issues.unassigned_jobs. A 200 response does not mean that every job was scheduled. Check the issues before you send schedules to technicians, and report the affected job IDs to the dispatcher.

Technician scheduling with an AI coding agent

Copy this prompt into Codex, Claude Code, Cursor, or another coding agent to add daily technician scheduling to your project.

Add daily technician scheduling to my project with the Geoapify Route Planner API.

Before you start
- Inspect the existing project first. Reuse its current framework, components, styling, HTTP client, and conventions. Make the smallest necessary changes.
- Ask only what the project does not answer: where technicians, visits, and booked windows are stored; the time zone of the service area; and whether the schedule is shown in a table, on a map, or both.
- Read the Geoapify API key from GEOAPIFY_API_KEY on the server. Never hardcode, print, or log the key, and do not call the Route Planner API from the browser.

API request
- POST https://api.geoapify.com/v1/routeplanner with mode, agents, and jobs.
- Agents: id, start_location and end_location as [longitude, latitude], time_windows for working hours, breaks as [{duration, time_windows}], and capabilities for skills.
- Jobs: id, location as [longitude, latitude], duration in seconds, time_windows for booked arrival windows, and requirements for needed skills.

Time handling
- Choose the start of the working day in the service area's time zone as 0. Convert working hours, booked windows, and break windows to seconds after it, using real timestamps so daylight saving time changes are handled.
- A job time window limits arrival, not completion. A break time window limits when the break starts.
- Convert every response start_time back to a local clock time. Work ends at start_time + duration.

Reading the result
- Use the start action as the departure time; it can be later than 0.
- The response also reports waiting time before a booked window as break actions without waypoint_index. Show only the break that matches the requested duration and window as the lunch break, and label the rest as waiting.
- Match actions to records by agent_id and job_id, not by array position.
- Check properties.issues.unassigned_jobs. List unassigned visits with their IDs instead of hiding them.
- Handle HTTP errors, timeouts, and limited 429 retries with backoff.
- Add a test with a mocked response that includes a waiting break and an unassigned job.

Review the generated code against the request in this guide. Check the coordinate order, the units (seconds), the time zone conversion, the waiting-time labels, and the handling of unassigned jobs.