Nurse Scheduling System¶
A flexible web application designed to streamline and automate nurse scheduling, suitable for a wide range of diverse and complex real-world requirements.
- Stable version (frontend-only) hosted on Netlify.
- Development version hosted on Netlify.
- Documentation hosted on Netlify.
- Source code hosted on GitHub.
Introduction¶
The nurse scheduling (or employee scheduling) problem is a well-known problem in the field of operations research (OR) and can be (approximately) solved efficiently by constrained optimization.
However, constraints can differ greatly between hospitals and wards, and there is currently no unified framework for modeling these diverse requirements. Most existing literature focuses on modeling an over-simplified constraint set, which is not applicable to real-world situations. Therefore, in practice, the problem is still often solved by hand with the help of Excel, which is often extremely time-consuming. The entire process requires several hours or even more than ten hours, depending on the problem complexity (e.g., co-scheduling of multiple understaffed wards).
This project (Nurse Scheduling System, or 護理排班系統 in Mandarin) aims to develop a flexible web app to automate the nurse scheduling task, and to provide a unified framework for modeling all types of real-world scenarios without sacrificing flexibility.
This project is in active development. Breaking changes may occur without notice. Please proceed with caution. Although the current version has been verified by domain experts and used successfully (with minimal post-adjustment) in several complex multi-ward scenarios involving up to ~100 nurses, it currently has a steep learning curve and lacks proper documentation.
Privacy Notice¶
This early work-in-progress project provides basic privacy protections, including anonymizing individual people IDs, removing descriptions where possible, and privacy-masking Sentry session replays. The hosted application uses analytics and error reporting, and sends scheduling data to the selected backend when you click Optimize. Ad blockers may block analytics and error reporting, but not optimization submissions. Do not submit sensitive information. See Privacy and Data Handling for details.
How to run¶
Prerequisites¶
- bun (for frontend development).
- uv (for backend development).
- Docker (optional, for Docker-based development environment and GPU solver).
- NVIDIA Container Toolkit (optional, for GPU solver).
These are not hard requirements. If you know what you are doing, you can also use other tools to manage dependencies, such as nvm/npm for Next.js, and virtualenv or conda for Python.
Quick Start¶
Clone the repository:
Linux (bash/zsh)¶
Start frontend:
In a new terminal, start backend:
cd core
uv venv --python 3.12
source .venv/bin/activate
uv pip install -r requirements.txt
fastapi dev nurse_scheduling/serve.py
macOS (bash/zsh)¶
macOS support is experimental.
Start frontend:
In a new terminal, start backend:
cd core
uv venv --python 3.12
source .venv/bin/activate
uv pip install -r requirements.txt
fastapi dev nurse_scheduling/serve.py
Windows (PowerShell)¶
Windows OS support is experimental.
Start frontend:
In a new terminal, start backend:
cd core
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
uv venv --python 3.12
.venv\Scripts\Activate.ps1
uv pip install -r requirements.txt
fastapi dev nurse_scheduling\serve.py
Linux Development and Docker¶
The commands below are Linux-focused reference material for setup, testing, and Docker.
For Linux only: to quickly set up all local environments (core, web-frontend, and docs) in one go, run:
For Docker-based development environment:
CPU solver:
# persist Codex/Claude Code/OpenCode auth/config across containers
mkdir -p ~/docker/.codex
mkdir -p ~/docker/.claude
touch ~/docker/.claude.json
mkdir -p ~/docker/opencode/.config/opencode
mkdir -p ~/docker/opencode/.local/share/opencode
# mount project files and Codex/Claude Code/OpenCode config
docker run --rm -it --network=host \
-v $(pwd):/app \
-v ~/docker/.codex:/root/.codex \
-v ~/docker/.claude:/root/.claude \
-v ~/docker/.claude.json:/root/.claude.json \
-v ~/docker/opencode/.config/opencode:/root/.config/opencode \
-v ~/docker/opencode/.local/share/opencode:/root/.local/share/opencode \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
j3soon/nurse-scheduling:dev
GPU solver:
# or build image with cuOpt support
docker build -f docker/Dockerfile.cuopt -t j3soon/nurse-scheduling:dev-cuopt .
The cuOpt image omits highspy because the pinned release has no CPython 3.14
wheel. Use another environment for the pulp/highs solver.
# persist Codex/Claude Code/OpenCode auth/config across containers
mkdir -p ~/docker/.codex
mkdir -p ~/docker/.claude
touch ~/docker/.claude.json
mkdir -p ~/docker/opencode/.config/opencode
mkdir -p ~/docker/opencode/.local/share/opencode
# mount project files and Codex/Claude Code/OpenCode config
docker run --rm -it --gpus all --network=host \
-v $(pwd):/app \
-v ~/docker/.codex:/root/.codex \
-v ~/docker/.claude:/root/.claude \
-v ~/docker/.claude.json:/root/.claude.json \
-v ~/docker/opencode/.config/opencode:/root/.config/opencode \
-v ~/docker/opencode/.local/share/opencode:/root/.local/share/opencode \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
j3soon/nurse-scheduling:dev-cuopt
Inside either development container, start Redis and run the backend in Redis mode:
redis-server --daemonize yes
redis-cli ping
cd /app/core
JOB_BACKEND=redis \
JOB_REDIS_URL=redis://localhost:6379/0 \
JOB_REDIS_KEY_PREFIX=nurse_scheduling:jobs:v0 \
uvicorn nurse_scheduling.serve:app --workers 3 --host 0.0.0.0 --port 8000 --no-access-log
Workers renew a 90-second presence lease while idle and optimizing. Set
JOB_WORKER_LEASE_SECONDS to change how long the server waits before marking a
worker offline and failing its active job.
or with X11 forwarding for running Playwright interactive mode in the container:
xhost +local:docker
mkdir -p ~/docker/.codex
mkdir -p ~/docker/.claude
touch ~/docker/.claude.json
mkdir -p ~/docker/opencode/.config/opencode
mkdir -p ~/docker/opencode/.local/share/opencode
# mount project files and Codex/Claude Code/OpenCode config, and forward X11 display
docker run --rm -it --network=host \
-v $(pwd):/app \
-v ~/docker/.codex:/root/.codex \
-v ~/docker/.claude:/root/.claude \
-v ~/docker/.claude.json:/root/.claude.json \
-v ~/docker/opencode/.config/opencode:/root/.config/opencode \
-v ~/docker/opencode/.local/share/opencode:/root/.local/share/opencode \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix \
j3soon/nurse-scheduling:dev
May need to run
rm -rf .nextinweb-frontendto clear the Next.js cache when switching between host and Docker environments.
Web Frontend¶
The commands below are tested on Linux only.
Run frontend unit/component tests:
Run frontend coverage:
Run frontend browser integration tests:
cd web-frontend
bunx playwright install-deps chromium
bunx playwright install chromium
bun run test:e2e
# or in interactive UI mode:
bun run test:e2e:ui
When using the repository docker/Dockerfile, Chromium is preinstalled in the image at
build time using the frontend's locked Playwright version. If you rebuild the
image after Playwright version changes, bun run test:e2e and
bun run test:e2e:ui should not require rerunning bunx playwright install chromium
inside each new docker run --rm container.
For the interactive UI mode, you may need to run the tests multiple times to get it passed, as the test is currently somewhat flaky. This is due to the delay of page update and is planned to be fixed in the future.
In GitHub Actions, frontend browser integration tests run after frontend unit/coverage tests. The workflow uploads Playwright reports as build artifacts so failed CI runs keep browser traces and reports for debugging.
Generate a separate browser-flow coverage report from Playwright:
This writes a separate report under web-frontend/coverage-e2e/ and does not replace the main Vitest coverage report under web-frontend/coverage/.
For building static site, run:
For linting, run:
buncan be replaced directly withnpmfor the basic Next.js workflow, but the documented project scripts assume Bun.
Core¶
We currently support thirteen solver selectors across OR-Tools and PuLP.
All backends other than OR-Tools/CP-SAT are experimental.
ortools/cp-satis the default solver and the most battle-tested one.ortools/mpsolver/cbcuses CBC through the OR-Tools linear MIP API and is covered by the normal schedule regression suite.ortools/mpsolver/scipandortools/mpsolver/cp-satuse SCIP and CP-SAT through the OR-Tools linear MIP API and are covered by the normal schedule regression suite.ortools/mpsolver/bopuses the legacy BOP engine. It has low-level and bounded schedule smoke coverage, but is not recommended for larger schedules because it can be substantially slower.ortools/mathopt/gscip,ortools/mathopt/cp-sat, andortools/mathopt/highsuse the bundled integer-capable engines through the newer OR-Tools MathOpt API and are covered by the normal schedule regression suite.pulp/cbcis covered by the normal schedule regression suite and opt-in real-world smoke checks.pulp/cuoptis the GPU-accelerated solver. Its real-world smoke check is opt-in and skips when the backend is unavailable.pulp/glpkuses the GLPK command-line solver and has low-level and bounded schedule smoke coverage. Installglpsolwithapt install glpk-utils,brew install glpk, orchoco install glpkbefore selecting it. GLPK can be substantially slower than the other supported backends on larger scheduling models.pulp/highsuses the HiGHS Python API and is covered by the normal schedule regression suite. Thehighspyversion is pinned to the HiGHS ABI bundled with OR-Tools.pulp/scipuses the SCIP Python API and is covered by the normal schedule regression suite.
Running optimization jobs can be cancelled or finished early with ortools/cp-sat,
ortools/mpsolver/scip, ortools/mpsolver/cp-sat, ortools/mpsolver/bop, and
ortools/mathopt/cp-sat. MathOpt/GSCIP, MathOpt/HiGHS, CBC, and PuLP backends do
not support cooperative interruption in this application.
cd core
# create virtual environment
uv venv --python 3.12
# activate virtual environment
source .venv/bin/activate
# install dependencies
uv pip install -r requirements.txt
# run CLI with default solver (ortools/cp-sat)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path]
# for example:
python -m nurse_scheduling.cli tests/testcases/basics/01_1nurse_1shift_1day.yaml
# run CLI with prettify and verbose
python -m nurse_scheduling.cli <input_file_path> [output_xlsx_path] --verbose --prettify
# record solver progress as JSON Lines for later plotting
python -m nurse_scheduling.cli tests/testcases/real/large-ward-with-87-people-2025-11.yaml --verbose --prettify --timeout 180 --progress-output progress.jsonl
# run CLI with PuLP/CBC solver (experimental)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/cbc
# run CLI with PuLP/cuOpt solver (experimental) and GPU required
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/cuopt
# run PuLP/GLPK (experimental; requires glpsol on PATH)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/glpk
# run non-commercial PuLP Python-API solvers (experimental)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/highs
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/scip
# explicit OR-Tools/CP-SAT selector
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/cp-sat
# run an OR-Tools MPSolver backend (experimental)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mpsolver/cbc
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mpsolver/scip
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mpsolver/cp-sat
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mpsolver/bop
# run an OR-Tools MathOpt backend (experimental)
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mathopt/gscip
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mathopt/cp-sat
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/mathopt/highs
Run tests:
cd core
# run low-level solver encoding tests
pytest --log-cli-level=INFO tests/test_solver_ortools_cp_sat.py
pytest --log-cli-level=INFO tests/test_solver_ortools_linear.py
pytest --log-cli-level=INFO tests/test_solver_ortools_mathopt.py
pytest --log-cli-level=INFO tests/test_solver_pulp_cbc.py
pytest --log-cli-level=INFO tests/test_solver_pulp_cuopt.py
pytest --log-cli-level=INFO tests/test_solver_pulp_glpk.py
pytest --log-cli-level=INFO tests/test_solver_pulp_python.py
# run schedule regression tests (OR-Tools / PuLP)
pytest --log-cli-level=INFO tests/test_schedule_ortools_cp_sat.py
pytest --log-cli-level=INFO \
tests/test_schedule_ortools_mpsolver_cbc.py \
tests/test_schedule_ortools_mpsolver_scip.py \
tests/test_schedule_ortools_mpsolver_cp_sat.py \
tests/test_schedule_ortools_mpsolver_bop.py
pytest --log-cli-level=INFO \
tests/test_schedule_ortools_mathopt_gscip.py \
tests/test_schedule_ortools_mathopt_cp_sat.py \
tests/test_schedule_ortools_mathopt_highs.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_cbc.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_cuopt.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_glpk.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_highs.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_scip.py
# run the normal core test suite
pytest --log-cli-level=INFO
# run the slower bounded real-world scenario checks explicitly
pytest --log-cli-level=INFO \
tests/real/schedule_ortools_cp_sat.py \
tests/real/schedule_pulp_cbc.py \
tests/real/schedule_pulp_cuopt.py
# run Python lint checks for core
ruff check nurse_scheduling tests
# auto-fix lint issues when possible
ruff check --fix nurse_scheduling tests
# apply consistent formatting
ruff format nurse_scheduling tests
Generate coverage report:
cd core
# terminal summary
pytest --cov=nurse_scheduling
# HTML report for local inspection
pytest --cov=nurse_scheduling --cov-report=html
# open report at:
# htmlcov/index.html
For more debugging output when a test fails:
cd core
pytest --log-cli-level=INFO tests/test_solver_ortools_cp_sat.py
pytest --log-cli-level=INFO tests/test_solver_pulp_cbc.py
pytest --log-cli-level=INFO tests/test_schedule_ortools_cp_sat.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_cbc.py
Note that setting WRITE_TO_CSV=True in core/tests/schedule_test_helper.py is often useful for creating new test cases.
The checks under core/tests/real/ intentionally omit pytest's test_ filename prefix so they are not included in the
normal core suite. They solve larger real-world scenarios with fixed optimization budgets and run in the separate
test-core-real.yaml GitHub Actions workflow.
Note: The frontend now has Vitest coverage plus Playwright browser integration tests. The root GitHub Actions badge currently still points at the core workflow.
Web Backend¶
The commands below are tested on Linux only.
cd core/nurse_scheduling
# development mode
fastapi dev serve.py
cd ..
# run curl (needs to be run after the server is running)
./tests/test_serve_curl.sh
# run serve tests (don't need to be run after the server is running)
python tests/test_serve.py
# or
pytest tests/test_serve.py --log-cli-level=INFO
By default, optimization job state is process-local memory:
For multiple Uvicorn workers or multiple backend machines, use Redis-backed job state. Redis stores job metadata,
queued job IDs, YAML inputs, XLSX artifacts, and replayable optimization events. Each backend process still runs at
most one optimization job locally, so --workers 3 allows up to three simultaneous jobs across those worker processes.
cd core
JOB_BACKEND=redis \
JOB_REDIS_URL=redis://localhost:6379/0 \
JOB_REDIS_KEY_PREFIX=nurse_scheduling:jobs:v0 \
uvicorn nurse_scheduling.serve:app --workers 3 --no-access-log
The optional JOB_WORKER_LEASE_SECONDS setting defaults to 90 seconds. Keep it
long enough to tolerate brief Redis interruptions. Every worker renews its
presence lease every third of that interval, including while idle.
Replayable event history is capped at 1,000 events per job. Set
JOB_MAX_EVENTS_PER_JOB to choose a different positive limit.
Without Docker, install and start Redis with your operating system package manager.
Ubuntu/Debian:
macOS with Homebrew:
Run the Redis backend tests against a local Redis database:
cd core
JOB_REDIS_TEST_URL=redis://localhost:6379/15 pytest --log-cli-level=INFO tests/test_optimize_job_backends.py
For Docker Compose deployment, docker/compose.backend.yml starts a Redis
service and configures the backend to use it:
Inspect Redis Data¶
The Compose deployment uses Redis database 0 and the key prefix
nurse_scheduling:jobs:v0. Open redis-cli from the Redis container:
Useful inspection commands include:
DBSIZE
SCAN 0 MATCH nurse_scheduling:jobs:v0:* COUNT 100
ZRANGE nurse_scheduling:jobs:v0:jobs 0 -1 WITHSCORES
ZRANGE nurse_scheduling:jobs:v0:queue 0 -1 WITHSCORES
SMEMBERS nurse_scheduling:jobs:v0:pending
ZRANGE nurse_scheduling:jobs:v0:workers:leases 0 -1 WITHSCORES
HGETALL nurse_scheduling:jobs:v0:workers:tokens
HGETALL nurse_scheduling:jobs:v0:workers:active
GET nurse_scheduling:jobs:v0:job:<job-id>
GET nurse_scheduling:jobs:v0:job:<job-id>:input
XRANGE nurse_scheduling:jobs:v0:job:<job-id>:events - + COUNT 20
HGETALL nurse_scheduling:jobs:v0:job:<job-id>:artifact_metadata
Use SCAN instead of KEYS * on a busy database. Job artifacts are binary and
are better inspected through the API download endpoint.
For a graphical browser, run Redis Insight on the Compose network:
docker run --rm \
--name redisinsight \
--network nurse-scheduling-backend_default \
-p 127.0.0.1:5540:5540 \
-v redisinsight:/data \
redis/redisinsight:latest
Open http://localhost:5540 and add a database with redis://default@redis:6379. Filter the Browser view with
nurse_scheduling:jobs:v0:*.
When Redis Insight runs on a remote VM, forward its locally bound port before opening it in a local browser:
Keep Redis and Redis Insight off public interfaces. Redis Insight can modify or delete stored data.
To run one backend worker with process-local memory and no Redis service, use the pre-Redis deployment configuration:
The bundled Redis service uses its default RDB snapshot policy with a persistent volume. Enable AOF or use a managed persistence policy when the deployment requires a smaller data-loss window after an abrupt Redis or host failure.
Documentation¶
The commands below are tested on Linux only.
cd docs
# create virtual environment
uv venv --python 3.12
# activate virtual environment
source .venv/bin/activate
# install dependencies
uv pip install -r requirements.txt
# preview documentation
mkdocs serve
For building static site, run:
Acknowledgments¶
This project would not have been possible without the contributors in CONTRIBUTORS.md.
License¶
This project is licensed under the AGPL-3.0 License.