How a fair rota gets drafted
Once a week, by default Thursday at 2pm local time, an EventBridge Scheduler rule fires the drafter Lambda. The Lambda reads the week, sorts the shifts, and fills them one at a time, deciding for each shift who’s the fair, qualified, available pick — or flagging it if nobody fits. The whole decision is plain Python. No model. No vector search. Every setting lives in the rules doc, where the manager can edit it without a deploy.
Key takeaways
- The drafter runs once a week via EventBridge Scheduler, by default Thursday at 2pm local time.
- Settings live in the rules doc — max-hours caps, rest gaps, skill requirements, and how hard to balance hours.
- Four outcomes per shift, every run: filled, fair-swap, short-staffed, held.
- DynamoDB tracks a running hours total per person so the draft balances load as it goes.
- The drafter itself never calls a model. The placement is entirely deterministic.
The decision flow, per shift
Fairness: the hours target isn’t magic, it’s in the sheet
Each person has a weekly hours target in the staff sheet — the full-timer is set to 38, the student to 12, the weekend-only person to 16. As the drafter fills shifts, it keeps a running total of the hours it’s already placed on each person this week. For every open shift, it prefers whoever is furthest below their target. Over a week, that evens the load: nobody ends up with every closing shift while someone else gets nine hours, unless availability genuinely forces it.
The settings exist for a reason. The max-hours cap stops the draft from quietly handing one person a sixth shift. The rest gap (default eleven hours) stops the close-then-open double that burns people out. The skill requirements stop a kitchen shift going to someone who’s never run the kitchen. Different shifts have different needs; the rules reflect that.
Per-person overrides exist too. The staff sheet has an optional column called cap_override. Set a number there and the drafter uses it instead of the default cap for that one person. This is the right escape hatch for the manager who agreed to let a senior staffer pick up extra hours over the holidays.
Four outcomes, always
Every shift, every run, lands in exactly one of four buckets. The names are simple on purpose.
- Filled. One qualified, available person was clearly the fair pick — furthest below their target — and got placed. Their running hours total in the
ss-hoursDynamoDB table is updated. Most shifts, most weeks, land here. - Fair-swap. Two people both fit and both sit close on hours. The shift goes to the one further below target to even things out, and a short note records why. This is the rule that keeps the rota feeling fair instead of arbitrary.
- Short-staffed. A shift has fewer qualified, available people than it needs — everyone cleared is either off, at their cap, or inside a rest gap. The drafter doesn’t pretend it’s fine. It flags the shift for the manager with the reason (“only Priya is cleared and she’s at her cap”) so a human can decide.
- Held. A shift the manager marked to assign by hand — the trial shift for the new hire, the sensitive Saturday the manager wants to place personally. The drafter leaves it open and notes it as held. The manager fills it during the approval step in Part 4.
State that makes the draft deterministic
The drafter reads and writes one DynamoDB table as it goes. ss-hours holds the running placed-hours total per person for the week being drafted: (person_id, week_id, hours_placed). With that table plus the week from the sheet and the settings from the rules doc, the placement logic is a few dozen lines of Python and zero magic. The same week, the same availability, the same settings always produce the same draft. Re-running the draft for a week produces the same rota (the run clears and rebuilds ss-hours for that week each time, so there’s no double-counting).
Approving a draft freezes it: the placements become the published schedule, and the ss-hours totals become the baseline that swaps in Part 5 adjust against. Re-drafting before approval just rebuilds from scratch.
Why the weekly draft uses no model
The drafter could call a model to “figure out a good rota.” It doesn’t. Two reasons. First, the draft should be the one part of the system the manager can fully reason about — if the rules say cap at five shifts and respect an eleven-hour rest gap, that’s exactly what happens, every time. A model in that loop would make “why did Sam get this shift?” impossible to answer cleanly. Second, model calls cost money and add latency, and a rule-based placement is both cheaper and more explainable for a problem this well-defined.
Bedrock fires elsewhere — on the time-off note lane in Part 2, and on the weekly fairness summary mentioned in Part 6. Not on the draft. The drafter itself is plain Python that reads a sheet and writes placements.
Next post: how an approved schedule reaches the team, how quiet hours are honored, and what each person actually sees.
All posts