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.

This article is part of the free-to-read Data Science Handbook
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:
where:
- : the set of all employees (indexed by )
- : the set of all shift types, such as Morning, Afternoon, Evening, and Night (indexed by )
- : the set of all time periods, typically days of the week (indexed by )
- : binary decision variable equal to 1 if employee is assigned to shift in time period , 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 independent yes-or-no decisions. Each decision variable can be either 0 or 1, which means you're navigating a solution space with 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).

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:
This formula says: for every shift type and every time period , the sum of all assignments (where ) should equal the required number of employees . The summation counts how many employees are assigned. We're adding up the ones. Why equality rather than "at least"? Because we can also model "exactly employees" if needed, though in practice you might use to allow overstaffing.
where:
- : the minimum number of employees required for shift in time period (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:
where:
- : the set of all infeasible assignments where employee is unavailable for shift in period (a subset of )
By setting for all , 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:
where:
- : the maximum number of shifts employee can work during the planning period (a non-negative integer, typically set based on labor regulations or organizational policies)
The summation counts all assignments for employee across all shifts and time periods. This sum equals the total number of shifts assigned to employee (since each is either 0 or 1). By requiring this sum to be , 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:
where:
- : the time gap between consecutive shifts (measured in the same units as , typically hours or time periods)
- : the minimum required rest period between shifts (often 8-12 hours, measured in the same units as )
- : two different shift types (the constraint applies to any pair of shifts)
- : the time period of the first shift
- : the time period of the second shift, occurring time units after the first
The constraint works as follows: if an employee works shift at time (so ), and the time gap is less than the minimum rest period , then they cannot work shift at time (forcing ). 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 , they cannot work another shift at time if the time gap is less than the minimum rest period .

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:
where:
- : the cost of assigning employee to shift in period (a non-negative real number, typically measured in currency units or preference scores)
The binary nature of works well here: when (no assignment), the term contributes nothing to the sum. When (assignment made), the cost is added. The triple summation ensures we consider every possible assignment, but only the ones that actually occur (where ) contribute to the total cost.
The cost 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 employees, shift types, and time periods, we have binary decision variables. Each variable can independently be 0 or 1, which means the total number of possible assignments is:
where:
- : the set of all possible solutions (before applying constraints)
- : the cardinality (size) of the employee set , i.e., the number of employees
- : the cardinality (size) of the shift type set , i.e., the number of shift types
- : the cardinality (size) of the time period set , i.e., the number of time periods
- : 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 decision variables, creating possible solutions. To put this in perspective, there are estimated to be 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.

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:
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:
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()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()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)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)1# Display availability matrix
2# Only the first 5 columns of the availability matrix
3print(availability_df.iloc[:, :5])1# Display availability matrix
2# Only the first 5 columns of the availability matrix
3print(availability_df.iloc[:, :5])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.

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.

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:
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 31# 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 3The 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:
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)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)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.

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.

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:
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)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)=== 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.
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 )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:
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])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:
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)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:
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)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)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:
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)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)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.
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...")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:
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)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)1# Solve the production-scale problem
2result = solver.solve(time_limit_seconds=30)1# Solve the production-scale problem
2result = solver.solve(time_limit_seconds=30)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


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 toTrueduring development to diagnose issues,Falsefor 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 withstatus,solve_time,feasible,total_assignments, andassignments_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.
Reference

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.
Related Content

Mixed Integer Linear Programming (MILP) for Factory Optimization: Complete Guide with Mathematical Foundations & Implementation
Complete guide to Mixed Integer Linear Programming (MILP) for factory optimization, covering mathematical foundations, constraint modeling, branch-and-bound algorithms, and practical implementation with Google OR-Tools. Learn how to optimize production planning with discrete setup decisions and continuous quantities.

NHITS: Neural Hierarchical Interpolation for Time Series Forecasting with Multi-Scale Decomposition & Implementation
Master NHITS (Neural Hierarchical Interpolation for Time Series), a deep learning architecture for multi-scale time series forecasting. Learn hierarchical decomposition, neural interpolation, and how to implement NHITS for complex temporal patterns in retail, energy, and financial data.

N-BEATS: Neural Basis Expansion Analysis for Time Series Forecasting
Complete guide to N-BEATS, an interpretable deep learning architecture for time series forecasting. Learn how N-BEATS decomposes time series into trend and seasonal components, understand the mathematical foundation, and implement it in PyTorch.
Stay updated
Get notified when I publish new articles on data and AI, private equity, technology, and more.

