Case Study

Winterization Scheduling and Routing

Role
AI Specialist, SunCo Lawns (Omaha, NE), a family-owned lawn and irrigation company
Timeframe
July to September 2026, in production for the October to November 2026 season
Status
Live. Season results due December 2026.

The problem

Every fall, SunCo shuts down roughly 2,000 residential sprinkler systems before the first freeze. The season was stuck at that number. In fall 2025 the company ran 1,972 jobs between September 17 and November 17, peaking at 111 in one day, while technician capacity was clearly higher.

The bottleneck was scheduling, not labor.

  • One irrigation CSR hand-scheduled the whole season, grouping routes by zip code. A caller asking for a shutdown often got "we'll call you back" instead of a date. In a market where five companies offer the same service, that's a lost sale whenever a competitor answers first.
  • Routes grouped by zip meant technicians lost hours to drive time between stops that looked close on paper and weren't.
  • The capacity math lived in one person's head and a personal document, and it broke on large properties.

Ownership wanted 2,500 to 2,700 customers in the same window. That's not a "work harder" gap. It needed a system.

What I built

Two pieces, shipped in sequence.

1. A route clustering engine (shipped August 24, 2026)

A Python command-line tool that:

  • geocoded every address on the shutdown list (1,817 addresses, zero failures),
  • priced a real drive-time matrix from Google's routing API (39,405 pairwise drive times, about $218 against a $500 ceiling), and
  • grew route "buckets" by nearest neighbor, stopping when the next stop would put any two stops in the route more than 12 minutes apart.

Output: 107 routes covering all 1,811 active stops, zero dropped, zero over capacity. Worst drive between any two stops on any route: 12.9 minutes. Median: 8 minutes.

2. A CSR booking app (live September 2, 2026; rolled out to six CSRs September 15)

A web app where a CSR types a caller's address while they're on the phone. The app returns the routes that can take that address within the 12-minute rule, each with its date and remaining room, and she books it on the call. A season board shows every route and date at a glance.

  • Stack: React and Vite, Firestore, Firebase Hosting, installable as a desktop app. No server code, so it runs on the free tier and has no dependency on any office machine during live calls.
  • Address input is Google Places autocomplete, restricted to the service area, so a mistyped address can't silently book someone in another state.
  • Each lookup does a free straight-line filter first, then pays for real drive times only against the handful of routes that survive. About seven cents per booking instead of dollars.
  • An append-only booking log records every placement and move from day one.
  • The app holds no customer names, phone numbers or emails. It answers one question, "where does this address fit," and the CSR records the booking in the company's main system afterward.

It became the system of record for the season on September 15.

Screenshots 1
Placeholder: screenshot 1
Screenshots 2
Placeholder: screenshot 2
Screenshots 3
Placeholder: screenshot 3

What was mine

I ran the project end to end: scoping with operations and ownership, the design decisions below, and every piece of rollout and communication. My self-hosted coding agent wrote the code. The decisions that made it work were mine:

  • Capacity is a ceiling, never a target. The first version of the clustering engine packed routes toward a capacity number, which glued distant stops together to hit a total. I caught it and rewrote the rule: grow by proximity, stop when the next stop is too far, and treat a half-full route as finished on purpose. That one change is the reason the routes are drivable.
  • Underfilled routes are deliberate. They're the open slots that call-ins fill during the season, not defects. The quality checks stopped flagging them.
  • Test placement against every stop, not a representative point. The agent's first plan checked a caller against one "anchor" per route. That would admit a caller four minutes from one edge of a route and sixteen from the other. I changed it to a cheap straight-line prune followed by real drive times against every member of the surviving routes.
  • Stay on one routing provider. The agent proposed a cheaper engine for the live lookups. The routes were built on Google drive times, so measuring the 12-minute rule on a different engine would make the two layers mean different things. The saving was about $50. Not worth it.
  • Log from day one. October can't be logged retroactively, and the log is how we'll know in November whether the "customers don't pick their day" policy held.
  • No contact information in the app, ever. Even after data-handling approval came through, because "approved" is exactly when that rule gets quietly dropped.

What came from others, and mattered: ownership moved the season start from mid-October to October 1 after checking what competitors did, which is what made 2,700 reachable at all. The operations director set the capacity numbers (110 target, 135 ceiling per route) and insisted on soft caps. The CSR lead killed my plan to pre-split zip codes across dates, and she was right: splitting risks losing the customer entirely if neither date works.

Three problems worth knowing about

  • A "barrier detector" that was wrong. I'd used the ratio of road distance to straight-line distance to catch rivers and highways between stops. Dense residential neighborhoods broke it: cul-de-sacs produce ratios of 5 to 40 with no barrier at all. I relaxed it to a weak secondary signal. The real guardrail is the 12-minute maximum.
  • Smaller routes didn't leave more room. I tested lower capacity caps on the theory that smaller routes reserve more space for call-ins. The opposite happened: more routes means more of them start pre-loaded with existing customers. A cap of 95 left room for about 501 new customers; 135 left room for about 530. The constraint was physical density, not the algorithm.
  • A 600-row gap that looked like stale data. For weeks the shutdown list came up ~1,215 when it should have been ~1,800. It was a service-type filter: 520 blank rows, 76 mid-season checks, a few repairs. Finding that meant reading the export, not trusting the summary.

Results

To be measured, December 2026:

  • Jobs completed against the 2,700 target.
  • Share of call-ins booked on the phone (ownership's success metric: 95%).
  • CSR scheduling hours, before and after.

I'm not projecting these. Seasonal work has too many variables (weather, staffing, customer growth) to claim a number before it exists.

What I'd do differently

  • Put the clustering engine under version control from the start. The shipped routes aren't reproducible from a commit. I accepted that risk deliberately in August, and it was the wrong trade. Code, config and the drive-time cache should have been pushed to a remote on day one.
  • Real login from day one. The app launched behind a shared password. That's a client-side gate, not access control, and the app holds customer addresses. Moving it to company Microsoft login is the first post-season change.
  • Ask the agent for artifacts, not status. Twice its reported progress ran ahead of reality. "Show me the file path and a row of output" fixed it both times. I'd make that the rule from the first day, not the third week.
  • Design day-of replanning. Weather and cancellations will reshuffle routes mid-season, and the app has no answer for that yet.