Field Operations Industry, chapter 06 of 26
Booking Appointments
Booking a visit across channels: arrival windows, slot search, expiring holds, async requests, self-service limits, reschedules, cancellations and concurrency.
An appointment is a promise with a time on it, made through half a dozen doors: a sales rep on a doorstep, a customer service agent on the phone, the customer at midnight on the portal, a technician in a driveway offering the next visit, and the batch scheduler booking thousands of visits a night. Every one of them asks the same availability engine for slots, and every one can collide with the others over the last opening on a Thursday. This chapter covers what a booking is, from the hold that keeps a slot while a contract is signed through reschedules and cancellations, and what keeps the promise consistent when many hands make it.
Arrival Windows, Not Times#
Field service doesn't promise "10:15". It promises a window: "between 8 and noon". The route that serves the visit isn't final when it's booked, the visits before it run long or short, and traffic is traffic. A window is a promise the operation can keep; a time is one it will break.
The tools converge on the same idea. Jobber describes an arrival window as communicating that you'll arrive within a range of time rather than at an exact start time, either starting at the scheduled time or centered on it (a two-hour window around 9:00 runs 8:00 to 10:00). ServiceTitan has preset windows that reps pick when booking or rescheduling, and doesn't let a preset run past midnight. Keep a small set of named windows per branch, in local time, for example:
| Window | Hours |
|---|---|
| Anytime | The technician's whole working day |
| Morning | 8:00 to 12:00 |
| Afternoon | 12:00 to 16:00 |
| Evening | 16:00 to 21:00 |
The window the customer sees and the time window the router plans against are the same fact, and they must stay so. Route Optimization treats the window as a hard constraint, and an "anytime" visit as constrained only by the shift. The sequenced time the router produces ("estimated 10:40") is internal until the day of service, when it becomes the basis for an on-the-way message and a live ETA (Dispatch & the Day of Service). Promising the estimate days ahead turns every re-optimization into a broken promise.
Finding a Slot#
The customer chooses a date and a window, never a technician or a route. Those are the operation's business, and exposing them ties the router's hands. So a slot search returns openings: dates and windows that at least one route in the property's area can absorb, as decided by the availability rules. Behind each opening is a ranked list of candidate placements (this route, this position, this technician).
Booking an opening tries the candidates in order until one succeeds. Between showing the opening and the customer clicking it, another booking may have taken that route's last stop; the next candidate is the fallback. Only when every candidate fails does the customer see "that time was just taken", along with fresh openings.
When nothing at all is available (every route locked, or no capacity in the window), say so in a way the customer can act on: a clear error with the branch's phone number, not an empty calendar. The explain mode tells staff why; the customer just needs to know a person can help.
Holds: Reservations That Expire#
Some bookings take longer than a click. A door-to-door sale runs through plan, pricing, payment and a contract signature before the customer is a customer, and the first service should be on the calendar before the rep leaves the porch. A phone call puts a customer on hold while the agent checks something. Holding the slot without booking it needs its own record: a reservation.
A reservation is a calendar entry that occupies a slot for a limited time:
- It expires. A reservation that isn't converted is released automatically, by a job that sweeps expired holds. Thirty minutes is a sensible default for a doorstep sale.
- It can be extended, up to a limit. Extend it before any step that may take a while, such as sending the contract for signature, so the hold survives queue lag between systems. Cap extensions, so an abandoned sale can't hold a Saturday morning forever.
- It converts once. When the sale completes, the reservation becomes a confirmed appointment, idempotently: the completion event may arrive twice, and must produce one appointment (Idempotency).
- It can be moved or cancelled before conversion, as an ordinary part of the sale.
Keep reservations in the same calendar as everything else. One table of calendar events, discriminated by type (appointment, reservation, appointment request, work break, out of office, generic), means a single overlap check covers all of them, and a technician's lunch can't be booked over any more than a reservation can.
Requests: "As Soon As Possible"#
Some bookings don't need a slot picked now. "Come back as soon as you can" (the pests are back, the customer wants a reservice) is a request, not a choice of window. Make it an appointment request: a placeholder that records the request type (new service, reschedule or reservice) and constraints, accepted immediately and placed by a worker shortly afterward.
The API answers 202 Accepted, which MDN describes as non-committal: accepted for processing, not yet processed, and it might still fail. So the response says how to follow up (a status URL, a notification when it's placed), the same pattern as any long-running operation. The worker drains requests at a controlled rate, so a surge of requests after a heavy-rain weekend doesn't stampede the slot search, and each placement is announced to the customer like any booking.
Booking Channels#
Every booking records its channel: which door it came through. The channel decides which rules apply, who gets credit, and how bookings are measured:
| Channel | Who books | Typical rules |
|---|---|---|
| Door-to-door sale | A sales rep, during the sale | Initial service only, held by a reservation |
| Inside sales | A phone sales rep | Restricted to routes already near the address |
| Customer service | A service agent | Broad access, with reasons logged for exceptions |
| Customer portal | The customer | The self-service limits below |
| Technician app | The technician, on site | Next visit or add-on service, often commissioned |
| Automated | The batch scheduler | Recurring visits inside their windows |
Carry the channel on the request (a header such as X-Sales-Channel set by each client, or a field the client can't omit) and onto the appointment. Then the availability rules can treat channels differently, reports can say which channel books which work, and commissions can be attributed to the right person.
What Customers May Do Themselves#
Self-service is cheaper than a call, and it's better for the customer, up to the point where a change needs a person. Draw that line in code, and show the customer why they're on the other side of it:
- The subscription must be in good standing. Active, or pending its first service, or past due by a little. A frozen or cancelled subscription can't book; the portal points to billing or to the team that can restart it.
- The initial service needs a person. It's often booked during the sale, can be long, and sets up the whole relationship. Moving it is a conversation.
- Same-day changes need a person. By the morning of service the route is dispatched and locked, and a change ripples through someone else's day.
- A visit already started can't be moved. The technician is there.
- Reservices are allowed for a while after a visit. A customer whose pests come back can request a free callback within a set number of days of the last completed visit, if the service is one that includes reservices, and no other rework is pending.
- Windows still apply. A reschedule outside the visit's compliance window is either blocked or allowed with an explicit acknowledgment that becomes the consent record (Recurring Schedules).
Every refusal carries a reason and a way forward (call this number, try these dates), rather than a disabled button.
Reschedules and Reservices Are New Appointments#
Moving an appointment by changing its date in place loses the history: when it was originally due, how many times it moved, who moved it and why. Instead, a reschedule cancels the original (with the reason "rescheduled") and creates a new appointment that points back to it. A reservice is a new appointment pointing back to the visit it redoes. Every appointment in a chain also carries the ID of the chain's first appointment, so "how many times did this visit move before it happened?" is a single query.
The chain is what the metrics need. A branch whose appointments move three times on average has a capacity problem disguised as a customer service one. Reservices per hundred visits, by technician, is a quality measure. Neither is computable once dates are edited in place.
A reservice has preconditions, and the API checks all of them: the original visit is completed, its service is reserviceable, the subscription is still active, and no other rework is already pending for it.
Cancellation#
Not every cancellation is the same event, and treating them alike sends the wrong message to the customer and the wrong signal to the business. Group cancellation reasons by who and why:
| Group | Reasons | Customer notified |
|---|---|---|
| Not serviced | Customer not home, no access, technician unavailable, inclement weather, other | Yes, with a way to rebook |
| Customer requested | The customer asked to cancel or move it | Yes, confirming |
| Internal | Duplicate, data correction, migration, system cleanup | No |
The internal group is quiet by design. A cleanup job that cancels 400 duplicate appointments must not send 400 apology texts.
Cancelling an appointment raises a question about the work it was for. The work orders on it (what was supposed to be done) can either be cancelled with it, or deferred: released back to the backlog to be booked again. A customer who cancels this month's visit still needs this month's treatment, so deferral is the usual default. A cancellation because the subscription itself was cancelled or frozen cancels the work outright.
Mistakes need an undo:
- Uncancel restores a cancelled appointment within a grace period, as long as its slot is still free.
- Status corrections between completed and cancelled (a technician closed the wrong visit) are allowed for a short window, then locked, because billing and compliance records follow completion.
- Check-in revives. If a technician checks in at a visit that was recently cancelled, they're standing at the house; the visit happened. Let the check-in restore it rather than fail, and record that it did.
A cancellation that leaves a recurring cycle unserved also gets a disposition (rescheduled in window, outside it with consent, declined, unreachable), as Recurring Schedules describes.
The Appointment's States#
Store the lifecycle as timestamps of transitions rather than a single status column: confirmed_at, started_at, completed_at, cancelled_at, and the accumulated paused time. The status is derived from which timestamps are set, and every transition keeps its time for free. Durations on site, how long before a visit customers confirm, how long after cancellation an uncancel happens: all of them come straight from the record. The guards in Payment States apply to the transitions themselves: each one is allowed only from specific states, and the API rejects the rest.
A separate operation, release, unassigns an appointment from its route without cancelling it. The appointment is still promised to the customer for that window; it just needs a different route, which re-optimization or a dispatcher provides.
Concurrency and Idempotency#
Bookings race. Two customers click the last Thursday morning slot; the batch scheduler and a customer service agent book the same customer's cycle a second apart; a customer double-taps "Book". The defenses stack:
- The database refuses overlaps. PostgreSQL's exclusion constraints are made for this:
EXCLUDE USING GIST (route_id WITH =, during WITH &&)rejects any calendar event whose time range overlaps another on the same route (thebtree_gistextension lets equality and range overlap combine). A check in application code is a race; a constraint is not (Transactions & Concurrency). - Capacity is locked with the route. Counting the stops on a route and inserting a new one happen in one transaction that locks the route row, so two bookings can't both see "one left".
- Requests carry idempotency keys. A retried booking returns the original result instead of booking again (Idempotency).
- Cycles are locked per subscription. A short lock on the subscription during same-day booking, and the duplicate-prevention window in batch scheduling, stop two channels from booking the same cycle twice.
Notifications Follow the Appointment#
Every booking queues messages for the future: a confirmation now, a reminder days before, an on-the-way text on the day. Every change to the appointment has to change those messages too. A reminder queued for a visit that was rescheduled tells the customer the wrong day; one queued for a cancelled visit sends a technician-less customer to wait at home.
So reminders are retractable. When an appointment is cancelled, rescheduled, released or deleted (and when its subscription, payment method or contact is removed), the queued messages for it are deleted before they go out, and new ones are queued for the new appointment. One rule saves a lot of confusion: send a cancellation notice only if the customer was told about the appointment in the first place. Customer Communications covers the message pipeline.
What This Means for the System You Build#
- Named arrival windows per branch, shared by the customer promise and the router, with estimated times kept internal until the day of service.
- Slot search that returns openings, with ranked candidate placements behind each one, tried in order on booking.
- Reservations that expire, extend up to a cap, and convert idempotently, in one calendar table with every other event type.
- Asynchronous appointment requests answered with
202 Acceptedand placed by a rate-limited worker. - A channel on every booking, driving rules, attribution and reporting.
- Self-service limits in code, each refusal with a reason and a way forward.
- Reschedules and reservices as new linked appointments, with a chain ID.
- Cancellation reasons grouped by who and why, with work orders deferred or cancelled, and uncancel, correction and check-in revival inside grace periods.
- Transitions stored as timestamps, with guarded state changes and release separate from cancel.
- Overlap prevention in the database, capacity checked under a route lock, idempotency keys, and per-subscription cycle locks.
- Retractable reminders, rebuilt on every change.
References#
Reference
- PostgreSQL: Range Types, Constraints on Ranges, exclusion constraints that prevent overlapping reservations, combined with equality through
btree_gist - MDN: 202 Accepted, accepted for processing but not completed, and non-committal about the outcome
In practice
- Jobber: Arrival windows, windows instead of exact start times, starting at or centered on the scheduled time
- ServiceTitan: Set up arrival windows, preset windows chosen when booking or rescheduling, which can't run past midnight