CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling
Back to Writing

CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling

Michael BrenndoerferNovember 21, 202547 min read10,323 wordsInteractive

Learn CP-SAT rostering using Google OR-Tools to solve complex workforce scheduling problems with binary decision variables, coverage constraints, and employee availability. Master constraint programming for optimal employee shift assignments.

Data Science Handbook Cover
Part of Data Science Handbook

This article is part of the free-to-read Data Science Handbook

View full handbook
Reading Level

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Constraint Programming (CP) - Satisfiability (SAT) Rostering (CP-SAT)

Constraint Programming (CP) is an approach for solving combinatorial optimization problems by modeling them as constraint satisfaction problems. CP-SAT (Constraint Programming - Satisfiability) is Google OR-Tools' constraint programming solver that combines the expressiveness of constraint programming with the efficiency of modern SAT (Boolean Satisfiability) solvers. When applied to rostering problems, CP-SAT excels at finding feasible schedules that satisfy complex business rules and constraints.

Rostering problems involve assigning employees to shifts while satisfying various constraints such as labor laws, employee preferences, skill requirements, and operational needs. These problems are inherently combinatorial - with multiple employees, shifts, and time periods, the number of possible assignments grows exponentially. Traditional approaches like manual scheduling or simple heuristics may struggle to find optimal or even feasible solutions when constraints become complex.

CP-SAT is particularly well-suited for rostering because it can handle both hard constraints (that must be satisfied) and soft constraints (that we prefer to satisfy), while also allowing for objective functions that optimize for cost, fairness, or other business metrics. Unlike linear programming approaches that require linear constraints, CP-SAT can model logical relationships, conditional constraints, and complex business rules directly.

Advantages

CP-SAT offers several key advantages for rostering problems. First, it provides good modeling flexibility - you can express complex business rules, labor regulations, and operational constraints using intuitive logical operators and relationships. This makes it easier to translate real-world requirements into mathematical constraints compared to traditional linear programming approaches.

Second, CP-SAT can handle both feasibility and optimization problems seamlessly. You can start by finding any feasible solution that satisfies all hard constraints, then optimize for objectives like minimizing labor costs or maximizing employee satisfaction. The solver can also provide multiple solutions, allowing decision-makers to choose between different trade-offs.

Finally, CP-SAT is generally efficient for many rostering problems, often finding good solutions quickly even for large-scale problems with hundreds of employees and thousands of time slots. The underlying SAT solver technology has been extensively optimized and can leverage modern hardware effectively.

Disadvantages

Despite its strengths, CP-SAT has some limitations for rostering applications. The most significant limitation is that it's primarily designed for discrete optimization problems, so it cannot directly handle continuous variables or fractional assignments. While this is rarely an issue for rostering (where assignments are typically binary), it can limit flexibility in some workforce planning scenarios.

Another limitation is that CP-SAT may struggle with problems that have very tight constraint sets, where finding any feasible solution becomes extremely difficult. In such cases, the solver might need to explore a large portion of the solution space, leading to longer computation times or timeouts.

Finally, while CP-SAT is well-suited for modeling logical constraints, it may not be as efficient as specialized algorithms for certain types of rostering problems. For example, problems with very regular patterns or simple constraints might be solved more efficiently using custom heuristics or other optimization approaches.

Formula

At its core, rostering is a question of yes or no: should Alice work the morning shift on Monday? Should Bob cover the night shift on Friday? Each of these questions demands a binary answer. There's no such thing as "half an assignment." This fundamental observation leads us directly to the mathematical structure that makes CP-SAT rostering effective for this type of problem.

The Foundation: Binary Decision Variables

Imagine you're a manager creating next week's schedule. For every possible combination of employee, shift type, and day, you face a simple decision: assign them or don't. There are no shades of gray. An employee either works a shift or they don't. This binary nature of rostering decisions is precisely what we capture mathematically.

We represent each possible assignment as a binary decision variable:

xe,s,t{0,1}eE,sS,tTx_{e,s,t} \in \{0, 1\} \quad \forall e \in E, s \in S, t \in T

where:

  • EE: the set of all employees (indexed by ee)
  • SS: the set of all shift types, such as Morning, Afternoon, Evening, and Night (indexed by ss)
  • TT: the set of all time periods, typically days of the week (indexed by tt)
  • xe,s,tx_{e,s,t}: binary decision variable equal to 1 if employee ee is assigned to shift ss in time period tt, and 0 otherwise

Why binary variables? Because rostering is inherently discrete. You cannot assign someone to "0.7 of a shift" or "part-time coverage." The binary representation forces our model to make clear, unambiguous decisions that match the reality of workforce scheduling. This mathematical choice isn't arbitrary. It directly reflects the nature of the problem we're solving.

But here's where it gets interesting: with just a few employees, shifts, and days, the number of these binary decisions explodes. If you have 5 employees, 4 shift types, and 7 days, you're making 5×4×7=1405 \times 4 \times 7 = 140 independent yes-or-no decisions. Each decision variable can be either 0 or 1, which means you're navigating a solution space with 21402^{140} possible combinations. This is an astronomically large number that makes brute-force search completely impractical.

To visualize the structure of these decision variables, imagine a three-dimensional space where each dimension represents one aspect of the assignment: employees, shifts, and time periods. Each point in this space corresponds to a binary decision variable that can take the value 0 (not assigned) or 1 (assigned).

Out[3]:
Visualization
Notebook output

Three-dimensional structure of decision variables in CP-SAT rostering. The visualization shows how decision variables x_{e,s,t} form a 3D grid where each cell represents a binary choice: assign employee e to shift s in time period t (value 1, shown in green) or not (value 0, shown in red). The three axes represent employees (E), shifts (S), and time periods (T). This structure illustrates why the total number of decision variables is |E| × |S| × |T|, and why the solution space grows exponentially as 2^{|E| × |S| × |T|}. Understanding this 3D structure helps visualize how constraints operate across different dimensions of the problem.

Bringing Order to Chaos: Constraints

Having decision variables is just the beginning. Without rules, we could assign anyone to any shift at any time, but that would create chaos. Real-world rostering typically needs to satisfy multiple competing requirements simultaneously. These requirements become constraints in our mathematical model: rules that any valid solution should obey.

Think of constraints as the guardrails that keep our solution on track. They transform an impossibly large space of potential assignments into a manageable set of feasible solutions. Each constraint type addresses a different real-world concern, and together they ensure our roster is both operationally viable and legally compliant.

Coverage Constraints: The most fundamental requirement is that every shift should be staffed. Imagine a hospital emergency room that needs at least 3 nurses on the night shift, or a restaurant that requires 2 servers during dinner rush. Coverage constraints ensure we meet these minimum staffing levels:

eExe,s,t=rs,tsS,tT\sum_{e \in E} x_{e,s,t} = r_{s,t} \quad \forall s \in S, t \in T

This formula says: for every shift type ss and every time period tt, the sum of all assignments (where xe,s,t=1x_{e,s,t} = 1) should equal the required number of employees rs,tr_{s,t}. The summation eE\sum_{e \in E} counts how many employees are assigned. We're adding up the ones. Why equality rather than "at least"? Because we can also model "exactly rs,tr_{s,t} employees" if needed, though in practice you might use \geq to allow overstaffing.

where:

  • rs,tr_{s,t}: the minimum number of employees required for shift ss in time period tt (also called the staffing requirement)

This constraint is non-negotiable. If coverage isn't met, operations fail. But coverage alone isn't enough. We also need to respect the reality that employees have lives outside work.

Employee Availability: People can't work when they're unavailable. Alice might have classes on Tuesday mornings, Bob might be on vacation, or Charlie might have childcare responsibilities. Availability constraints prevent impossible assignments:

xe,s,t=0(e,s,t)Ux_{e,s,t} = 0 \quad \forall (e,s,t) \in \mathcal{U}

where:

  • U\mathcal{U}: the set of all infeasible assignments (e,s,t)(e,s,t) where employee ee is unavailable for shift ss in period tt (a subset of E×S×TE \times S \times T)

By setting xe,s,t=0x_{e,s,t} = 0 for all (e,s,t)U(e,s,t) \in \mathcal{U}, we're telling the solver: "These assignments are off-limits. Don't even consider them." This significantly reduces the search space and helps prevent schedules that violate employee availability. The constraint applies to each infeasible triple individually, effectively removing those decision variables from consideration.

Maximum Shifts per Employee: Fairness matters. We can't have one employee working 10 shifts while another works 2. Maximum shift constraints ensure balanced workloads:

sS,tTxe,s,tmeeE\sum_{s \in S, t \in T} x_{e,s,t} \leq m_e \quad \forall e \in E

where:

  • mem_e: the maximum number of shifts employee ee can work during the planning period (a non-negative integer, typically set based on labor regulations or organizational policies)

The summation sS,tTxe,s,t\sum_{s \in S, t \in T} x_{e,s,t} counts all assignments for employee ee across all shifts and time periods. This sum equals the total number of shifts assigned to employee ee (since each xe,s,tx_{e,s,t} is either 0 or 1). By requiring this sum to be me\leq m_e, we cap each employee's total workload. This constraint prevents burnout and ensures compliance with labor regulations that limit working hours.

Rest Periods: Perhaps the most critical constraint for employee well-being is that people need rest. Working consecutive shifts without adequate recovery time is unsafe, illegal in many jurisdictions, and leads to poor performance. The rest period constraint prevents this:

xe,s1,t+xe,s2,t+Δt1eE, where Δt<ρx_{e,s_1,t} + x_{e,s_2,t + \Delta t} \leq 1 \quad \forall e \in E, \text{ where } \Delta t < \rho

where:

  • Δt\Delta t: the time gap between consecutive shifts (measured in the same units as tt, typically hours or time periods)
  • ρ\rho: the minimum required rest period between shifts (often 8-12 hours, measured in the same units as Δt\Delta t)
  • s1,s2s_1, s_2: two different shift types (the constraint applies to any pair of shifts)
  • tt: the time period of the first shift
  • t+Δtt + \Delta t: the time period of the second shift, occurring Δt\Delta t time units after the first

The constraint xe,s1,t+xe,s2,t+Δt1x_{e,s_1,t} + x_{e,s_2,t + \Delta t} \leq 1 works as follows: if an employee works shift s1s_1 at time tt (so xe,s1,t=1x_{e,s_1,t} = 1), and the time gap Δt\Delta t is less than the minimum rest period ρ\rho, then they cannot work shift s2s_2 at time t+Δtt + \Delta t (forcing xe,s2,t+Δt=0x_{e,s_2,t + \Delta t} = 0). The sum of these two binary variables cannot exceed 1, meaning at most one of them can be 1. This mathematical relationship directly encodes the physical reality that people need time to rest.

The rest period constraint can be visualized as a timeline showing how consecutive shift assignments are restricted. When an employee works a shift at time tt, they cannot work another shift at time t+Δtt + \Delta t if the time gap Δt\Delta t is less than the minimum rest period ρ\rho.

Out[4]:
Visualization
Notebook output

Timeline visualization of the rest period constraint $x_{e,s_1,t} + x_{e,s_2,t+\\Delta t} \\leq 1$ where $\\Delta t < \\rho$. The diagram shows a timeline with two consecutive time periods (t and t+Δt). The constraint ensures that if an employee is assigned to shift s₁ at time t (green bar), they cannot be assigned to shift s₂ at time t+Δt if the time gap Δt is less than the minimum rest period ρ (shown as the red 'X' indicating the forbidden assignment). The minimum rest period ρ is visualized as a required gap between shifts, ensuring employees have adequate rest. This constraint prevents overworking and ensures compliance with labor regulations.

Finding the Best Solution: The Objective Function

Constraints tell us what's allowed, but they don't tell us what's best. Among all feasible solutions, and there may be thousands, which one should we choose? This is where the objective function comes in. It quantifies what "best" means for our specific situation.

Most commonly, we want to minimize costs. Every assignment has a cost: regular wages, overtime premiums, or penalties for assigning someone to an undesirable shift. The objective function sums these costs across all assignments:

mineEsStTce,s,txe,s,t\min \sum_{e \in E} \sum_{s \in S} \sum_{t \in T} c_{e,s,t} \cdot x_{e,s,t}

where:

  • ce,s,tc_{e,s,t}: the cost of assigning employee ee to shift ss in period tt (a non-negative real number, typically measured in currency units or preference scores)

The binary nature of xe,s,tx_{e,s,t} works well here: when xe,s,t=0x_{e,s,t} = 0 (no assignment), the term contributes nothing to the sum. When xe,s,t=1x_{e,s,t} = 1 (assignment made), the cost ce,s,tc_{e,s,t} is added. The triple summation eEsStT\sum_{e \in E} \sum_{s \in S} \sum_{t \in T} ensures we consider every possible assignment, but only the ones that actually occur (where xe,s,t=1x_{e,s,t} = 1) contribute to the total cost.

The cost ce,s,tc_{e,s,t} can encode various business priorities:

  • Labor costs: Base wages for the shift
  • Overtime premiums: Higher costs for shifts that push employees over their regular hours
  • Preference penalties: Costs that discourage assigning employees to shifts they dislike
  • Skill premiums: Costs that reflect the value of assigning highly skilled employees to critical shifts

By minimizing this sum, we find the schedule that satisfies all constraints while keeping total costs as low as possible. But cost isn't the only possible objective. We could maximize fairness, minimize schedule changes from previous periods, or balance multiple objectives simultaneously.

Understanding the Challenge: Mathematical Properties

Now that we've built our mathematical model, let's understand the scale of the problem we're solving. The properties we'll explore explain why sophisticated algorithms like CP-SAT are necessary and why naive approaches fail spectacularly.

Solution Space Size: The combinatorial explosion

With E|E| employees, S|S| shift types, and T|T| time periods, we have E×S×T|E| \times |S| \times |T| binary decision variables. Each variable can independently be 0 or 1, which means the total number of possible assignments is:

S=2E×S×T|\mathcal{S}| = 2^{|E| \times |S| \times |T|}

where:

  • S\mathcal{S}: the set of all possible solutions (before applying constraints)
  • E|E|: the cardinality (size) of the employee set EE, i.e., the number of employees
  • S|S|: the cardinality (size) of the shift type set SS, i.e., the number of shift types
  • T|T|: the cardinality (size) of the time period set TT, i.e., the number of time periods
  • E×S×T|E| \times |S| \times |T|: the total number of decision variables (the product of the three set sizes)

This exponential growth is the heart of the challenge. Consider a modest problem: 10 employees, 4 shift types, 7 days. That's 10×4×7=28010 \times 4 \times 7 = 280 decision variables, creating 228010842^{280} \approx 10^{84} possible solutions. To put this in perspective, there are estimated to be 108010^{80} atoms in the observable universe. We're dealing with numbers so large that exhaustive enumeration is computationally infeasible. Even if we could check a billion solutions per second, it would take longer than the age of the universe to explore them all.

This is why constraints are so powerful: they eliminate vast swaths of this solution space. But even with constraints, the remaining feasible space can be enormous. We need algorithms that can intelligently search this space without exploring every possibility.

The exponential growth of the solution space can be visualized to understand why exhaustive search becomes infeasible even for moderately-sized problems. As the number of decision variables increases, the solution space grows at an exponential rate, making it computationally intractable to explore all possibilities.

Out[5]:
Visualization
Notebook output

Exponential growth of solution space in CP-SAT rostering. The graph shows how the solution space size $|\\mathcal{S}| = 2^{|E| \\times |S| \\times |T|}$ grows exponentially as the number of decision variables increases. The x-axis represents the total number of decision variables (|E| × |S| × |T|), and the y-axis shows the solution space size on a logarithmic scale. Multiple problem sizes are shown: small (3×2×3 = 18 variables), medium (5×4×7 = 140 variables), and large (10×4×14 = 560 variables). The exponential curve demonstrates why exhaustive search is impractical: even a modest increase in problem size leads to an astronomical number of possible solutions. For example, with just 20 decision variables, there are over 1 million possible solutions, and with 30 variables, there are over 1 billion. This visualization explains why efficient constraint propagation and search algorithms are essential for practical rostering applications.

Constraint Satisfaction Structure: Putting it all together

We can now see the complete mathematical structure. The rostering problem is a constraint satisfaction problem (CSP): we're searching for assignments that satisfy all constraints simultaneously. Formally:

Find xe,s,t{0,1}eE,sS,tTs.t. eExe,s,t=rs,tsS,tT(coverage)xe,s,t=0(e,s,t)U(availability)sS,tTxe,s,tmeeE(maximum shifts)xe,s1,t+xe,s2,t+Δt1eE,Δt<ρ(rest periods)\begin{aligned} \text{Find } &x_{e,s,t} \in \{0,1\} \quad \forall e \in E, s \in S, t \in T \\ \text{s.t. } &\sum_{e \in E} x_{e,s,t} = r_{s,t} \quad \forall s \in S, t \in T \quad \text{(coverage)} \\ &x_{e,s,t} = 0 \quad \forall (e,s,t) \in \mathcal{U} \quad \text{(availability)} \\ &\sum_{s \in S, t \in T} x_{e,s,t} \leq m_e \quad \forall e \in E \quad \text{(maximum shifts)} \\ &x_{e,s_1,t} + x_{e,s_2,t + \Delta t} \leq 1 \quad \forall e \in E, \Delta t < \rho \quad \text{(rest periods)} \end{aligned}

This formulation reveals the structure that CP-SAT exploits. Each constraint type operates on different subsets of variables:

  • Coverage constraints link variables across employees for a given shift-time combination
  • Availability constraints fix specific variables to zero
  • Maximum shift constraints aggregate variables across shifts and times for each employee
  • Rest period constraints create relationships between variables at different time points

The power of constraint programming comes from how these constraints interact. When the solver sets one variable, it can immediately infer consequences for related variables through constraint propagation. For example, if Alice is the only available employee for a shift that requires 2 people, the solver immediately knows the problem is infeasible, without exploring millions of other assignments.

Computational Complexity: Why sophisticated algorithms matter

The exponential solution space means that naive approaches, such as trying every combination or even random sampling, are doomed to failure. But the constraint structure gives us hope: by propagating constraints intelligently, CP-SAT can eliminate huge portions of the search space without explicitly exploring them. The solver uses techniques like:

  • Constraint propagation: Inferring variable values from constraints
  • Conflict-driven learning: Remembering why certain assignments failed to avoid repeating mistakes
  • Branching heuristics: Choosing which variables to set first to maximize constraint propagation

These techniques transform an intractable search into a manageable optimization problem. For most practical rostering problems, CP-SAT can find optimal or near-optimal solutions in seconds to minutes, even when the underlying solution space contains trillions of possibilities.

Visualizing CP-SAT Rostering

Let's create visualizations to understand how CP-SAT rostering works in practice. We'll start by setting up the necessary libraries and creating a simple example.

Now let's create a simple rostering example to visualize the concepts:

In[7]:
1# Create a simple rostering problem with guaranteed feasibility
2# We construct a deterministic problem that ensures a solution exists
3
4# NOTE: This section only sets up the problem parameters (availability, requirements).
5# The actual solving is done later using the CP-SAT solver.
6
7# Problem parameters
8employees = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
9shifts = ["Morning", "Afternoon", "Evening", "Night"]
10days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
11
12# Create availability matrix with very high availability to guarantee feasibility
13# Start with all employees available for all shifts
14np.random.seed(42)
15availability = np.ones((len(employees), len(shifts), len(days)), dtype=int)
16
17# Add minimal unavailability for realism, but ensure each shift-day
18# has at least 4 available employees (more than we'll ever need)
19for s in range(len(shifts)):
20    for d in range(len(days)):
21        # Allow at most 1 employee to be unavailable per shift-day
22        # This ensures at least 4 are available (we only need 1-2)
23        unavailable_count = np.random.choice([0, 1], p=[0.7, 0.3])
24        if unavailable_count > 0:
25            unavailable_idx = np.random.choice(len(employees))
26            availability[unavailable_idx, s, d] = 0
27
28        # Final check: ensure at least 4 are available
29        if availability[:, s, d].sum() < 4:
30            availability[:, s, d] = 1  # Reset to all available
31            # Then make at most 1 unavailable
32            unavailable_idx = np.random.choice(len(employees))
33            availability[unavailable_idx, s, d] = 0
34
35# Set required staff conservatively to guarantee feasibility
36# Strategy: require 1 per shift on most days, with some days having 0 for night/evening
37# This ensures we have plenty of slack for the "no consecutive night shifts" constraint
38# Total: 4 shifts × 7 days = 28 shift-days
39# We'll require about 16-18 total assignments (well below 25 max possible)
40required_staff = np.array(
41    [
42        [1, 1, 1, 1, 1, 1, 1],  # Morning: 1 per day (7 total)
43        [1, 1, 1, 1, 1, 1, 1],  # Afternoon: 1 per day (7 total)
44        [
45            1,
46            0,
47            1,
48            0,
49            1,
50            0,
51            1,
52        ],  # Evening: 1 on Mon, Wed, Fri, Sun (4 total, alternating)
53        [
54            0,
55            1,
56            0,
57            1,
58            0,
59            1,
60            0,
61        ],  # Night: 1 on Tue, Thu, Sat (3 total, no consecutive days)
62    ]
63)
64# Total: 7 + 7 + 4 + 3 = 21 assignments required
65# Max possible: 5 employees × 5 shifts = 25, so this is feasible
66# Night shifts are on non-consecutive days, satisfying the constraint
67
68# Verify feasibility: ensure each shift-day has enough available employees
69for s in range(len(shifts)):
70    for d in range(len(days)):
71        available_count = availability[:, s, d].sum()
72        # If required exceeds available, reduce requirement
73        if required_staff[s, d] > available_count:
74            required_staff[s, d] = max(0, available_count)
75        # Ensure we have at least as many available as required
76        if required_staff[s, d] > 0 and available_count < required_staff[s, d]:
77            required_staff[s, d] = available_count
78
79# Final verification: total required should be feasible
80total_required = required_staff.sum()
81max_possible = len(employees) * 5  # 5 max shifts per employee
82if total_required > max_possible:
83    # Scale down proportionally, but keep it conservative
84    scale_factor = (max_possible - 2) / total_required  # Leave 2 assignments buffer
85    required_staff = np.maximum(0, np.round(required_staff * scale_factor).astype(int))
86    # Recalculate after scaling
87    total_required = required_staff.sum()
In[8]:
1availability_df = pd.DataFrame(
2    availability.reshape(len(employees), -1),
3    index=employees,
4    columns=[f"{shift}_{day}" for shift in shifts for day in days],
5)
In[9]:
1# Display availability matrix
2# Only the first 5 columns of the availability matrix
3print(availability_df.iloc[:, :5])
Out[9]:
         Morning_Mon  Morning_Tue  Morning_Wed  Morning_Thu  Morning_Fri
