Scheduling a regatta By hand, by solver, by agent
Three attempts at scheduling a rowing regatta: by hand, with a constraint solver, and with a coding agent. The first two failed in different ways. The third worked, and the process is now written down as a skill.
How do you keep every rower in a 200-boat rowing regatta from being in two places at once? That, mostly, is what it means to schedule a regatta.
For the past few years, I’ve been the registrar for two regattas: the Stonewall Regatta for DC Strokes Rowing Club and Capital Sprints for Capital Rowing Club, both raced out of the Anacostia Community Boathouse in Washington, DC.
| Flight | Time | Event | Lane | Description | Club | Age | Hcp |
|---|---|---|---|---|---|---|---|
| 47 | 8:44 AM | 8 | 1 | Mens Masters 4x | Washington Rowing School | 65 | 28.9 |
| 47 | 8:44 AM | 8 | 2 | Mens Masters 4x | North Carolina Rowing Center | 59 | 21.8 |
| 47 | 8:44 AM | 8 | 3 | Mens Masters 4x | Potomac | 46 | 7.2 |
| 47 | 8:44 AM | 9 | 4 | Womens Masters 4x | Baltimore | 38 | 2.4 |
| 57 | 9:08 AM | 13 | 2 | Mixed Masters 4+ | Gray Knights | 52 | 13.5 |
| 57 | 9:08 AM | 13 | 3 | Mixed Masters 4+ | D.C. Strokes | 37 | 2.2 |
In a sprint regatta, races go off in flights: a group of boats starts together, then another group a few minutes behind it, then another, all day long. The woman racing a single at 9:00 is also stroking an eight later in the morning. When her single crosses the line, she has to row back to the dock, get the boat out of the water, find the crew of her eight, launch a different boat, and row up to the start. If the eight is scheduled to race too soon after her first race, she physically cannot be in both races.
Scheduling is a matter of simultaneously satisfying multiple constraints. Some of the constraints are explicit. The course has five lanes, so no flight can hold more than five boats. A flight goes off every six minutes. Rowers need at least 45 minutes between races. Racing stops for lunch and restarts an hour later. And some crews share a physical boat, which has to come off the water from one race before it can launch for the next.
The constraints that actually make the job hard are the implicit ones, the ones nobody writes down and that everybody notices when they break. The schedule should run in roughly the order it ran the previous year, because people build their whole day around it. Morning events should stay morning events. Crews of the same age and class should race near each other, so a fifty-year-old double isn’t lined up against a crew of teenagers. The day should stay compact; nobody thanks you for solving conflicts by extending the regatta. When something has to move, a small move is a favor and a big one is an insult. None of this is written down anywhere, and all of it is what people mean, the week after, when they say the schedule was good.
Add it up and the sheet turns adversarial. Move one event to fix a conflict and you’ve usually created two more, three flights away, between crews you weren’t looking at. Keeping people out of two places at once turns out to be the easy half; the hard half is doing it without breaking anything people quietly expect. It took me three attempts over a few regattas to manage both.
First attempt: by hand
My scheduling method on the first attempt was hope and hubris. How hard could it be? I put the events in a sensible order, spaced them evenly, and waited.
The conflict detection system was email. Entrants would write to say, more or less: our stroke seat is in events 14 and 16, you have to move us. So I’d drag 16 later, which pushed it too close to 18, which had its own doublers, and someone else would write. Every fix was a new conflict I couldn’t see, because seeing it meant holding every roster in my head at once. I got through the regatta this way. I would not call it scheduling so much as apologizing in bulk. The end result was a schedule that was good enough. Some rowers simply had to live with the conflicts and drop out of some of their races. Not ideal.
Second attempt: by solver
The second time, I did what a programmer does after a regatta’s worth of apologizing: I wrote code. I rebuilt the schedule as a constraint model in OR-Tools, Google’s optimization library. The shape of it is that every flight gets a start-time variable, every boat gets a lane, every rule becomes a constraint, and the solver hunts for a schedule that satisfies all of them at once. Unlike me, it could hold the whole problem in view.
The explicit constraints went in first, and even the easy ones weren’t cheap. The plainest one, that the course can only hold so much racing at once, meant creating a boolean for every flight at every minute of the race day. Thousands of tiny switches for one sentence of regatta common sense. The rule I cared about most, keeping one rower’s races apart, took several tries before it started actually working. But all of it went in, and the solver happily accepted it all.
The implicit list is where the project died. I still have the file, and it reads like a graveyard. Singles race before lunch: written, then commented out. Open and Masters eights on opposite sides of noon: the same. Keep crews of the same age near each other: never even attempted. Each new rule fought the others, the model kept coming back infeasible, and the script still prints the solver’s infeasibility report from my trying to understand why.
I commented constraints out one by one until the whole thing was feasible again. We raced what was left.
What was left worked, for the definition of “worked” still in the model. Conflicts went to nearly zero. It just also shredded the schedule to get there. The flights ran in the only order left in the model: a lone single wedged between eights, races that had always run before lunch banished to mid-afternoon, similar crews scattered to opposite ends of the day. The solver had satisfied exactly what I asked for and nothing I meant.
I can only write the list of implicit constraints down today because I watched that schedule violate it item by item: the running order, the morning-ness of mornings, the ages racing together.
Third attempt: by agent
On the third attempt I skipped the code. I opened Codex, pointed GPT-5.5 at the three tables that define the problem (the boat entries, the roster of who’s in each boat, and the live heat sheet in Google Sheets), and told it what I wanted in plain language. It turned into one long conversation that I kept reopening for a week as entries and changes trickled in.
The hard constraints were the printable list: the lanes, the conflict window, the lunch break, no boat entered twice. The soft ones, the ones that killed the solver, I could finally just say: stay as close to the existing schedule as you can. Keep events on their side of lunch. Keep similar crews together. When something has to move, move the smallest thing the smallest distance.
The agent did not shred anything. It treated the existing schedule as a baseline to preserve, and came back with small batches of moves, each with a reason attached. Its analysis of my first draft found 66 conflicts. Applying the constraints took that to 15, then 7, then 2, and the last 2 were the same two rowers from one family (brothers) — an overlap I accepted rather than tear up the afternoon to fix. When a batch of new entries landed three days later, we rebuilt the whole sheet: 149 boats, zero conflicts. We could even haggle over the rules themselves: what would the conflict count look like with a 40-minute window instead of 45, or with 35?
It surfaced tradeoffs instead of deciding them. It found no conflict-free way to keep the men’s masters eights scheduled after lunch without letting the afternoon sprawl, so it laid out the options, and the cross-lunch moves became a trade-off I was actively aware of. It also caught things no optimizer would have, because they weren’t in the objective: an athlete entered twice under two different birthdates, a lineup seat held by someone named “Tbd Tbd”.
That last catch is the real difference. The solver could only ever satisfy the constraints I managed to write. The agent could be told the parts I had failed to formalize, and it could push back.
Next regatta: by skill
After the third attempt I spent an evening doing the one thing I’d never done: I wrote the process down. It’s now a skill: a markdown playbook the agent loads before touching a schedule. It encodes the workflow: validate the entries before scheduling anything, satisfy the hard constraints first, optimize the soft ones second, and verify by re-reading the final sheet rather than trusting memory. It records the domain traps. A 4x has eight oars and a 4+ has four, so combining them into one flight makes for lopsided racing (but a 4x and an 8+ might work). And it carries the guardrails: never silently accept an athlete conflict; fix it, get it explicitly accepted, or report it unresolved.
Before the next regatta, the entries will export, the lineups will trickle in, somebody’s stroke seat will be double-entered in two events half an hour apart, and I will not open a spreadsheet and start dragging. I’ll hand the agent the files and the skill and be done with my job a few minutes early.
That’s the part of this that stays with me. On the first attempt, the scheduling knowledge lived in my head, and it leaked out through apology emails. On the second, the fraction I could write as constraints got trapped in a solver’s model. On the third, all of it went into a conversation. And conversations evaporate. The skill is the first version that accumulates. Every mistake the agent and I catch at the next regatta becomes a line in the playbook, and the regatta after that starts from there. I spent three regattas trying to produce a better schedule, and the thing that finally worked was producing a better scheduler.