Build a weekly shift rota for a small team that holds up: collect demand, availability, and contract limits into a constraint matrix, check coverage arithmetic before naming anyone, set fairness rules for nights and weekends in writing, build the rota with a constraint checker, handle swaps and last-minute gaps through a tested approval path, and publish one versioned rota everyone trusts. Ships a rota builder, checker, swap validator, and fairness reporter in Python, a file schema, a constraints and law reference, and a worked two-week rota for a nine-person team. Use when someone asks to build or fix a staff rota, schedule shifts for a shop, cafe, care team, clinic desk, hotel, or support team, share night or weekend shifts fairly, check a rota against rest and hours rules, or approve a shift swap. Not for scheduling meetings across calendars (use the scheduling skill), not for engineering on-call rotations and incident paging (use the on-call playbook), and not for social media posting calendars.
name: shift_scheduling description: "Build a weekly shift rota for a small team that holds up: collect demand, availability, and contract limits into a constraint matrix, check coverage arithmetic before naming anyone, set fairness rules for nights and weekends in writing, build the rota with a constraint checker, handle swaps and last-minute gaps through a tested approval path, and publish one versioned rota everyone trusts. Ships a rota builder, checker, swap validator, and fairness reporter in Python, a file schema, a constraints and law reference, and a worked two-week rota for a nine-person team. Use when someone asks to build or fix a staff rota, schedule shifts for a shop, cafe, care team, clinic desk, hotel, or support team, share night or weekend shifts fairly, check a rota against rest and hours rules, or approve a shift swap. Not for scheduling meetings across calendars (use the scheduling skill), not for engineering on-call rotations and incident paging (use the on-call playbook), and not for social media posting calendars."
Shift Scheduling
Purpose
A small team rota fails in the same few places. Coverage gets counted in heads instead of hours, so the team is short before anyone calls in sick. One person quietly collects the nights and weekends because they never complain. A late shift runs into an early shift with eight hours between them and nobody does the subtraction. A swap gets agreed in a hallway and breaks a skill requirement nobody remembered. Then three versions of the rota circulate and nobody knows which one is real.
This skill builds the rota in the order that prevents those failures: demand first, constraints second, fairness rules third, names last. It ships a builder and checker (scripts/rota.py) that enforce the hard rules mechanically, so the argument about a rota is about the rules the team chose, not about whose spreadsheet is right.
Workflow
1. Write down demand before names
For each day in the cycle, record which shifts run, what time they start and end, how many people each one needs, and which shifts need a skill holder present, for example a supervisor, keyholder, or first-aider. Note the days demand changes: weekends, market days, holidays, events. A post that needs a skill holder is a different post from one that does not, and mixing them is how skill gaps hide.
Set the cycle length. One week is easier to fill, two weeks balances fairer because unpopular shifts have room to spread. The worked example in references/worked-rota.md uses two weeks.
2. Build the constraint matrix
One row per person, collected the same way for everyone, with a deadline before building starts:
- Contract minimum and maximum hours per week. These are the person's real contract numbers, not what they usually end up working.
- Availability: dates they cannot work at all, and single shifts they cannot work. Approved leave and training days go in here, not in a side conversation.
- Blocked shifts: shift types the person never works, for example a no-nights agreement.
- Skills held, spelled exactly as the demand side spells them.
- Preferences, recorded separately from constraints. A preference is a wish the builder can honor when nothing else is at stake. The moment a preference is treated as a hard rule without being written as one, the rota stops making sense to everyone else.
Rules for the whole rota go in the same file: minimum rest between shifts, maximum days in a row, maximum nights in a row, and the publishing notice the team promises. Defaults and where each number comes from are in references/constraints-and-law.md. The honest starting points: 11 hours of rest matches the EU legal floor and published NIOSH guidance, and 3 nights in a row matches NIOSH guidance on consecutive nights. US federal law sets no rest period and no maximum hours for adults, so a US team choosing lower numbers is making a legal but deliberate choice, and it should be written down as one.
3. Run the coverage arithmetic
Before assigning anyone, add it up:
- Weekly demand hours: slots per week multiplied by shift length.
- Weekly supply range: the sum of contract minimums and the sum of contract maximums.
- Demand must sit between the two, with slack. If demand is within a few hours of total maximum supply, one absence breaks the rota, and the fix is more people, fewer posts, or shorter opening hours, not a cleverer arrangement.
For a post covered around the clock, the arithmetic is 168 hours a week for one post, which is 4.2 people at 40 hour contracts before leave and sickness. Add a relief allowance from the team's actual absence record and state the number used. See references/constraints-and-law.md for the formula and a measured example.
If the arithmetic fails, stop and report it with the numbers. A rota that cannot exist on paper will not be saved by effort.
4. Set the fairness rules in writing
Decide, before building, what fairness means for this team:
- Which loads count: nights, weekend shifts, and total hours are the standard three. Add early starts, closes, or onerous posts where the team feels them.
- The window fairness is judged over: the full cycle, not a single week. A person can take the worse week one if week two evens it out.
- The tolerance: how much spread is acceptable before anyone acts. The checker defaults to 16 hours, 2 nights, and 2 weekend shifts across a cycle.
- The remedy: imbalances inside tolerance ride. Imbalances past tolerance become a credit in the next cycle, meaning the person who carried more gets first claim on the lighter load next time. Credits are recorded on the published rota, so they survive a change of manager.
- People blocked from a shift type by agreement sit outside that load's fairness count. Their zero is a rule, not a favor, and counting it as one poisons every comparison.
5. Build the rota
Write the problem as JSON following references/rota-schema.md, then build:
python3 scripts/rota.py build rota-input.json -o rota.json
The builder fills the scarcest slots first, meaning the slots fewest people can legally take, and balances hours as it goes, with night shifts balanced on night counts first. When a slot cannot be filled inside the hard rules, it tries one repair move, shifting a blocking assignment to someone who can take it cleanly. Any slot still open is printed as UNFILLED with the instruction not to publish. An unfilled slot is information: a rule, a contract, or the headcount is wrong, and the fix belongs there.
Then check the result as if someone else built it:
python3 scripts/rota.py check rota.json
The checker enforces coverage against demand, required skills, availability and blocked shifts, overlapping shifts, minimum rest, weekly maximum hours, and the consecutive day and night limits. A rota with hard violations does not get published, softened, or explained away. Fix the input and rebuild.
6. Publish one version
- Publish in one place, in one format, with a version number and the date it was published. Every change after that is a new version. Old versions stay on file, because pay questions and, in some places, the law ask for them later. Los Angeles requires schedule records kept for three years.
- Publish as early as the team's notice rule promises. Where a predictive scheduling law applies, 14 days ahead is the common legal pattern in the US, and late changes can carry predictability pay. Check whether your city or state has one before choosing a shorter promise. References/constraints-and-law.md lists where these laws exist and what they typically require.
- The published rota carries three things alongside the grid: the fairness counts for the cycle, any credits owed into next cycle, and the name of the person who can approve changes. A rota without an approver produces hallway swaps.
7. Handle swaps through the validator
Swaps are where good rotas quietly break. The protocol:
- Workers propose swaps between themselves. Both forms are supported: one person takes a shift outright, or two people exchange shifts.
- Nobody works the swapped rota until it is validated and approved. Test it first:
python3 scripts/rota.py swap rota.json take 2026-11-02 early dee alex python3 scripts/rota.py swap rota.json exchange 2026-11-02 early dee 2026-11-02 late gus
- The validator rebuilds the rota with the swap applied and reports any new violation the swap creates, including ones neither person can see from their own shifts, such as a shift losing its only skill holder. It also prints both people's hours, nights, and weekend counts before and after, so the approver sees the fairness cost, not just the legality.
- A clean swap gets approved, applied to the published file, rechecked, and issued as a new version. A rejected swap goes back with the specific rule it breaks, so the pair can find a different trade instead of arguing about fairness in the abstract.
8. Fill last-minute gaps from a standing ladder
Decide the order before the day someone calls in sick:
- Offer the shift to existing staff first, starting with people under their contract minimum and people owed hours, and checking the validator before confirming anyone. Several predictive scheduling laws require offering hours to current staff before bringing in new people, so this order is both fairer and often the legal one.
- Split the gap: two people cover half each, if both halves pass the checker.
- The supervisor or owner covers it, and records the fact. A manager who regularly covers gaps is hiding a staffing problem the arithmetic in step 3 would have shown.
- Close or reduce the service for that shift, announced early, instead of running short in a way that burns out whoever did show up.
- Record what actually happened against the published rota. In places with predictability pay, an employer-initiated late change is a pay event, not just a diary entry. Worker-initiated swaps are usually treated differently, which is one more reason to keep the two apart in the record.
9. Close the cycle and carry the credits
At the end of each cycle, run the fairness report on the rota as actually worked, including swaps and gap fills:
python3 scripts/rota.py fairness rota.json
Compare it with the published plan. Differences between planned and worked fairness are the true credits owed, and they go into the next cycle's input as a note the builder's operator honors when checking the new rota's fairness output. Then archive the final version with its change history.
Output Contract
Deliver, in this order:
- Demand summary: shifts, times, headcount per shift, skill-gated posts, and how demand varies across the week.
- Constraint matrix: per person, contract minimum and maximum hours, unavailable dates, blocked shifts, and skills, plus the rota-wide rules with the source or choice behind each number.
- Coverage arithmetic: weekly demand hours, supply range, slack, and the relief allowance used, or a plain statement that the arithmetic fails and by how much.
- Fairness rules: which loads count, the window, the tolerance, and the credit remedy.
- The rota file and its checker output: the PASS line, or every violation with the fix applied.
- Publishing record: where the single version lives, its version number and publish date, and whether it meets the notice promise.
- Swap and gap protocol for this team, with the validator commands, and the credits carried into the next cycle.
Operating Rules
- Never present one jurisdiction's law as a general rule. Scheduling law varies by country, state, city, industry, and employer size. Name the place every time a legal number appears, and tell the user to confirm the current local rule for anything they will rely on.
- Keep the three kinds of number separate: law, published safety guidance, and team planning choices. The reference file labels each one. A default in the checker is a choice, not a finding.
- Names go on the rota last. A rota built by placing favorite people first and checking rules after will fail the checker and deserve to.
- No hard violation gets published. Not for a deadline, not because the person affected said it is probably fine. Consent-based exceptions, where the local law allows them at a premium rate, are a written management decision made outside the checker, and the published rota records them as exceptions.
- Preferences never silently become constraints, and constraints never silently get ignored. Both changes happen in the open, in the file, with a new version.
- Count fairness over the cycle and fix it with credits. Rewriting a published rota to perfect the fairness counts teaches the team that the published version means nothing.
- Keep the rota data minimal. Names or staff ids, skills, availability, and contract hours are what the job needs. Health reasons behind an availability block stay out of the file; the block itself is enough.
- When the arithmetic says the team is too small, say so with the numbers and stop. Producing a rota that only works if nobody is ever sick is a false promise, not a schedule.
No ratings yet — be the first to rate it.