Pro move: hand this prompt to your agent.
Copy handoff →

A free walkthrough

A senior resident asking r/Residency how they make their call schedules. Title: For all the senior/chief residents, how do you make the call schedules at your program? 28 upvotes, 13 comments.
The question. r/Residency, 3 months ago.
A reply on the post saying: As someone who did this, there is no replacement for just fucking getting shit done on Excel. No AI is going to do a better job. 60 upvotes.
The top reply on that thread. 60 upvotes — more than the original post.

How to make a call schedule with AI that doesn’t hallucinate.

That reply is half right. ChatGPT alone won’t build you a fair call schedule. It will gladly invent one that looks fine and quietly skips a vacation request. But ChatGPT plus a compact constraint solver will — deterministically, in seconds. And the whole thing is free.

I built this for the senior resident, the chief, the fellow, the one partner in the group, the person who got handed the spreadsheet and is now spending every other weekend trying to make it fair. If that’s you, follow along. If it’s someone you know, send them this page.

Full disclosure: I’m the founder of NiceSchedule. The walkthrough below is the free, DIY version of what we build for anesthesia groups that need the schedule run for them.

The distinction

Why ChatGPT alone keeps failing at this.

Once upon a time, if you asked an LLM how many r’s were in strawberry, it would confidently tell you two. Meanwhile a one-line Python program would just count the string and return three. That is the difference between a plausible answer and a checked answer.

Your call schedule needs the checked answer. A language model predicts the most likely next token; that is genuinely great for translating messy human rules into something more precise. It is the wrong tool to be the schedule. It will produce a calendar that looks reasonable and quietly skip a vacation, double-book a post-call physician, or leave a weekend uncovered.

A constraint solver works differently. You hand it variables, hard rules, and what to prefer. It searches for assignments that satisfy the rules. If the rules conflict, it tells you the schedule is infeasible instead of inventing one.

The right pattern: use AI to help write the constraints. Use a solver to make the schedule.

This is the part most "just use ChatGPT" posts miss. ChatGPT is not the scheduler. The solver is the scheduler. ChatGPT is the helper you use to write the solver, clean the inputs, explain errors, and add one rule at a time.

The v1 rule set

What the sample solver actually does.

I kept this intentionally small. It is not a production anesthesia scheduler. The point is to prove the workflow runs end-to-end before adding real-world complexity.

  1. Cover every required shift in coverage.csv.
  2. Respect shift eligibility in clinicians.csv.
  3. Block hard vacation and no-call requests in requests.csv.
  4. Assign each clinician to at most one shift per day.
  5. Enforce a minimum rest gap between assignments.
  6. Prefer fair total assignment counts.
  7. Prefer fair weekend assignment counts.
  8. Use prior history.csv assignments as part of the fairness picture.
  9. Prefer more rest spacing when the hard rules leave room.
That is enough to make a useful dummy schedule. Real groups usually add holiday rotation, partner-vs-non-partner allocation, site-specific coverage, post-call recovery, locked assignments, and local exceptions. The agent handoff section below shows how to layer those in one at a time.

Inputs

The four CSV files

The solver reads structured CSVs instead of free-form text. That is what keeps the schedule grounded — the AI never gets to invent who exists or who is on vacation.

clinicians.csv

Who can be scheduled, which shifts they cover, target counts, max counts, and rest rules.

coverage.csv

The demand file. Each date, each shift type, and how many people are required.

requests.csv

Vacation, no-call requests, and soft preferences. Hard requests block; soft requests pay a penalty.

history.csv

Prior assignments. Used to make the new schedule fair against recent history.

File Example row Meaning
clinicians.csv fox,A. Fox,1,1.0,1,1,5,6,2,3,1 A. Fox is active, can cover OR and OB, targets 5 shifts, maxes at 6, targets 2 weekend shifts.
coverage.csv 2026-06-01,OB,1 June 1 needs one OB clinician.
requests.csv r1,cary,2026-06-05,2026-06-07,vacation,1,,Family trip M. Cary cannot be assigned to any shift from June 5 through June 7.
history.csv 2026-05-02,fox,OR,final A. Fox covered OR on May 2 — counts toward fairness history.

Full column-by-column schema is in docs/csv-schema.md.

First run

Run the dummy data before touching your own data.

Do not debug Python, CSV formatting, and your real scheduling rules at the same time. Run the sample exactly as-is first, see the schedule come out, then adapt.

Shortcut for Claude Code & Codex users: skip the manual steps. Open the agent handoff block below, paste it into your agent, and let it do the install and the first solve. Come back here to read what it’s doing.

First, clone the repo:

git clone https://github.com/rumblelab/ai-call-scheduler.git
cd ai-call-scheduler
01

Create a virtual environment

python3 -m venv .venv
02

Install OR-Tools

.venv/bin/python -m pip install -r requirements.txt

On Windows, the activate path is different. Use .venv\Scripts\python wherever you see .venv/bin/python.

03

Run the solver

.venv/bin/python solver.py
04

Inspect the output

Status: OPTIMAL
Wrote 28 assignments to output/sample_schedule.csv

Open output/sample_schedule.csv for the raw data, or open output/sample_schedule.html to see the beautiful, printable grid version.

Don’t move to real data until the dummy schedule works. When you do move to real data, replace names with IDs first.

The output

Here’s what comes out of the solver.

Below is the actual output/sample_schedule.csv the sample produces, laid out as a real call calendar. Two weeks. Six clinicians. One OR shift and one OB shift per day. Status: OPTIMAL. 28 assignments. Every hard constraint respected.

Sample schedule — June 1–14, 2026
Clinician
Mon 1
Tue 2
Wed 3
Thu 4
Fri 5
Sat 6
Sun 7
Mon 8
Tue 9
Wed 10
Thu 11
Fri 12
Sat 13
Sun 14
M. Cary
·
OB
·
OR
VAC
VAC
VAC
OR
·
·
·
·
OR
·
A. Fox
·
OR
·
·
·
OB
·
·
OR
·
OR
·
·
OR
R. Aken
·
·
OR
·
·
OR
·
·
·
OR
·
OR
·
·
C. Johns
·
·
OB
·
OB
·
·
OB
·
·
OB
·
·
OB
J. Judy
OR
·
·
·
OR
·
OR
·
·
OB
·
·
OB
·
S. Reed
OB
VAC
·
OB
·
·
OB
·
OB
·
·
OB
·
·

A few things worth pointing out, because they prove the solver actually respected the rules:

  • Cary’s vacation June 5–7 is honored. Zero assignments those days.
  • Reed’s hard block on June 2 is honored. Reed has zero assignments that day — even though Reed covers five other shifts in the window.
  • Johns asked off OB on June 10 only (shift-specific, not a full day). The solver honored it — Johns has no OB shift on June 10 — without blocking other shift types. That distinction matters for real groups.
  • Weekend calls are spread across the group. Fox and Judy take two weekend shifts; the others take one.
  • Total counts are within 1 of each clinician’s target. No one is "the one who always covers."

That is the holy-shit moment, if you spent last weekend doing this in Excel. It came out of a single Python file in seconds — and this is the exact grid you’ll get when you run solver.py on your own machine.

Demand generation

coverage.csv is the file you actually have to fill out.

Of the four CSVs, this is the demand side. If a row exists, the solver tries to staff it. If a row is missing, the solver does not know that shift exists. So this is the file you have to think about carefully.

That said: don’t type it by hand. If your coverage pattern is regular, have your agent write a small script that generates it.

Tell an agent something like this
Create a Python script that writes coverage.csv for June 2026.
Rules:
- Every Saturday and Sunday needs one NORTH call and one SOUTH call.
- Weekdays do not need call coverage in this example.
- Output columns: date, shift_type, required_count.
- Dates in ISO format (YYYY-MM-DD).

It will produce rows like:

date,shift_type,required_count
2026-06-06,NORTH,1
2026-06-06,SOUTH,1
2026-06-07,NORTH,1
2026-06-07,SOUTH,1

The same pattern handles:

  • one OB call every day
  • two OR calls on weekdays, one OR call on weekends
  • different sites: NORTH, SOUTH, OB
  • holiday-specific call requirements
  • higher required counts on known heavy days
When you add a new shift type to coverage.csv, add a matching eligibility column in clinicians.csv. For example, shift type NORTH expects can_north. If you forget, the solver fails fast with a clear error — see docs/troubleshooting.md.

The messy part

Turning messy emails into requests.csv.

The biggest barrier isn't the solver — it's the data entry. You have a folder full of "Please give me June 12th off" emails and a PDF of the resident roster. Don't type them into the CSV by hand.

This is where ChatGPT or Claude excels. Give them the messy text and ask for the structured CSV.

Paste this into ChatGPT or Claude
I am using a call scheduler that needs a requests.csv file.
Columns: request_id, clinician_id, start_date, end_date, request_type, hard, shift_type, note.

Here is a list of emails from residents:
"[Paste messy email text here]"

And here is our clinician roster:
"[Paste name list here]"

Please generate the CSV rows.
Use clinician_id from the roster.
Assume hard=1 for vacation, hard=0 for 'prefer off'.
Leave shift_type blank to block all shifts that day; fill it in (e.g. OB) to block only one shift type.
Dates in ISO format.

Once you have the CSV, save it to data/my_data/requests.csv and run the solver. If the solver says No feasible schedule found., one of those requests made it impossible to cover the shifts. Change a 1 to a 0 in the hard column (making the request soft) and re-solve, or remove the conflicting row.

Customization

Once the sample works, change one thing at a time.

Copy the sample data into your own folder:

cp -R data/sample data/my_data

Copy the sample config:

cp config/sample_rules.json config/my_rules.json

In config/my_rules.json, point at your data:

{
  "input_dir": "data/my_data",
  "output_csv": "output/my_schedule.csv"
}

Run it:

.venv/bin/python solver.py --config config/my_rules.json

