Skip to content

Repository files navigation

Nurse Scheduling System

tests Netlify Status codecov docs

An automated nurse schedule optimization system designed for diverse and complex real-world requirements.

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) provides a flexible web app and framework for automating schedule optimization across real-world scenarios. It has generated schedules used by real wards with minimal post-adjustment. We keep the main deployment stable, publish the latest features separately, retain versioned releases, and strive to preserve backward compatibility.

Development builds may introduce breaking changes. Use the stable or versioned deployments when repeatability is important.

Project Scope

This project focuses on the difficult and time-consuming part of rostering: turning staffing requirements, rules, and individual preferences into a good schedule. A scheduler still needs to define the ward's concrete rules. Infeasible staffing requirements may also require the scheduler to decide what can safely be relaxed. We continue to improve the ease and flexibility of expressing these constraints. Automated re-optimization also makes changed requests less costly and allows more preferences to be considered than a manual process often can.

The system complements rather than replaces a hospital's existing coordination and workforce-management processes. It intentionally does not prescribe user accounts, leave approvals, shift swaps, schedule publication, audit trails, or other self-service and governance workflows. Preferences can come from an existing hospital system, a spreadsheet, or paper and then be entered by the person preparing the schedule. In our on-site discussions, a head nurse or senior ward member typically owned this task, collecting requests was not the main bottleneck, and constructing or revising the schedule could take several hours or more than ten hours. Organizations can retain any required approvals and audit records in their existing systems.

Keeping request coordination separate makes the optimizer hospital-system agnostic and easier to adopt. Hospitals that need direct integration can import data into the scheduling format and export the result to their systems. Please open an issue to discuss additional import or export requirements.

Two hosted optimization servers are provided as free, shared, best-effort services. The lower-capacity secondary server is available as a fallback. Please use them fairly and do not abuse them. You can also self-host the backend software from this repository.

Privacy Notice

The hosted application anonymizes individual people IDs and removes descriptions by default before sending a schedule for optimization. A schedule without direct identifiers may not identify anyone by itself, but dates, groups, and patterns can still be sensitive in context. Use nicknames or non-identifying IDs when in doubt. For greater control, self-host the open-source frontend and backend so your organization can inspect the code and apply its own security and retention policies. See Privacy and Data Handling for details.

AI Beta Access

During the evaluation period, the hosted AI assistant is gated by an API key by default.

To request access for experimentation, email admin@nursescheduling.org from your institution email address. Include your institution's name and a short description of how you plan to evaluate the assistant.

Before requesting or using access, review Privacy and Data Handling. Do not submit personal, confidential, regulated, or otherwise sensitive information.

Support

For general questions, open a GitHub issue. For personal questions, email admin@nursescheduling.org.

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:

git clone https://github.com/j3soon/nurse-scheduling.git
cd nurse-scheduling

Linux (bash/zsh)

Start frontend:

cd web-frontend
bun install
bun run dev

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:

cd web-frontend
bun install
bun run dev

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:

cd web-frontend
bun install
bun run dev

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:

./scripts/setup_env.sh

For Docker-based development environment:

CPU image:

# build image
docker build -f docker/Dockerfile -t j3soon/nurse-scheduling:dev .
# persist Codex/Claude Code/OpenCode/Pi 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
mkdir -p ~/docker/pi/agent
# mount project files and Codex/Claude Code/OpenCode/Pi 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 ~/docker/pi/agent:/root/.pi/agent \
  -v /etc/localtime:/etc/localtime:ro \
  -v /etc/timezone:/etc/timezone:ro \
  j3soon/nurse-scheduling:dev

GPU image with cuOpt support:

# 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/Pi 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
mkdir -p ~/docker/pi/agent
# mount project files and Codex/Claude Code/OpenCode/Pi 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 ~/docker/pi/agent:/root/.pi/agent \
  -v /etc/localtime:/etc/localtime:ro \
  -v /etc/timezone:/etc/timezone:ro \
  j3soon/nurse-scheduling:dev-cuopt

After entering a container, use the Core commands to run the CLI or the Web Backend commands to start a server. Use the GPU image for pulp/cuopt.

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
mkdir -p ~/docker/pi/agent
# mount project files and Codex/Claude Code/OpenCode/Pi 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 ~/docker/pi/agent:/root/.pi/agent \
  -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 .next in web-frontend to clear the Next.js cache when switching between host and Docker environments.