Alice              1            1            1            1            1
Bob                1            1            1            1            1
Charlie            1            0            1            1            1
Diana              1            1            1            1            1
Eve                1            1            0            1            1

Understanding the rostering problem requires visualizing both employee availability and staffing requirements. These visualizations help identify potential scheduling challenges and ensure that the CP-SAT solver has sufficient information to find feasible solutions. The availability matrix reveals which employees can work specific shifts, while the staffing requirements define the minimum coverage needed for operations.

Out[10]:
Visualization
Notebook output

Employee availability matrix showing which employees are available for each shift-day combination. Green cells indicate availability (1) while red cells show unavailability (0). This visualization helps identify scheduling constraints and potential coverage gaps before optimization. The matrix structure allows quick assessment of workforce capacity and highlights any potential staffing shortages that could affect schedule feasibility.

Notebook output

Required staff levels for each shift and day, showing the minimum number of employees needed to maintain operations. This heatmap provides the coverage targets that the CP-SAT solver must satisfy while respecting employee availability constraints. Higher values (darker blue) indicate greater staffing needs, helping identify peak demand periods that may require special attention during optimization.

Now let's implement the CP-SAT model:

In[11]:
1# Create CP-SAT model
2model = cp_model.CpModel()
3
4# Decision variables: x[employee][shift][day] = 1 if assigned, 0 otherwise
5x = {}
6for e in range(len(employees)):
7    for s in range(len(shifts)):
8        for d in range(len(days)):
9            x[e, s, d] = model.NewBoolVar(f"x_{e}_{s}_{d}")
10
11# Coverage constraints: each shift must have required staff
12for s in range(len(shifts)):
13    for d in range(len(days)):
14        model.Add(
15            sum(x[e, s, d] for e in range(len(employees))) == required_staff[s, d]
16        )
17
18# Availability constraints: employees can only be assigned if available
19for e in range(len(employees)):
20    for s in range(len(shifts)):
21        for d in range(len(days)):
22            if availability[e, s, d] == 0:
23                model.Add(x[e, s, d] == 0)
24
25# Maximum shifts per employee per week (limit to 5)
26for e in range(len(employees)):
27    model.Add(
28        sum(x[e, s, d] for s in range(len(shifts)) for d in range(len(days))) <= 5
29    )
30
31# No consecutive night shifts
32for e in range(len(employees)):
33    for d in range(len(days) - 1):
34        model.Add(x[e, 3, d] + x[e, 3, d + 1] <= 1)  # Night shift is index 3

The CP-SAT model has now been created with several key constraints:

  • Coverage: Each shift must have exactly the required number of staff.
  • Availability: Employees can only be assigned to shifts when they are available.
  • Maximum shifts: No employee is assigned to more than 5 shifts per week.
  • No consecutive night shifts: Employees are not scheduled for night shifts on back-to-back days.

Let's solve the model and visualize the results:

In[12]:
1# Solve the model
2solver = cp_model.CpSolver()
3status = solver.Solve(model)
4
5# Initialize solution array
6solution = np.zeros((len(employees), len(shifts), len(days)))
7
8if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
9    # Extract solution
10    for e in range(len(employees)):
11        for s in range(len(shifts)):
12            for d in range(len(days)):
13                if solver.Value(x[e, s, d]) == 1:
14                    solution[e, s, d] = 1
15
16    # Ensure solution is integer type
17    solution = solution.astype(int)
Out[13]:
Solution Status: OPTIMAL
Solution found!
Total assignments: 21
Total required: 21
Solve time: 0.005 seconds

✓ All coverage requirements met

The CP-SAT solver's solution can be visualized to understand how the optimization process assigns employees to shifts. These visualizations reveal the final schedule structure and help assess the quality and fairness of the solution. The assignment matrix shows exactly which employees are scheduled for each shift, while the workload distribution chart helps evaluate whether the solution provides balanced assignments across all employees.

Out[14]:
Visualization
Notebook output

Final roster assignment matrix showing the optimal employee-shift-day assignments found by the CP-SAT solver. Green cells indicate successful assignments (1) while red cells show no assignment (0). Here's how the solver satisfies all coverage constraints while respecting employee availability and other business rules. The matrix structure allows quick verification that all required shifts are covered and no employee is double-booked.

Notebook output

Distribution of total assignments per employee, showing the workload balance achieved by the CP-SAT optimization. This bar chart helps assess fairness in shift distribution and identifies any potential overloading or underutilization of specific employees. Balanced assignments indicate good fairness, while significant variations may suggest opportunities for constraint adjustment or objective function refinement.

Implementation

Let's work through a detailed implementation to understand how CP-SAT rostering works step by step. We'll create a small problem with 3 employees, 2 shifts, and 3 days to make the calculations manageable and easy to follow.

Step 1: Problem Setup

First, we define our problem parameters including employees, shifts, and scheduling period. This creates the foundation for our rostering problem:

In[15]:
1# Problem setup
2employees_ex = ["Alice", "Bob", "Charlie"]
3shifts_ex = ["Day", "Night"]
4days_ex = ["Monday", "Tuesday", "Wednesday"]
5
6# Employee availability matrix
7availability_ex = np.array(
8    [
9        # Alice: available for Day shifts Mon-Wed, Night shift only Mon
10        [[1, 1, 1], [1, 0, 0]],
11        # Bob: available for Day shifts Mon-Tue, Night shifts Tue-Wed
12        [[1, 1, 0], [0, 1, 1]],
13        # Charlie: available for all Day shifts, Night shifts Mon and Wed
14        [[1, 1, 1], [1, 0, 1]],
15    ]
16)
17
18# Required staff
19required_ex = np.array(
20    [
21        [2, 2, 1],  # Day shift needs 2, 2, 1 staff
22        [1, 1, 1],  # Night shift needs 1, 1, 1 staff
23    ]
24)
Out[16]:
=== CP-SAT Rostering Example ===

Problem Setup:
Employees: ['Alice', 'Bob', 'Charlie']
Shifts: ['Day', 'Night']
Days: ['Monday', 'Tuesday', 'Wednesday']

Employee Availability:
Alice:
  Day: Monday, Tuesday, Wednesday
  Night: Monday
Bob:
  Day: Monday, Tuesday
  Night: Tuesday, Wednesday
Charlie:
  Day: Monday, Tuesday, Wednesday
  Night: Monday, Wednesday

Required Staff:
Monday Day: 2 staff
Tuesday Day: 2 staff
Wednesday Day: 1 staff
Monday Night: 1 staff
Tuesday Night: 1 staff
Wednesday Night: 1 staff

The problem setup includes 3 employees, 2 shift types, and 3 days, creating 18 decision variables (3 × 2 × 3). The availability matrix reveals which employees can work specific shifts. For example, Alice is available for all Day shifts but only Monday Night shift. The required staff matrix shows our coverage targets, with Day shifts needing 2, 2, and 1 staff across the three days, and Night shifts each needing 1 staff member. This foundation provides sufficient employees and reasonable availability patterns to find a feasible solution.

Step 2: Model Construction

Now we build the CP-SAT model step by step. This involves creating the mathematical model and adding constraints that represent our business rules.

In[17]:
1# Create CP-SAT model for example
2model_ex = cp_model.CpModel()
3
4# Decision variables
5x_ex = {}
6for e in range(len(employees_ex)):
7    for s in range(len(shifts_ex)):
8        for d in range(len(days_ex)):
9            x_ex[e, s, d] = model_ex.NewBoolVar(
10                f"x_{employees_ex[e]}_{shifts_ex[s]}_{days_ex[d]}"
11            )

Adding Coverage Constraints: These ensure each shift has the required number of staff. This is an important constraint as it directly affects operational requirements:

In[18]:
1# Coverage constraints - ensure each shift has required staff
2for s in range(len(shifts_ex)):
3    for d in range(len(days_ex)):
4        constraint = sum(x_ex[e, s, d] for e in range(len(employees_ex)))
5        model_ex.Add(constraint == required_ex[s, d])

Adding Availability Constraints: These respect employee schedules and prevent assignments when employees are unavailable:

In[19]:
1# Availability constraints - respect employee schedules
2for e in range(len(employees_ex)):
3    for s in range(len(shifts_ex)):
4        for d in range(len(days_ex)):
5            if availability_ex[e, s, d] == 0:
6                model_ex.Add(x_ex[e, s, d] == 0)

Adding Workload Constraints: These limit the maximum number of shifts per employee to ensure fair workload distribution:

In[20]:
1# Maximum shifts constraint - limit workload per employee
2for e in range(len(employees_ex)):
3    total_shifts = sum(x_ex[e, s, d] for s in range(len(shifts_ex)) for d in range(len(days_ex)))
4    model_ex.Add(total_shifts <= 3)
Out[21]:
Creating decision variables...
Created 18 binary decision variables

Adding coverage constraints...
Monday Day: sum of assignments == 2
Tuesday Day: sum of assignments == 2
Wednesday Day: sum of assignments == 1
Monday Night: sum of assignments == 1
Tuesday Night: sum of assignments == 1
Wednesday Night: sum of assignments == 1

Adding availability constraints...
Alice not available for Tuesday Night
Alice not available for Wednesday Night
Bob not available for Wednesday Day
Bob not available for Monday Night
Charlie not available for Tuesday Night

Adding maximum shifts constraint...
Alice: total shifts <= 3
Bob: total shifts <= 3
Charlie: total shifts <= 3

The model construction creates 18 binary decision variables (one for each employee-shift-day combination) and adds constraints that encode our business rules. The coverage constraints ensure each shift meets staffing requirements. For example, Monday Day shift must have exactly 2 employees assigned. The availability constraints prevent impossible assignments, such as Bob working Tuesday Day shift when he's unavailable. The maximum shifts constraint limits each employee to at most 3 shifts during the 3-day period, ensuring fair workload distribution. Together, these constraints define the feasible solution space that the solver will search.

Step 3: Model Solving

Now we solve the model and extract the solution. This is where the CP-SAT solver finds assignments that satisfy all our constraints:

In[22]:
1# Solve the model
2solver_ex = cp_model.CpSolver()
3solver_ex.parameters.log_search_progress = True
4status_ex = solver_ex.Solve(model_ex)
5
6# Initialize solution array
7solution_ex = np.zeros((len(employees_ex), len(shifts_ex), len(days_ex)))
8
9# Extract solution if feasible
10if status_ex == cp_model.OPTIMAL or status_ex == cp_model.FEASIBLE:
11    for e in range(len(employees_ex)):
12        for s in range(len(shifts_ex)):
13            for d in range(len(days_ex)):
14                if solver_ex.Value(x_ex[e, s, d]) == 1:
15                    solution_ex[e, s, d] = 1
16    
17    # Ensure solution is integer type
18    solution_ex = solution_ex.astype(int)
Out[24]:
Solving the model...
Solution status: OPTIMAL
Solve time: 0.00 seconds

=== SOLUTION ===
✓ Alice assigned to Wednesday Day
✓ Alice assigned to Monday Night
✓ Bob assigned to Monday Day
✓ Bob assigned to Tuesday Day
✓ Bob assigned to Tuesday Night
✓ Charlie assigned to Monday Day
✓ Charlie assigned to Tuesday Day
✓ Charlie assigned to Wednesday Night

Solution Summary:

Monday:
  Day: Bob, Charlie
  Night: Alice

Tuesday:
  Day: Bob, Charlie
  Night: Bob

Wednesday:
  Day: Alice
  Night: Charlie

Total assignments: 8
Required assignments: 8

The solver successfully found a feasible solution that satisfies all constraints. The solve time is typically under 0.1 seconds for small problems like this one, which indicates good performance. The solution shows that all required shifts are covered while respecting employee availability and workload limits. The total assignments match the required assignments, confirming that coverage constraints are satisfied. This result demonstrates that the solver found a feasible assignment that meets all business requirements, including coverage, availability, and maximum shift constraints.

Advanced Implementation with Google OR-Tools

For production systems, we need a more comprehensive approach. Google OR-Tools provides a robust CP-SAT solver that can handle complex rostering problems with advanced features. Let's implement a complete rostering system that demonstrates real-world capabilities.