Change one file at a time. First clinicians. Then coverage. Then requests. Then history. If the model becomes infeasible, the newest change is usually where the conflict lives — remove that rule first and inspect what changed.

For worked examples of common adaptations — adding a third shift type, locked assignments, weekend-pairing rules — see docs/adaptation-cookbook.md.

Agent handoff

If you have Claude Code or Codex, just paste this.

This whole tutorial is designed so a coding agent can drive it. Claude Code, Codex, Cursor, ChatGPT — any of them can clone the repo, read the docs, run the dummy solve, and then help you adapt one rule at a time.

I’d strongly recommend Claude Code or Codex over a chat-only assistant for this. Local-folder agents can inspect files, run the solver, and see the actual output. Chat-only assistants can only guess at what your CSVs look like.

Copy this block into your agent
I want to use the free NiceSchedule call schedule solver tutorial.

Public walkthrough:
https://niceschedule.com/how-to-make-a-schedule-with-ai/

GitHub repo:
https://github.com/rumblelab/ai-call-scheduler

My goal, in three phases:

PHASE 1 — Prove it works.
Clone the repo and run the dummy solve exactly as-is. Then explain
to me in plain English what the solver did and what the output means.

PHASE 2 — Set it up for my practice.
Once the dummy works, follow docs/new-practice-setup.md to walk me
through configuring my own schedule, step by step:
  - my clinician roster
  - my coverage pattern (use scripts/generate_coverage.py with my
    description, don't make me hand-type the file)
  - my hard vs soft rules (post-call rest, partner allocation,
    weekend pairing, etc.)
  - my vacation and time-off requests (I'll paste whatever format
    I have — emails, a list, a spreadsheet)
  - my first real solve, and a triage of the output before we change
    anything

PHASE 3 — Iterate.
Add one rule at a time. After each change, re-run and tell me what
shifted in the output. If the solver becomes infeasible, walk back
to the most recent change and use docs/troubleshooting.md to diagnose.

Before changing anything, please read these files in order:
- README.md
- docs/scheduler-agent-skill.md
- docs/new-practice-setup.md
- docs/csv-schema.md
- docs/adaptation-cookbook.md
- docs/troubleshooting.md
- docs/agent-privacy.md
- config/sample_rules.json
- data/sample/clinicians.csv
- data/sample/coverage.csv
- data/sample/requests.csv
- data/sample/history.csv
- scripts/generate_coverage.py
- solver.py

Safety rules:
- Don't ask me for real names or schedules until the dummy solve works.
- If you need example data at any point, invent fake data.
- Add one rule at a time and tell me how to test it.
- Per docs/agent-privacy.md, keep this data local. Don't paste real
  schedules into public tools.

Start by running the dummy solve and showing me the output.

Good first asks for your agent

  • "Generate a new coverage.csv for a different month."
  • "Add a third shift type (NIGHT) and the matching eligibility column."
  • "Add a locked_assignments.csv file so I can pin specific people to specific shifts."
  • "Add a rule that nobody works both Saturday and Sunday of the same weekend."
  • "Add a soft preference for spreading holidays fairly."
Each of those is sketched as a worked example in docs/adaptation-cookbook.md.

Privacy

Don’t paste private schedules into public tools.

Even without patient information, physician schedules, vacation requests, and internal group rules can still be sensitive. Start with the dummy data. When you swap in your own, replace names with IDs first.

  • Don’t upload real call history to public GitHub issues.
  • Don’t paste real vacation requests into tools your group hasn’t cleared.
  • Check retention and training settings on whatever AI tool you use.
  • When asking for help, use synthetic examples.

The longer version is in docs/agent-privacy.md.

Where it breaks

This gets hard when the rules become real.

The basic pattern is not magic. The hard part is making it work for a real group: cleaning requests, importing historical fairness, modeling local exceptions, fixing infeasible months, post-call recovery, locked assignments, holiday rotation, secure access, readable schedule views, distribution, and month-over-month maintenance.

That is roughly the moment a DIY script stops being enough. A real schedule needs a place where requests live, where history is tracked across months, where people can be added or removed without breaking previous solves, and where the schedule can be safely shared with the group after it’s reviewed.

If that’s the wall you’ve hit, see how NiceSchedule handles it for real groups — or jump to the cards below.

One last thing

Three ways this is useful to someone right now.

Share it with the resident who’s stuck.

Every program has one. Senior or chief who got handed the spreadsheet, lost the weekend, and is going to be the hero who shows up Monday with a working solver. Every time they use this, they’ll remember the friend who sent it.

Should I host a web version?

If you’d rather not run Python locally — a hosted version with the same solver, a clean form for requests, and a one-click "build my schedule" button — email me and tell me what your group needs. If enough people want it, I’ll build it next.

Email me about the web version

Your group is past DIY?

If you’re running call for a real anesthesia group and don’t want your schedule depending on a script someone vibe-coded, send me your current spreadsheet and the rules in your head. That’s how we onboard. And if you know a doc who’d be saved by this — forward them niceschedule.com.

Send us your spreadsheet