Web Frontend

The commands below are tested on Linux only.

cd web-frontend
bun install
bun run dev

Run frontend unit/component tests:

cd web-frontend
bun run test

Run frontend coverage:

cd web-frontend
bun run test: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.

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:

cd web-frontend
bun run test:e2e:coverage
bun run coverage:e2e:report

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:

cd web-frontend
bun run build

Hosting on Netlify

The root netlify.toml builds the static frontend into web-frontend/out and publishes the documentation under /docs. After linking the repository to a Netlify project, open Project configuration → Environment variables and configure these variables with the Builds scope:

Variable Value Sensitive
NEXT_PUBLIC_SENTRY_DSN Public DSN for the frontend Sentry project. No
SENTRY_ENVIRONMENT production for the production deploy context. Use a distinct value such as staging for branch deploys. No
SENTRY_PROJECT Slug of the frontend Sentry project. No
SENTRY_AUTH_TOKEN Sentry organization auth token allowed to create releases and upload source maps for the frontend project. Yes

Mark SENTRY_AUTH_TOKEN as Contains secret values in Netlify. Never prefix it with NEXT_PUBLIC_, put it in netlify.toml, or commit it to an environment file. The Next.js Sentry build plugin reads it only while building, then uploads the release and source maps. Without it, the site still builds and browser events still reach Sentry through NEXT_PUBLIC_SENTRY_DSN, but production stack traces may remain minified.

To create the token:

  1. Follow Sentry's auth-token instructions to create an organization token through an internal integration.
  2. Grant org:ci for release and source-map operations. Ensure the integration can access the team that owns SENTRY_PROJECT.
  3. Copy the generated token into Netlify as SENTRY_AUTH_TOKEN, select the Builds scope, and mark it as Contains secret values.

Trigger a new deploy after changing any build environment variable. Configure different SENTRY_ENVIRONMENT values per Netlify deploy context when production and branch deploys share the same frontend Sentry project.

For linting, run:

cd web-frontend
bun run lint -- --fix

bun can be replaced directly with npm for the basic Next.js workflow, but the documented project scripts assume Bun.

Experimental AI Chat

The experimental chat answers questions about the schedule currently open in the frontend. Image and UTF-8 TXT, Markdown, and CSV attachments are enabled by default. Disable them independently with AI_ATTACHMENT_MODE=none and AI_DOCUMENT_ATTACHMENT_MODE=none. The chat runs as a separate backend process and sends the schedule and enabled inputs to an OpenAI-compatible provider.

Create a local configuration file. The real docker/.env file is ignored by Git:

cp docker/.env.example docker/.env
# Review and update the AI values. Set AI_AUTH_REQUIRED=false and leave
# AI_AUTH_TOKEN and AI_AUTH_TOKENS empty only for intentional local no-auth use.

Start the AI backend and frontend in separate terminals:

./scripts/start_ai_backend.sh
./scripts/start_frontend.sh --hostname 0.0.0.0

Open http://localhost:3000/experimental-ai, select Change, then select Use localhost. The local AI backend listens on http://localhost:8001. The page otherwise uses https://api.nursescheduling.org/ai by default. See the AI assistant backend guide for container commands, configuration, security notes, and focused tests.

Core

The main solver paths are:

  • ortools/cp-sat, labeled OR-Tools | CP-SAT, is the recommended CPU solver and the default.
  • pulp/cuopt, labeled PuLP | cuOpt, is the experimental GPU solver. It requires the NVIDIA cuOpt runtime and a supported GPU.

See the solver reference for the full experimental solver matrix, platform requirements, runtime capabilities, and test coverage.

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 the CPU solver, OR-Tools | CP-SAT is the default
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver ortools/cp-sat
# for example:
python -m nurse_scheduling.cli tests/testcases/basics/01_1nurse_1shift_1day.yaml
# run the GPU solver, PuLP | cuOpt
python -m nurse_scheduling.cli <input_file_path> [output_csv_path] --solver pulp/cuopt
# 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 tests:

cd core
# run the normal core test suite
pytest --log-cli-level=INFO
# run focused OR-Tools | CP-SAT tests
pytest --log-cli-level=INFO \
  tests/test_solver_ortools_cp_sat.py \
  tests/test_schedule_ortools_cp_sat.py
# run focused PuLP | cuOpt tests in the GPU environment
pytest --log-cli-level=INFO \
  tests/test_solver_pulp_cuopt.py \
  tests/test_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_schedule_ortools_cp_sat.py
pytest --log-cli-level=INFO tests/test_solver_pulp_cuopt.py
pytest --log-cli-level=INFO tests/test_schedule_pulp_cuopt.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, the server exposes only OR-Tools | CP-SAT and keeps job state in process-local memory:

cd core
JOB_BACKEND=memory \
OPTIMIZE_SOLVERS=ortools/cp-sat \
OPTIMIZE_DEFAULT_SOLVER=ortools/cp-sat \
uvicorn nurse_scheduling.serve:app --no-access-log

To expose a GPU-only PuLP | cuOpt server, run this command in the cuOpt environment or GPU development container:

cd core
JOB_BACKEND=memory \
OPTIMIZE_SOLVERS=pulp/cuopt \
OPTIMIZE_DEFAULT_SOLVER=pulp/cuopt \
uvicorn nurse_scheduling.serve:app --no-access-log

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.

The backend is the source of truth for the optimization controls shown by the frontend. GET /optimize/options returns the allowed solvers, integer timeout range, running-job controls, and prettify default. Configure them with:

export OPTIMIZE_SOLVERS=ortools/cp-sat,pulp/cuopt
export OPTIMIZE_DEFAULT_SOLVER=ortools/cp-sat
export OPTIMIZE_MIN_TIMEOUT_SECONDS=1
export OPTIMIZE_DEFAULT_TIMEOUT_SECONDS=300
export OPTIMIZE_MAX_TIMEOUT_SECONDS=3600
export OPTIMIZE_DEFAULT_PRETTIFY=true

The server is unauthenticated by default, which suits local development. Set the legacy API_AUTH_TOKEN or a JSON object such as API_AUTH_TOKENS='{"institution-a":"key"}' to require a bearer key on every application route except /info and /ready:

cd core
API_AUTH_TOKEN="$(openssl rand -base64 32)" \
uvicorn nurse_scheduling.serve:app --no-access-log

GET /info reports auth.required so the frontend can prompt for a key. The generated /openapi.json, /docs, and /redoc routes are disabled while authentication is configured. The images under docker/ set API_AUTH_REQUIRED=true, so a deployed backend refuses to start without a configured key. Serving one without authentication requires API_AUTH_REQUIRED=false.

Only advertise solvers available on that machine. The server validates the configured runtimes at startup.

Without Docker, install and start Redis with your operating system package manager.

Ubuntu/Debian:

sudo apt-get update
sudo apt-get install redis-server
redis-server --daemonize yes
redis-cli ping

macOS with Homebrew:

brew install redis
brew services start redis
redis-cli ping

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:

cd docker
docker compose -f compose.backend.yml up -d --build

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:

docker compose -f compose.backend.yml exec redis redis-cli -n 0

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.

To run one backend worker with process-local memory and no Redis service, use the pre-Redis deployment configuration:

cd docker
docker compose -f compose.backend.memory.yml up -d --build

The bundled Redis service persists an AOF with appendfsync everysec and keeps an RDB fallback after six hours when at least one write has occurred. This limits the usual abrupt-failure exposure to approximately the latest second, while an RDB-only recovery can be up to six hours behind. Redis installed outside the bundled Compose deployment keeps its system persistence policy.

Documentation

The commands below are tested on Linux only.

# create virtual environment
uv venv --python 3.12 docs/.venv
# activate virtual environment
source docs/.venv/bin/activate
# install dependencies
uv pip install -r docs/requirements.txt
# preview documentation on the port used by local page-help links
zensical serve

For building static site, run:

zensical build --clean --strict

Acknowledgments

This project would not have been possible without the contributors in CONTRIBUTORS.md.

See ACKNOWLEDGMENTS.md for the free services this project relies on.

License

This project is licensed under the AGPL-3.0 License.

References

About

A flexible web application designed to streamline and automate nurse scheduling, suitable for a wide range of diverse and complex real-world requirements.

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages