dabke
v0.81.1
Published
Scheduling library powered by constraint programming (CP-SAT)
Maintainers
Readme
dabke
TypeScript scheduling library powered by constraint programming.
Define teams, shifts, coverage, and rules declaratively. dabke compiles them into a CP-SAT model and solves for an optimized schedule.
import {
defineSchedule,
t,
time,
cover,
shift,
maxHoursPerWeek,
minRestBetweenShifts,
timeOff,
minimizeCost,
weekdays,
weekend,
} from "dabke";
const schedule = defineSchedule({
roles: ["barista", "server"],
// Times: WHEN you need people (named periods for coverage + rules)
times: {
breakfast: time({ startTime: t(7), endTime: t(10) }),
lunch_rush: time(
{ startTime: t(11, 30), endTime: t(14) },
{ startTime: t(11), endTime: t(15), dayOfWeek: weekend },
),
closing: time({ startTime: t(20), endTime: t(22) }),
},
// Coverage: HOW MANY people you need during each time
coverage: [
cover("breakfast", "barista", 1),
cover("lunch_rush", "barista", 2, { dayOfWeek: weekdays }),
cover("lunch_rush", "barista", 3, { dayOfWeek: weekend }),
cover("lunch_rush", "server", 1),
cover("closing", "server", 1),
],
// Shifts: WHEN people CAN work (the actual time slots)
// An opener covers breakfast + lunch; a closer covers lunch + closing
shiftPatterns: [
shift("opener", t(6), t(14)),
shift("mid", t(10), t(18)),
shift("closer", t(14), t(22)),
],
rules: [
maxHoursPerWeek(40),
minRestBetweenShifts(11),
timeOff({ appliesTo: "alice", dayOfWeek: weekend }),
minimizeCost(),
],
});Quick Start
npm install dabkedabke compiles scheduling problems into a constraint model. You need a CP-SAT solver to find the solution:
docker pull christianklotz/dabke-solver
docker run -p 8080:8080 christianklotz/dabke-solverOnce you have a schedule definition, create a config with runtime data and solve:
import { ModelBuilder, HttpSolverClient, parseSolverResponse, resolveAssignments } from "dabke";
const config = schedule.createSchedulerConfig({
schedulingPeriod: {
dateRange: { start: "2026-02-09", end: "2026-02-15" },
},
members: [
{ id: "alice", roles: ["barista", "server"], pay: { hourlyRate: 1500 } },
{ id: "bob", roles: ["barista"], pay: { hourlyRate: 1200 } },
{ id: "carol", roles: ["barista", "server"], pay: { hourlyRate: 1400 } },
{ id: "dave", roles: ["barista", "server"], pay: { hourlyRate: 1200 } },
{ id: "eve", roles: ["barista", "server"], pay: { hourlyRate: 1300 } },
],
});
const builder = new ModelBuilder(config);
const { request, canSolve } = builder.compile();
const solver = new HttpSolverClient(fetch, "http://localhost:8080");
const response = await solver.solve(request);
const result = parseSolverResponse(response);
const shifts = resolveAssignments(result.assignments, config.shiftPatterns);Key Concepts
Times vs shift patterns. These are two distinct things. Times are named periods you need coverage for ("breakfast", "lunch_rush"). Shift patterns are the time slots people actually work ("opener 6-14", "closer 14-22"). They don't need to match. The solver assigns members to shift patterns whose hours overlap with times to satisfy coverage.
Coverage. How many people with which roles you need during each time. Coverage can vary by day of week or specific dates using scoping options or the variant form of cover().
Rules. Business constraints expressed as function calls. Rules with priority: "MANDATORY" (the default) are hard constraints the solver will never violate. Rules with LOW, MEDIUM, or HIGH priority are soft preferences the solver optimizes across.
Scoping. Every rule accepts appliesTo (a role, skill, or member ID), plus time scoping (dayOfWeek, dateRange, dates, recurringPeriods) to target when and who:
maxHoursPerDay(8, { appliesTo: "barista", dayOfWeek: weekdays }),
timeOff({ appliesTo: "alice", dateRange: { start: "2026-02-09", end: "2026-02-13" } }),
maxHoursPerWeek(30, { appliesTo: "bob", priority: "MEDIUM" }),Cost optimization. Members declare pay as hourly or salaried. Cost rules (minimizeCost, dayMultiplier, overtimeMultiplier, tieredOvertimeMultiplier, etc.) tell the solver to minimize total labor cost, factoring in overtime, day premiums, and time-based surcharges.
Validation. ModelBuilder reports coverage gaps and rule violations both pre-solve (via compile()) and post-solve (via builder.reporter.analyzeSolution()). Use summarizeValidation() for grouped reporting.
Rules
Scheduling
| Function | Description |
| -------------------------- | ------------------------------------ |
| maxHoursPerDay(hours) | Max hours per person per day |
| maxHoursPerWeek(hours) | Max hours per person per week |
| minHoursPerDay(hours) | Min hours per person per day |
| minHoursPerWeek(hours) | Min hours per person per week |
| maxShiftsPerDay(n) | Max shift assignments per day |
| maxConsecutiveDays(n) | Max consecutive working days |
| minConsecutiveDays(n) | Min consecutive working days |
| minRestBetweenShifts(h) | Min rest hours between shifts |
| timeOff(opts) | Block assignments during periods |
| preference(level, opts) | Prefer or avoid assigning members |
| preferLocation(id, opts) | Prefer members at specific locations |
| assignTogether(opts) | Keep members on the same shifts |
Cost
| Function | Description |
| ---------------------------------------- | ---------------------------------------- |
| minimizeCost() | Minimize total labor cost |
| dayMultiplier(factor, opts?) | Pay multiplier for specific days |
| daySurcharge(amount, opts?) | Flat surcharge per hour on specific days |
| timeSurcharge(amount, window, opts?) | Surcharge during a time-of-day window |
| overtimeMultiplier(opts) | Weekly overtime pay multiplier |
| overtimeSurcharge(opts) | Weekly overtime flat surcharge |
| dailyOvertimeMultiplier(opts) | Daily overtime pay multiplier |
| dailyOvertimeSurcharge(opts) | Daily overtime flat surcharge |
| tieredOvertimeMultiplier(tiers, opts?) | Graduated overtime rates |
LLM Integration
dabke ships an llms.txt with complete API documentation for AI code generation:
import { apiDocs } from "dabke/llms";Development
Unit tests (no dependencies): npm run test:unit
Integration tests (requires Docker): npm run test:integration
The test harness builds and starts a solver container from solver/Dockerfile automatically.
import { startSolverContainer } from "dabke/testing";
const solver = await startSolverContainer();See CONTRIBUTING.md for how to add rules and contribute.
