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
- 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
durationis 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_windowscan be scheduled at any time within the technician's working hours. - All durations and windows use seconds. 45 minutes is
2700, 1 hour is3600, and 90 minutes is5400. - Coordinates in
start_location,end_location, andlocationuse[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-annastarts at841(08:14), not at0. Use thestartaction time as the departure time. - Waiting time appears as
breakactions. In this response,tech-annahas threebreakactions, but only one break was requested. The actions at16469and23250have nowaypoint_index, and their durations do not match the requested1800seconds. 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 ofwater-heater-38th. Show only the break that matches your requesteddurationand window as lunch. Label the others as waiting time. - Arrival inside the window. Each job
start_timefalls inside its booked window. Work may finish after the window ends. - Skills. All three
gas-certifiedjobs went totech-anna, the only technician with that capability. - Workload. The optimizer finds a feasible, efficient plan. It does not split the day equally:
tech-benreturns 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.