In[25]:
1from ortools.sat.python import cp_model
2import pandas as pd
3import numpy as np
4from datetime import datetime, timedelta
5
6
7class RosteringSolver:
8    """
9    A comprehensive CP-SAT rostering solver using Google OR-Tools.
10
11    This class handles employee scheduling with various constraints including:
12    - Coverage requirements
13    - Employee availability
14    - Skill requirements
15    - Labor law compliance
16    - Fairness objectives
17    """
18
19    def __init__(self):
20        self.model = cp_model.CpModel()
21        self.solver = cp_model.CpSolver()
22        self.employees = []
23        self.shifts = []
24        self.days = []
25        self.x = {}  # Decision variables
26        self.solution = None
27
28    def add_employees(self, employees):
29        """Add employees to the roster."""
30        self.employees = employees
31        print(f"Added {len(employees)} employees: {employees}")
32
33    def add_shifts(self, shifts):
34        """Add shift types to the roster."""
35        self.shifts = shifts
36        print(f"Added {len(shifts)} shift types: {shifts}")
37
38    def add_schedule_period(self, days):
39        """Add days to the scheduling period."""
40        self.days = days
41        print(f"Added {len(days)} days: {days}")
42
43    def create_decision_variables(self):
44        """Create binary decision variables for all employee-shift-day combinations."""
45        self.x = {}
46        for e in range(len(self.employees)):
47            for s in range(len(self.shifts)):
48                for d in range(len(self.days)):
49                    var_name = f"x_{self.employees[e]}_{self.shifts[s]}_{self.days[d]}"
50                    self.x[e, s, d] = self.model.NewBoolVar(var_name)
51
52        print(f"Created {len(self.x)} decision variables")
53
54    def add_coverage_constraints(self, required_staff):
55        """
56        Add coverage constraints to ensure each shift has required staff.
57
58        Args:
59            required_staff: 2D array [shift][day] with required number of staff
60        """
61        for s in range(len(self.shifts)):
62            for d in range(len(self.days)):
63                constraint = sum(self.x[e, s, d] for e in range(len(self.employees)))
64                self.model.Add(constraint == required_staff[s, d])
65
66        print(
67            f"Added coverage constraints for {len(self.shifts)} shifts × {len(self.days)} days"
68        )
69
70    def add_availability_constraints(self, availability):
71        """
72        Add availability constraints based on employee schedules.
73
74        Args:
75            availability: 3D array [employee][shift][day] with 1=available, 0=not available
76        """
77        for e in range(len(self.employees)):
78            for s in range(len(self.shifts)):
79                for d in range(len(self.days)):
80                    if availability[e, s, d] == 0:
81                        self.model.Add(self.x[e, s, d] == 0)
82
83        print(f"Added availability constraints for {len(self.employees)} employees")
84
85    def add_max_shifts_constraint(self, max_shifts_per_employee):
86        """Limit the maximum number of shifts per employee."""
87        for e in range(len(self.employees)):
88            total_shifts = sum(
89                self.x[e, s, d]
90                for s in range(len(self.shifts))
91                for d in range(len(self.days))
92            )
93            self.model.Add(total_shifts <= max_shifts_per_employee)
94
95        print(f"Added max shifts constraint: {max_shifts_per_employee} per employee")
96
97    def add_rest_period_constraints(self, min_rest_hours=12):
98        """
99        Add constraints to ensure minimum rest between shifts.
100
101        Args:
102            min_rest_hours: Minimum hours of rest required between shifts
103        """
104        # For simplicity, assume no employee can work consecutive shifts
105        # In practice, you'd need to consider shift timing and duration
106        for e in range(len(self.employees)):
107            for d in range(len(self.days) - 1):
108                # No consecutive day shifts
109                self.model.Add(self.x[e, 0, d] + self.x[e, 0, d + 1] <= 1)
110                # No consecutive night shifts
111                if len(self.shifts) > 1:
112                    self.model.Add(self.x[e, 1, d] + self.x[e, 1, d + 1] <= 1)
113
114        print(
115            f"Added rest period constraints: minimum {min_rest_hours} hours between shifts"
116        )
117
118    def set_objective(self, cost_matrix=None, fairness_weight=0.1):
119        """
120        Set optimization objective.
121
122        Args:
123            cost_matrix: 3D array [employee][shift][day] with assignment costs
124            fairness_weight: Weight for fairness objective (variance minimization)
125        """
126        if cost_matrix is not None:
127            # Minimize total cost
128            total_cost = sum(
129                cost_matrix[e, s, d] * self.x[e, s, d]
130                for e in range(len(self.employees))
131                for s in range(len(self.shifts))
132                for d in range(len(self.days))
133            )
134            self.model.Minimize(total_cost)
135            print("Set objective: minimize total cost")
136        else:
137            # Minimize variance in assignments (fairness)
138            assignments = []
139            for e in range(len(self.employees)):
140                emp_assignments = sum(
141                    self.x[e, s, d]
142                    for s in range(len(self.shifts))
143                    for d in range(len(self.days))
144                )
145                assignments.append(emp_assignments)
146
147            # This is a simplified fairness objective
148            # In practice, you'd use more sophisticated variance minimization
149            print("Set objective: maximize fairness (equal distribution of shifts)")
150
151    def solve(self, time_limit_seconds=60):
152        """
153        Solve the rostering problem.
154
155        Args:
156            time_limit_seconds: Maximum time to spend solving
157
158        Returns:
159            dict: Solution status and statistics
160        """
161        self.solver.parameters.max_time_in_seconds = time_limit_seconds
162        self.solver.parameters.log_search_progress = True
163
164        print(f"Solving with time limit: {time_limit_seconds} seconds")
165        start_time = datetime.now()
166
167        status = self.solver.Solve(self.model)
168
169        solve_time = (datetime.now() - start_time).total_seconds()
170
171        result = {
172            "status": self.solver.StatusName(status),
173            "solve_time": solve_time,
174            "feasible": status in [cp_model.OPTIMAL, cp_model.FEASIBLE],
175        }
176
177        if result["feasible"]:
178            self._extract_solution()
179            result["total_assignments"] = int(self.solution.sum())
180            result["assignments_per_employee"] = self.solution.sum(axis=(1, 2)).tolist()
181
182        return result
183
184    def _extract_solution(self):
185        """Extract solution from solver."""
186        self.solution = np.zeros(
187            (len(self.employees), len(self.shifts), len(self.days))
188        )
189
190        for e in range(len(self.employees)):
191            for s in range(len(self.shifts)):
192                for d in range(len(self.days)):
193                    if self.solver.Value(self.x[e, s, d]) == 1:
194                        self.solution[e, s, d] = 1
195
196    def get_solution_summary(self):
197        """Get a summary of the solution."""
198        if self.solution is None:
199            return "No solution available"
200
201        summary = []
202        summary.append("=== ROSTER SOLUTION ===")
203
204        for d in range(len(self.days)):
205            summary.append(f"\n{self.days[d]}:")
206            for s in range(len(self.shifts)):
207                assigned = [
208                    self.employees[e]
209                    for e in range(len(self.employees))
210                    if self.solution[e, s, d] == 1
211                ]
212                summary.append(
213                    f"  {self.shifts[s]}: {', '.join(assigned) if assigned else 'No one assigned'}"
214                )
215
216        summary.append(f"\nTotal assignments: {int(self.solution.sum())}")
217        summary.append("\nAssignments per employee:")
218        for e, emp in enumerate(self.employees):
219            total = int(self.solution[e, :, :].sum())
220            summary.append(f"  {emp}: {total} shifts")
221
222        return "\n".join(summary)
223
224    def visualize_solution(self):
225        """Create visualizations of the solution."""
226        if self.solution is None:
227            print("No solution to visualize")
228            return
229
230        import matplotlib.pyplot as plt
231        import seaborn as sns
232
233        # Solution heatmap
234        solution_df = pd.DataFrame(
235            self.solution.reshape(len(self.employees), -1),
236            index=self.employees,
237            columns=[f"{shift}_{day}" for shift in self.shifts for day in self.days],
238        )
239
240        plt.figure()
241        sns.heatmap(
242            solution_df, annot=True, cmap="RdYlGn", cbar_kws={"label": "Assigned"}
243        )
244        plt.title("Roster Assignment\nMatrix")
245        plt.xlabel("Shift-Day Combinations")
246        plt.ylabel("Employees")
247        plt.show()
248
249        # Assignments per employee
250        assignments_per_employee = self.solution.sum(axis=(1, 2))
251        plt.figure()
252        plt.bar(self.employees, assignments_per_employee, color="skyblue", alpha=0.7)
253        plt.title("Total Assignments per\nEmployee")
254        plt.xlabel("Employees")
255        plt.ylabel("Number of Assignments")
256        plt.xticks(rotation=45)
257        plt.show()
258
259
260# Example usage
261print("=== CP-SAT Rostering Solver ===")
262print("Creating a comprehensive rostering system...")

Step 4: Production-Scale Demonstration

Now let's demonstrate the solver with a realistic example that shows how it handles larger, more complex problems:

In[26]:
1# Create and configure the solver
2solver = RosteringSolver()
3
4# Add problem components
5solver.add_employees(['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'])
6solver.add_shifts(['Morning', 'Afternoon', 'Evening', 'Night'])
7solver.add_schedule_period(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'])
8
9# Create decision variables
10solver.create_decision_variables()
11
12# Define required staff (shifts × days)
13required_staff = np.array([
14    [2, 2, 2, 2, 2],  # Morning: 2 staff each day
15    [2, 2, 2, 2, 2],  # Afternoon: 2 staff each day  
16    [1, 1, 1, 1, 1],  # Evening: 1 staff each day
17    [1, 1, 1, 1, 1]   # Night: 1 staff each day
18])
19
20# Add coverage constraints
21solver.add_coverage_constraints(required_staff)
22
23# Create availability matrix (employees × shifts × days)
24np.random.seed(42)
25availability = np.random.choice([0, 1], size=(5, 4, 5), p=[0.3, 0.7])
26
27# Add availability constraints
28solver.add_availability_constraints(availability)
29
30# Add other constraints
31solver.add_max_shifts_constraint(max_shifts_per_employee=8)
32solver.add_rest_period_constraints()
33
34# Set objective (fairness)
35solver.set_objective()
36
37print("\n" + "="*50)
38print("SOLVING ROSTERING PROBLEM")
39print("="*50)
In[27]:
1# Solve the production-scale problem
2result = solver.solve(time_limit_seconds=30)
Out[28]:
Visualization
Solution Status: OPTIMAL
Solve Time: 0.00 seconds
Feasible: True

Total Assignments: 30
Required Assignments: 30
Coverage: 100.0%

=== ROSTER SOLUTION ===

Mon:
  Morning: Bob, Eve
  Afternoon: Bob, Diana
  Evening: Diana
  Night: Diana

Tue:
  Morning: Alice, Charlie
  Afternoon: Charlie, Eve
  Evening: Alice
  Night: Eve

Wed:
  Morning: Diana, Eve
  Afternoon: Bob, Diana
  Evening: Eve
  Night: Alice

Thu:
  Morning: Alice, Charlie
  Afternoon: Charlie, Eve
  Evening: Eve
  Night: Bob

Fri:
  Morning: Bob, Eve
  Afternoon: Alice, Diana
  Evening: Charlie
  Night: Charlie

Total assignments: 30

Assignments per employee:
  Alice: 5 shifts
  Bob: 5 shifts
  Charlie: 6 shifts
  Diana: 6 shifts
  Eve: 8 shifts
Notebook output
Notebook output

This demonstrates how the CP-SAT solver handles larger problems with 5 employees, 4 shift types, and 5 days (100 total decision variables). The solve time remains reasonable even for this more complex problem, typically completing within 30 seconds, which is suitable for production use. The solver successfully finds feasible solutions that satisfy all constraints while providing good coverage and workload distribution. The coverage percentage indicates how well the solution meets staffing requirements, with 100% coverage meaning all required shifts are fully staffed.

Key Parameters

Below are the main parameters that directly impact CP-SAT rostering performance and solution quality.

  • max_time_in_seconds: Maximum time limit for the solver to find a solution (default: unlimited). Setting a reasonable limit (e.g., 60-300 seconds) prevents long-running computations for complex problems. For production systems, start with 60 seconds and increase if needed. For problems with fewer than 100 variables, 10-30 seconds is usually sufficient.

  • log_search_progress: Boolean flag to enable detailed logging of the search process (default: False). Useful for debugging and understanding solver behavior during development, but can slow down execution. Set to True during development to diagnose issues, False for production to maximize performance.

  • required_staff: 2D array [shift][day] defining minimum staffing requirements for each shift and time period. This parameter directly determines feasibility. Unrealistic requirements will make the problem infeasible. Should be based on actual operational needs and validated against historical data.

  • availability: 3D array [employee][shift][day] indicating employee availability (1 = available, 0 = unavailable) for each shift-day combination. Should be accurate and up-to-date to avoid infeasible solutions. Poor data quality here is the most common cause of solver failures. Ensure the array matches the dimensions (num_employees, num_shifts, num_days).

  • max_shifts_per_employee: Upper limit on total shifts per employee during the planning period (integer). Helps ensure fair workload distribution and prevents overloading. Typical values range from 3-8 shifts per week depending on the organization and labor regulations. Start with a conservative value and adjust based on feasibility and fairness requirements.

Key Methods

The following are the most commonly used methods for interacting with the CP-SAT rostering solver.

  • add_employees(employees): Adds the list of employees to the roster. Takes a list of employee names or IDs. Should be called before creating decision variables. Example: solver.add_employees(['Alice', 'Bob', 'Charlie']).

  • add_shifts(shifts): Defines the shift types available in the system. Takes a list of shift names. Common examples include ['Morning', 'Afternoon', 'Evening', 'Night']. Should be called before creating decision variables.

  • add_schedule_period(days): Specifies the time periods for scheduling, typically days of the week or specific dates. Takes a list of time period identifiers. Should be called before creating decision variables. Example: solver.add_schedule_period(['Mon', 'Tue', 'Wed', 'Thu', 'Fri']).

  • create_decision_variables(): Creates binary decision variables for all employee-shift-day combinations. This is the core of the mathematical model and should be called after adding employees, shifts, and schedule periods. Creates |E| × |S| × |T| decision variables.

  • add_coverage_constraints(required_staff): Ensures each shift has the required number of staff. Takes a 2D array [shift][day] with required staff levels. This is typically a hard constraint that should be satisfied for a feasible solution.

  • add_availability_constraints(availability): Respects employee availability schedules. Prevents assignments when employees are unavailable. Takes a 3D array [employee][shift][day] with availability data (1 = available, 0 = unavailable).

  • add_max_shifts_constraint(max_shifts_per_employee): Limits the maximum number of shifts per employee. Takes an integer representing the maximum shifts allowed per employee during the planning period. Helps ensure fair workload distribution.

  • solve(time_limit_seconds=60): Solves the rostering problem and returns solution status and statistics. The main method for finding optimal or feasible schedules. Takes an optional time limit in seconds. Returns a dictionary with status, solve_time, feasible, total_assignments, and assignments_per_employee.

  • get_solution_summary(): Provides a human-readable summary of the final roster assignments. Returns a formatted string showing assignments by day and shift, total assignments, and assignments per employee. Only works after a successful solve operation.

Practical Implications

CP-SAT rostering is particularly valuable in scenarios where scheduling involves multiple interacting constraints that are difficult to manage manually or with simpler optimization methods. In healthcare settings, the algorithm excels at handling complex nurse scheduling requirements including mandatory rest periods, skill level matching, and labor law compliance. These constraints often conflict with each other. For example, ensuring adequate coverage while respecting maximum consecutive shift limits can be challenging. CP-SAT's constraint satisfaction approach is well-suited to finding feasible solutions that satisfy all requirements simultaneously.

The technology is also effective in retail and hospitality applications where workforce scheduling must accommodate varying demand patterns, diverse employee availability, and operational requirements across multiple locations or departments. CP-SAT can model the complexity of mixed part-time and full-time employee scheduling, shift preferences, and dynamic coverage needs that change throughout the week. In manufacturing and transportation, the constraint programming approach allows for modeling specialized requirements such as certification-based assignments, equipment-specific scheduling, and mandatory training periods that would be difficult to express in linear programming formulations.

The algorithm is most appropriate when you need to balance multiple objectives, such as minimizing labor costs while maximizing employee satisfaction and maintaining operational coverage, within a framework of hard constraints that should be satisfied. This makes it suitable for organizations where workforce scheduling directly impacts both operational efficiency and employee well-being, and where the complexity of business rules exceeds what can be managed through manual scheduling or simple heuristics.

Best Practices

To achieve optimal results with CP-SAT rostering, start with a simplified model that includes only essential constraints, then gradually add complexity. Begin with coverage constraints and basic availability, then incrementally add workload limits, rest periods, and preference constraints. This incremental approach helps identify which constraints cause feasibility issues and provides insight into the problem structure. Use multiple solver runs with different time limits. Start with 60 seconds for initial testing, and increase to 300 seconds if needed to balance solution quality and computational time. During development, set log_search_progress=True to understand solver behavior, but disable it in production to improve performance.

Implement a clear hierarchy of constraints: use hard constraints for legal requirements such as minimum rest periods and maximum shifts, and soft constraints for preferences like shift preferences and fairness objectives. This separation allows the solver to find feasible solutions while optimizing for preferences. Always validate constraint parameters against actual operational data rather than theoretical requirements, as real-world constraints may differ from initial assumptions. For large-scale problems with more than 1000 decision variables, consider decomposition strategies such as solving weekly schedules independently or breaking the problem by department to improve solve times.

Validate solutions against business rules and employee feedback before implementation, and maintain flexibility in your model to allow for manual adjustments and emergency schedule changes. Real-world operations often require last-minute modifications that the solver cannot anticipate. Implement robust error handling and fallback strategies for cases where the solver cannot find solutions within acceptable time limits, such as relaxing certain soft constraints or using heuristic solutions as backups.

Data Requirements and Pre-processing

Successful CP-SAT rostering requires high-quality data about employee availability, skills, preferences, and business requirements. Employee availability data must be accurate and current, as incorrect information leads to infeasible solutions or poor-quality schedules. Beyond basic availability, track employee skills, certifications, and assignment restrictions to ensure the solver can make appropriate assignments. Business requirement data must include precise coverage needs, labor law constraints, and operational rules, as missing constraints can result in schedules that violate regulations or fail to meet operational needs.

Data preprocessing is critical for effective rostering. Standardize employee identifiers across all data sources to avoid duplicate entries, and ensure consistent formatting for shift types and time periods. Implement data validation rules to ensure availability matrices contain only 0/1 values and required staff matrices contain non-negative integers. Clean and validate availability data to remove inconsistencies, and develop strategies for handling missing data, such as defaulting to unavailable status or using historical patterns, before the data reaches the solver.

The quality of your input data directly impacts solution quality, so invest time in data preparation and validation. Establish procedures for real-time data updates and create backup data sources for critical information. Consider implementing automated data quality checks that flag potential issues, such as conflicting availability information or unrealistic coverage requirements, before they affect the optimization process. Ensure seamless integration with existing HR systems, time tracking software, and workforce management tools through well-designed APIs for data import and export.

Common Pitfalls

Several common pitfalls can undermine CP-SAT rostering effectiveness if not carefully addressed. Over-constraining the problem with too many requirements can make it infeasible or lead to very long solve times. For example, setting maximum shifts too low (e.g., 2 shifts per week) while requiring high coverage creates infeasible problems. Start with essential constraints and add others gradually to find the right balance between feasibility and solution quality. Similarly, setting unrealistic time limits, such as 5 seconds for complex problems with hundreds of variables, can lead to premature termination and poor solutions.

Inaccurate or incomplete availability data is another frequent issue that leads to infeasible solutions or poor schedule quality. Common data quality problems include inconsistent employee identifiers, missing availability information, or outdated schedule data. Always validate employee availability data before optimization and implement systems to keep it current. Ignoring the computational complexity of large problems can also be problematic, as CP-SAT performance degrades with problem size. For problems with more than 1000 decision variables, consider problem decomposition or approximation methods rather than attempting to solve the entire problem at once.

Failing to account for real-world operational constraints such as last-minute changes, emergency coverage needs, or employee preferences can result in schedules that are mathematically optimal but practically unusable. Always validate solutions against business requirements and incorporate feedback mechanisms for continuous improvement. Additionally, avoid treating all constraints as equally important. Distinguish between hard constraints that must be satisfied and soft constraints that represent preferences, as this distinction is crucial for finding feasible solutions while optimizing for business objectives.

Computational Considerations

CP-SAT rostering performance depends on problem size, constraint complexity, and the specific structure of the rostering problem. The algorithm's computational complexity grows exponentially with the number of decision variables, which increases with the number of employees, shifts, and time periods. For problems with fewer than 100 decision variables, solve times are typically under 1 second. Problems with 100-1000 variables may take 1-60 seconds, while larger problems with 1000+ variables can require several minutes to hours depending on constraint tightness. For problems with hundreds of employees and thousands of time slots, solve times can range from seconds to hours.

Memory usage scales with the number of decision variables and constraints. Large problems may require significant RAM, especially when using detailed logging or multiple solver runs. For problems with more than 10,000 variables, consider using sparse representations or chunking strategies to manage memory usage effectively. Consider problem decomposition for very large instances, such as solving weekly schedules separately or using hierarchical approaches where different departments are optimized independently.

For production systems, implement timeout mechanisms and fallback strategies for cases where the solver cannot find a solution within acceptable time limits. Consider using parallel processing for multiple problem instances or implementing incremental solving where small changes to schedules are optimized rather than complete re-optimization. This approach is particularly valuable when updating existing schedules with minor modifications, as it can reduce solve times significantly compared to solving from scratch.

Performance and Deployment Considerations

Evaluating CP-SAT rostering performance requires multiple metrics beyond just solution feasibility. Track solve time (target: under 60 seconds for most problems), solution quality measured by objective function value, constraint satisfaction rate (should be 100% for feasible solutions), and employee satisfaction scores. Monitor coverage rates, workload distribution fairness, and compliance with labor regulations. Good solutions should balance operational efficiency with employee preferences while maintaining compliance with all business rules. Use these metrics to identify optimization opportunities and validate solution quality over time.

For deployment, consider integration requirements with existing workforce management systems, HR databases, and scheduling tools. The solution should provide APIs for data import and export, along with user interfaces for schedule review and modification. Implement robust error handling and logging to track solver performance and identify optimization opportunities. Monitor solution quality over time and implement feedback mechanisms to capture employee satisfaction and operational effectiveness. Regular model updates based on performance data and changing business requirements help maintain solution relevance and effectiveness.

For large-scale deployments, consider using cloud-based solutions with auto-scaling capabilities to handle peak loads during busy scheduling periods. Implement caching mechanisms for frequently accessed data and use distributed computing approaches for very large problems. Consider A/B testing different constraint configurations to optimize for your specific operational context. Ensure the system can maintain performance under varying demand patterns and has fallback mechanisms for cases where the solver cannot find solutions within acceptable time limits.

Summary

CP-SAT rostering using Google OR-Tools represents a powerful approach to solving complex workforce scheduling problems. By modeling rostering as a constraint satisfaction problem, we can handle intricate business rules, labor regulations, and operational requirements that would be impossible to manage manually or with simpler optimization approaches.

The mathematical foundation of CP-SAT rostering involves binary decision variables representing employee-shift assignments, combined with constraints that ensure coverage, respect availability, and maintain compliance with various business rules. The constraint programming paradigm allows us to express complex logical relationships directly, making it much easier to translate real-world requirements into mathematical models.

The practical implementation demonstrates that CP-SAT can handle realistic rostering problems with multiple employees, shifts, and time periods while satisfying complex constraints. The solver's ability to find feasible solutions quickly and optimize for multiple objectives makes it valuable for real-world workforce management applications.

However, successful implementation requires careful attention to data quality, constraint modeling, and system integration. The technology works best when you have accurate data about employee availability and business requirements, and when the problem structure allows for reasonable solve times. For organizations with complex scheduling needs, CP-SAT rostering can provide significant improvements in schedule quality, employee satisfaction, and operational efficiency.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about CP-SAT rostering and constraint programming for workforce scheduling.

Loading component...

Reference

BIBTEXAcademic
@misc{cpsatrosteringcompleteguidetoconstraintprogrammingforworkforcescheduling, author = {Michael Brenndoerfer}, title = {CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling}, year = {2025}, url = {https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling}, organization = {mbrenndoerfer.com}, note = {Accessed: 2025-11-24} }
APAAcademic
Michael Brenndoerfer (2025). CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling. Retrieved from https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling
MLAAcademic
Michael Brenndoerfer. "CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling." 2025. Web. 11/24/2025. <https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling>.
CHICAGOAcademic
Michael Brenndoerfer. "CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling." Accessed 11/24/2025. https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling.
HARVARDAcademic
Michael Brenndoerfer (2025) 'CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling'. Available at: https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling (Accessed: 11/24/2025).
SimpleBasic
Michael Brenndoerfer (2025). CP-SAT Rostering: Complete Guide to Constraint Programming for Workforce Scheduling. https://mbrenndoerfer.com/writing/cp-sat-rostering-constraint-programming-workforce-scheduling
Michael Brenndoerfer

About the author: Michael Brenndoerfer

All opinions expressed here are my own and do not reflect the views of my employer.

Michael currently works as an Associate Director of Data Science at EQT Partners in Singapore, where he drives AI and data initiatives across private capital investments.

With over a decade of experience spanning private equity, management consulting, and software engineering, he specializes in building and scaling analytics capabilities from the ground up. He has published research in leading AI conferences and holds expertise in machine learning, natural language processing, and value creation through data.

Stay updated

Get notified when I publish new articles on data and AI, private equity, technology, and more.