Field Operations Industry, chapter 04 of 26
Recurring Schedules
Turning a service cadence into dated visits: recurrence rules, seasonal intervals, anchors, tolerance windows, the backlog, missed cycles and batch scheduling.
Most of the revenue in field operations repeats. Pest control comes every month or every quarter, lawn treatments follow the growing season, pools get serviced every week, and furnaces and air conditioners get tuned up twice a year. The service agreement doesn't promise a date. It promises a cadence: "every 30 days in summer, every 60 in winter", or "four treatments a year". The operation has to turn that cadence into dated visits on real routes, months ahead, and keep doing it while visits slip, customers move them and weather cancels whole days. A recurring schedule is the system that keeps the promise measurable. For every subscription it can say when the next visit is due, whether it's booked, and whether it's booked inside the window the agreement allows.
The Promise Is a Cadence, Not a Date#
Generating a year of appointments when the agreement is signed is the obvious design, and it's wrong. Those appointments start rotting on the first day. The initial service runs late, the customer goes on vacation in June, a storm takes out a Tuesday in August. Each change shifts every appointment after it, and the system spends its time rewriting a future that was never real. Meanwhile, every pre-booked appointment holds a slot on a route that hasn't been built, for a technician who may not work there by then.
Store the rule, and derive the dates from it:
- The rule is the contract: how often, in which months, and how much drift is allowed.
- The next target is derived from the rule and the last visit, and recomputed whenever a visit completes.
- The appointment is booked close to the target, from capacity that actually exists.
Three things live on the subscription and are kept current by events, not computed on every read: the next target date, its window, and a pointer to the appointment booked for it, if there is one. Every screen that asks "when's my next service?" reads them, and so do the backlog, the batch scheduler and the reminder system. They're derived data, so a rebuild from the rule and the visit history must reproduce them exactly.
Expressing Cadence as Rules#
The standard language for recurrence is the iCalendar recurrence rule (RRULE) from RFC 5545, and it's worth adopting even if nothing ever exports a calendar file. A rule has a required frequency (DAILY, WEEKLY, MONTHLY, YEARLY and finer), an INTERVAL that defaults to 1, and filters such as BYMONTH and BYDAY. The RFC's own example of "every other week, forever" is FREQ=WEEKLY;INTERVAL=2. Libraries implement it in every language (python-dateutil's rrule describes itself as a complete implementation of the RFC's rules), so you don't write the date arithmetic yourself. In RFC 5545, DTSTART is the first instance of the series. For completion-anchored service (below), that's the anchor, so each new anchor re-evaluates the rule from a new start.
Service cadences fit RRULE well:
| Service | Rule |
|---|---|
| Weekly pool service | FREQ=WEEKLY;INTERVAL=1 |
| Every-other-week cleaning | FREQ=WEEKLY;INTERVAL=2 |
| Pest control every 30 days, April to October | FREQ=DAILY;INTERVAL=30;BYMONTH=4,5,6,7,8,9,10 |
| The same plan every 60 days the rest of the year | FREQ=DAILY;INTERVAL=60;BYMONTH=1,2,3,11,12 |
| Twice-yearly HVAC tune-up | FREQ=MONTHLY;INTERVAL=6 |
A seasonal cadence is several rules on one subscription, split by month. Two rules must never claim the same month, or the scheduler has two answers for one date, so enforce non-overlapping BYMONTH sets when rules are saved. A plan that pauses in winter (common for lawn care and for warm-season pest plans in cold markets) simply has no rule covering those months. A subscription must always have at least one rule. A subscription with none is never due, and an active customer who's never due is a silent cancellation.
Each rule carries more than the RRULE:
- Tolerance: how many days early and how many days late a visit may land and still count (below).
- Source: where it came from. The contract (copied from the plan when the agreement was signed), a customer request ("only come on Fridays"), or a correction.
- Consent reference: for any change the customer asked for or agreed to, a pointer to the recorded consent. A rule the customer changed is a contract amendment, and it needs the same evidence as one.
Copy the plan's rules onto the subscription when the agreement is signed, rather than referencing the plan. Plans change; the customer agreed to the cadence that existed when they signed. The same goes for per-state variations: when a state needs a different default (a tighter late tolerance, say), store it as a state override on the plan's rule, and copy the resolved result at signing.
Anchors: Counting From the Last Visit#
There are two ways to place the next visit, and the industry uses both:
- Calendar-anchored: the visit belongs to a fixed slot, such as every Tuesday or the first week of each quarter. Weekly pool routes and lawn mowing work this way: the route is the same every week, and the customer knows their day.
- Completion-anchored: the next visit is due a set interval after the last one actually happened. This suits treatments, where the product applied last time sets how soon the next one is useful or allowed.
For completion-anchored service, the anchor is the date of the last regular visit, and the next target is the anchor plus the interval. When a visit runs ten days late, the next one moves ten days too, so the spacing between treatments holds. The initial service is the first anchor.
Not every visit moves the anchor. A reservice (a free callback when pests come back between treatments, also called a callback or a retreat) isn't a regular visit, and if it reset the anchor, every complaint would push the next paid service further out. Only regular completions move it. Corrections can move it too: when a customer agrees, in writing, to a visit well outside the window, that consent can reset the anchor, and the disposition records that it did.
The anchor is a fact derived from the visit history. Store the pointer to the visit it came from, so it can be recomputed and audited, rather than a bare date someone can edit.
Target Dates and Windows#
A target date alone is too rigid to schedule against. Routes are built by area, and a technician in a given neighborhood on a Thursday can serve every customer there whose target falls nearby. So every target gets a window: the target minus an early tolerance through the target plus a late tolerance, in days.
Both edges matter, for different reasons:
- Late is the obvious failure: the customer is paying for protection that has lapsed, and the agreement usually says how late is too late (below).
- Early is the failure operations underestimates. A treatment product's label limits how often it may be applied, and EPA's guidance is blunt: using a pesticide inconsistently with its labeling violates federal law (Regulated Materials covers labels). A customer billed per service also notices when two visits land a week apart. Early visits need a reason, and the reason is recorded.
Tolerances belong to the rule, so a tight premium plan and a loose basic plan can coexist, and a state override can tighten them.
When a visit completes, classify it against the window it was due in, and keep that classification forever. It's the service compliance log:
| Classification | Meaning |
|---|---|
on_time |
Completed inside the window |
late |
Completed after the window closed |
early_violation |
Completed before the window opened, with no recorded reason |
early_customer_requested |
Completed early at the customer's request, with consent recorded |
reservice |
A callback, which isn't judged against the cadence |
The log answers questions that come from regulators, from customers disputing a bill, and from the business itself: what share of service was on time last month, by branch and by technician. Classify at completion time, against the rule in force then, and never recompute the history when the rule later changes.
The Backlog#
With targets and windows on every subscription, the backlog is a query rather than a report someone builds by hand. Every active subscription falls into exactly one tier:
| Tier | Condition | What it asks for |
|---|---|---|
| Overdue | The window closed, and no visit completed | Book it now, and record why it slipped |
| Due now | The window is open, and nothing is booked | Book it inside the window |
| Scheduled out of compliance | Booked, but outside the window | Move it, or record consent |
| Scheduled, not notified | Booked inside the window, but the customer hasn't been told | Send the reminder |
| Upcoming | The window hasn't opened yet | Nothing yet |
The tiers run worst first, so a dashboard sorted by tier is a worklist. The second-to-last tier exists because a booked appointment the customer doesn't know about isn't really booked: it ends in a no-show, or a locked gate.
Aggregated by branch, the backlog becomes the capacity signal. Divide the subscriptions due over the next few weeks by the visits each branch actually completes per day (a trailing average, say over 20 working days), and you have the days of work the branch is behind or ahead. Snapshot the result regularly, monthly at minimum. A backlog that grows a little each month is the earliest sign a branch needs another technician, long before the overdue tier explodes in peak season.
Missed Cycles and Consent#
Some visits can't happen in their window: the customer is away, the gate is locked, the technician quit, the week was all rain. What happens next is a business decision, a contract term and sometimes a legal requirement, so model it explicitly. When a visit is cancelled, record a cancellation disposition next to the reason:
- Who cancelled: the customer or the company. This decides whether a missed cycle is the customer's choice or the company's failure.
- The outcome: rescheduled inside the window; rescheduled outside it with the customer's consent; rescheduled outside it without consent (a compliance exception); customer declined; customer unreachable.
- The consent: a reference to the recorded consent (a signed document, a recorded call, a logged text reply), when the outcome needs one.
- The policy in force: a snapshot of the rule that applied, so a later change in policy doesn't rewrite the history.
"Customer declined" and "unreachable after the required attempts" can waive the cycle. The visit is skipped, the anchor moves on, and depending on the contract, the customer isn't billed for it. How many contact attempts count as unreachable, whether consent must be written or can be verbal, and how far outside the window a visit may move are set by the agreement and by company policy, and they can differ by state, so check the current rules for the trade in each state you operate in. Encode them as a policy per state, with effective dates, and evaluate each disposition against the policy in force on its date. Don't hard-code one state's rules into the scheduler. When a state's rules change, the policy table changes, and the scheduler and the reports follow.
Exclusions: Holidays, Weather and Seasons#
Some dates aren't available no matter what the rules say:
- Holidays, per branch, because the set differs by state and by company. RRULE has a mechanism for this:
EXDATEremoves specific dates from a recurrence set, and libraries such as python-dateutil also support exclusion rules. In practice it's simpler to keep holidays as branch-level exclusions that the slot search respects than to write them into every subscription's rule. - Weather incidents: a branch-wide stand-down for snow, flood, fire or extreme heat. Dispatch covers declaring one; for scheduling, the effect is that the date becomes an exclusion, and nothing can be booked or rescheduled onto it.
- Seasonal pauses, expressed as rules that don't cover those months, as above.
Exclusions don't move targets. A target that falls on the Fourth of July is still due that week; the scheduler simply can't place it on that day. If exclusions eat the whole window (a week of snow inside a tight window), the subscription surfaces as overdue with a weather reason, which is accurate. Don't silently widen the window to make the numbers look better.
Batch Scheduling#
Customers book some visits themselves and call in for others, but most recurring visits are placed by the system. Batch scheduling is a job that walks every subscription due soon and books it into real capacity. It runs per branch, on that branch's own schedule and time zone, because a branch's "tonight" is its local night and its routes are built in local days (Time). A typical run:
- Select the subscriptions whose window opens within the search horizon (two weeks is a reasonable default), oldest window first.
- Exclude any that already have a pending or confirmed appointment, a completed visit inside the current window, or a visit completed today. Add a duplicate-prevention window as well, so that two runs, or a run racing a customer's own booking, can't book the same cycle twice.
- Search for slots inside the window, applying everything Territories & Capacity describes: the area the property is in, technicians with the skills and licenses for the products, route capacity, the per-technician daily cap, and the customer's preferences.
- Book the best slot, or skip with a reason.
Skips are the useful output. A run that books 900 visits and skips 140 without saying why is a run nobody can act on. Record a reason on every skip:
| Skip reason | Meaning | Who acts |
|---|---|---|
no_users |
No technician works the area on any day in the window | Branch manager: staffing |
no_areas |
The property doesn't fall in any serviceable area | Operations: territory data |
no_availability |
Technicians exist, but every route is full or locked | Branch manager: capacity |
flag |
An account alert blocks automatic booking (a billing hold, "call first") | Customer service |
technician_limit |
Every candidate technician hit the daily cap | Routing |
preferences |
Slots exist, but none match the customer's preferences | Customer service: call the customer |
Customer preferences (mornings or afternoons, certain weekdays, a preferred technician) are soft constraints in batch scheduling. Try them first, and fall back to any slot inside the window before giving up, because a visit on a non-preferred day beats a missed cycle. Record that the preference wasn't met, so the reminder can say so and the customer can move it.
The job must be idempotent: running it twice books nothing twice. Running it often is fine; missing a night must be harmless, so the next run picks up the same work (Background Jobs). Keep a sweep for the opposite failure too: active subscriptions with no booked visit and no visit in a long time, which usually means the rules or the anchor are broken, not that the customer is happy.
What This Means for the System You Build#
- Cadence stored as recurrence rules on the subscription, copied from the plan at signing, with non-overlapping seasonal rules and at least one rule always.
- Each rule with its tolerances, source and consent reference.
- Next target, window and booked appointment kept current on the subscription by events, and reproducible from the rules and visit history.
- Anchors that only regular visits move, pointing to the visit they came from.
- A service compliance log that classifies every completion against its window, once, at completion.
- A backlog by tier, queryable per branch, with regular capacity snapshots.
- Cancellation dispositions with outcome, consent and a snapshot of the state policy in force.
- Branch-level exclusions for holidays and weather that block booking without moving targets.
- Batch scheduling per branch, in local time, idempotent, with duplicate prevention and a reason on every skip.
References#
Specifications
- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar), the RECUR value type (FREQ, INTERVAL, BYMONTH, BYDAY), DTSTART as the first instance, EXDATE and RDATE, and the "every other week" example
Reference
- EPA: Introduction to Pesticide Labels, "the label is the law", including how often a product may be used
In practice
- python-dateutil: rrule, an implementation of the RFC's recurrence rules, with
rrulesetexclusion dates and exclusion rules