,

This is not an ML problem: constraint solvers with OR-Tools CP-SAT

LEARN · RULES, HEURISTICS & SYMBOLIC AI

Imagine you’re building a scheduling system for a fulfillment center.

You have six employees, ten shifts to cover, different qualifications, availability restrictions, workload limits, and individual preferences. Every shift must be staffed. Nobody should work incompatible consecutive shifts. Ideally, everyone gets a reasonably fair schedule.

Your engineering team proposes machine learning.

Perhaps a neural network could learn good schedules from historical examples. Or a recommendation model could rank employees for each shift. Maybe a reinforcement learning agent could discover an assignment policy.

But there’s a fundamental problem with that approach.

You don’t need to predict a good schedule. You need to construct a valid one.

And if several valid schedules exist, you need to find the best according to explicit business objectives.

That’s not primarily a machine learning problem. It’s a constraint optimization problem.

Google’s OR-Tools includes CP-SAT, a solver that can model these decisions directly. Instead of collecting training examples, designing features, optimizing a loss function, and hoping your predictions satisfy business rules, you define the rules and ask the solver to find assignments that satisfy them.

The difference is enormous:

  • A predictive model estimates what might happen.

  • A heuristic constructs an answer using a predefined strategy, without necessarily proving that it’s valid or optimal.

  • A constraint solver searches for assignments that satisfy mathematical requirements.

  • An optimization solver also attempts to find the best feasible assignment and, when possible, prove its optimality.

In this tutorial, we’ll build a runnable employee scheduling system using Python and OR-Tools CP-SAT. We’ll model qualifications, availability, rest restrictions, fairness, and preferences. Then we’ll deliberately make the problem impossible, examine the solver’s response, and explore techniques for taking constraint optimization into production.

By the end, you’ll know how to recognize problems where training another model is unnecessary—and how to replace that complexity with a solver.

1. Prediction versus optimization: choose the right problem

The most important decision happens before writing code.

Ask yourself:

Am I trying to estimate an unknown value, or choose the best action given known requirements?

Those are fundamentally different questions.

When prediction is appropriate

Suppose you operate an online store and want to know how many orders will arrive tomorrow.

Tomorrow’s demand is unknown. Historical data might contain useful patterns involving weekdays, promotions, seasonal effects, and prices.

This is a prediction problem.

A regression model or time-series forecasting system might estimate demand. You evaluate it using held-out data, forecast errors, and real-world performance.

The output is an estimate, not a decision.

When optimization is appropriate

Now suppose you already have 500 confirmed orders waiting to be processed.

You know:

  • Which employees are available.

  • Which machines can process each order.

  • How long each operation takes.

  • Which orders have priority.

  • When orders must be completed.

  • Which resources cannot operate simultaneously.

Your question is no longer how many orders will arrive.

It’s how to allocate the available resources to process those orders.

A predictive model might imitate assignments made by supervisors, but even a model with excellent predictive accuracy could produce overlapping machine reservations, violate deadlines, or overload an employee.

A constraint solver lets you encode these restrictions explicitly.

When you need both

Prediction and optimization are not competitors. They often belong in consecutive stages.

A realistic planning system might work like this:

Historical orders and operational data
                |
                v
      Demand forecasting model
                |
                v
       Estimated workload
                |
                v
   CP-SAT assignment/scheduling
                |
                v
       Feasible operational plan
                |
                v
        Actual execution
                |
                v
      Monitoring and feedback

Machine learning estimates uncertain demand. Optimization decides how to respond to that demand.

A better forecast doesn’t automatically produce a better schedule, and a perfect optimizer cannot correct a badly specified demand forecast.

The problem chooser

Use this quick checklist before choosing your technology.

Your question Start with
How many units will sell next week? Forecasting or regression
Which employee should cover each shift? Constraint optimization
Which jobs should run on which machines? Constraint programming or mixed-integer optimization
What is the probability an order will be returned? Classification
How should inventory be allocated across warehouses? Optimization
Which delivery route should a vehicle take? Vehicle routing optimization
Which items will customers prefer? Recommendation models
How do we schedule staff for uncertain future demand? Forecasting + optimization
Does a new checkout design increase conversions? Experimentation and causal analysis

A useful warning sign is a specification filled with words such as must, cannot, exactly one, at most, before, after, capacity, and minimize.

Those phrases often describe constraints and objectives, not prediction targets.

2. What CP-SAT actually does

CP-SAT combines ideas from constraint programming, Boolean satisfiability (SAT), and mathematical optimization.

Instead of writing a procedural algorithm that chooses one employee, checks the consequences, and repairs mistakes later, you describe the entire problem declaratively.

The model has three essential ingredients.

Decision variables

These represent choices the solver controls.

For example:

  • Whether an employee works a particular shift.

  • Which machine executes a job.

  • When an operation starts.

  • Whether a task should be accepted.

  • How many units to manufacture.

A Boolean variable represents a yes-or-no decision. An integer variable represents a bounded numerical decision.

Hard constraints

These describe conditions that must always hold.

Examples include:

  • Every shift has exactly one assigned employee.

  • An employee cannot work two overlapping shifts.

  • A machine cannot process two jobs simultaneously.

  • An assignment must respect qualifications.

  • Resource usage cannot exceed capacity.

The solver cannot knowingly trade away these requirements to improve the objective.

If no assignment satisfies them all, the mathematical model is infeasible.

The objective function

This describes what to optimize among feasible solutions.

For example:

  • Minimize operating cost.

  • Minimize late deliveries.

  • Minimize preference violations.

  • Minimize total completion time.

  • Maximize fulfilled orders.

A model can also have no objective, in which case CP-SAT searches for a feasible solution.

Importantly, CP-SAT operates over integer-valued decisions and integer coefficients. Monetary amounts and other continuous quantities generally need an appropriate integer representation, such as cents instead of dollars, or carefully scaled measurement units.

Unlike a model trained from examples, CP-SAT does not learn an approximate mapping between inputs and desired outputs. It searches through the possible assignments using constraint propagation, presolve, SAT-based reasoning, and optimization techniques.

This allows it to provide something a normal predictor cannot: an optimality certificate in the form of a proven objective bound, when the search establishes optimality.

There is an important qualification: not every optimization problem can be solved to optimality within a practical time limit. CP-SAT can return a feasible incumbent without having proved it optimal.

We’ll handle that distinction explicitly in our code.

3. Install OR-Tools and prepare the project

We’ll use Python and the modern CP-SAT API exposed through OR-Tools.

As of October 2026, the latest stable PyPI release is OR-Tools 9.15.6755, published in January 2026. The 9.15 release includes CP-SAT improvements, Python-layer optimizations, and Python 3.14 support. We’ll pin that version so the tutorial has a reproducible dependency.

You need:

  • Python 3.11 or newer.

  • A working pip installation.

  • A terminal and text editor.

  • No GPU, machine learning framework, or training dataset.

Create a project directory and virtual environment:

mkdir cp-sat-scheduling
cd cp-sat-scheduling
python -m venv .venv
source .venv/bin/activate

The activation command above is for macOS and Linux shells. Windows uses the appropriate activation script for its shell.

Install the solver:

python -m pip install "ortools==9.15.6755"

Verify the installation:

python -c "import ortools; print(ortools.__version__)"

The expected version is:

9.15.6755

That’s the only third-party package required.

4. Define a real scheduling problem

Let’s model the fulfillment center described earlier.

We have six employees:

  • Ava

  • Ben

  • Cora

  • Dev

  • Eli

  • Fay

The center operates Monday through Friday with two shifts per day: opening and closing.

That gives us ten positions to fill.

Hard business requirements

Every valid solution must satisfy these rules:

  1. Every opening shift has exactly one employee.

  2. Every closing shift has exactly one employee.

  3. Each employee works at most one shift per day.

  4. Only appropriately qualified employees may close.

  5. Approved unavailability must be respected.

  6. Nobody may close one day and open the next.

  7. Every employee receives at least one shift.

  8. No employee receives more than two shifts during the week.

These are non-negotiable rules in our example.

Notice that these requirements involve multiple assignments simultaneously.

For example, choosing Ava for Tuesday’s closing shift can affect who is eligible for Wednesday’s opening shift. Choosing Ben for another day might leave too few available employees later.

This interdependence is why a greedy algorithm can be troublesome.

The globally best assignment may require choosing a locally less attractive employee for one shift so that every other requirement remains satisfiable.

Soft business preferences

We also want to respect employee preferences whenever possible.

Each employee has a preferred shift type. Additionally, some employee-day-shift combinations have specific undesirable-assignment penalties.

We’ll use:

  • 0 points for an assignment matching the preferred shift type.

  • 2 points for an assignment outside that preference.

  • Additional integer penalties for particular unwanted assignments.

Our objective is to minimize total preference penalty while satisfying every hard requirement.

The objective score is not a probability, accuracy metric, or prediction error. It’s a deliberately constructed measure of schedule quality.

5. Build the complete CP-SAT scheduling system

Save the following as schedule.py.

The script is self-contained and includes model construction, optimization, status handling, independent validation, and readable output.

import argparse
from ortools.sat.python import cp_model

DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri")
SHIFTS = ("open", "close")
STAFF = ("Ava", "Ben", "Cora", "Dev", "Eli", "Fay")

CLOSE_QUALIFIED = {"Ava", "Cora", "Dev", "Fay"}

UNAVAILABLE = {
    ("Ava", "Wed"),
    ("Ben", "Fri"),
    ("Cora", "Tue"),
    ("Dev", "Mon"),
    ("Eli", "Thu"),
    ("Fay", "Wed"),
}

PREFERRED_SHIFT = {
    "Ava": "close",
    "Ben": "open",
    "Cora": "close",
    "Dev": "close",
    "Eli": "open",
    "Fay": "close",
}

AVOID = {
    ("Ava", "Fri", "close"): 9,
    ("Ben", "Tue", "open"): 2,
    ("Cora", "Thu", "open"): 3,
    ("Dev", "Wed", "close"): 3,
    ("Fay", "Mon", "close"): 5,
}

def solve_schedule(max_shifts: int) -> None:
    model = cp_model.CpModel()

    # One Boolean variable for every possible assignment.
    assigned = {
        (person, day, shift): model.new_bool_var(
            f"{person}_{day}_{shift}"
        )
        for person in STAFF
        for day in DAYS
        for shift in SHIFTS
    }

    # Each shift requires exactly one employee.
    for day in DAYS:
        for shift in SHIFTS:
            model.add_exactly_one(
                assigned[p, day, shift] for p in STAFF
            )

    # Availability, qualifications, and daily workload.
    for person in STAFF:
        for day in DAYS:
            model.add_at_most_one(
                assigned[person, day, s] for s in SHIFTS
            )

            if (person, day) in UNAVAILABLE:
                for shift in SHIFTS:
                    model.add(
                        assigned[person, day, shift] == 0
                    )

            if person not in CLOSE_QUALIFIED:
                model.add(
                    assigned[person, day, "close"] == 0
                )

    # No closing shift followed by next-day opening.
    for person in STAFF:
        for i in range(len(DAYS) - 1):
            model.add(
                assigned[person, DAYS[i], "close"]
                + assigned[person, DAYS[i + 1], "open"]
                <= 1
            )

    # Every employee works between 1 and max_shifts.
    for person in STAFF:
        total = sum(
            assigned[person, day, shift]
            for day in DAYS
            for shift in SHIFTS
        )

        model.add(total >= 1)
        model.add(total <= max_shifts)

    # Minimize the total penalty for unwanted assignments.
    model.minimize(
        sum(
            (
                (
                    2
                    if shift != PREFERRED_SHIFT[person]
                    else 0
                )
                + AVOID.get((person, day, shift), 0)
            ) * assigned[person, day, shift]
            for person in STAFF
            for day in DAYS
            for shift in SHIFTS
        )
    )

    # Configure and run the solver.
    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = 10.0
    solver.parameters.num_search_workers = 1

    status = solver.solve(model)

    status_names = {
        cp_model.OPTIMAL: "OPTIMAL",
        cp_model.FEASIBLE: "FEASIBLE",
        cp_model.INFEASIBLE: "INFEASIBLE",
        cp_model.MODEL_INVALID: "MODEL_INVALID",
        cp_model.UNKNOWN: "UNKNOWN",
    }

    print(f"Status: {status_names[status]}")

    if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
        print("No schedule can be displayed for this status.")
        return

    # Extract the selected assignments.
    schedule = {}

    for day in DAYS:
        for shift in SHIFTS:
            matches = [
                person
                for person in STAFF
                if solver.value(
                    assigned[person, day, shift]
                ) == 1
            ]

            assert len(matches) == 1
            schedule[day, shift] = matches[0]

    # Validate important constraints independently.
    workload = {person: 0 for person in STAFF}

    for day in DAYS:
        opener = schedule[day, "open"]
        closer = schedule[day, "close"]

        assert opener != closer
        assert closer in CLOSE_QUALIFIED
        assert (opener, day) not in UNAVAILABLE
        assert (closer, day) not in UNAVAILABLE

        workload[opener] += 1
        workload[closer] += 1

    for person in STAFF:
        assert 1 <= workload[person] <= max_shifts

    for i in range(len(DAYS) - 1):
        assert (
            schedule[DAYS[i], "close"]
            != schedule[DAYS[i + 1], "open"]
        )

    # Display the result.
    print(f"Penalty: {solver.objective_value:.0f}")
    print(
        "Proven lower bound: "
        f"{solver.best_objective_bound:.0f}"
    )

    for day in DAYS:
        print(
            f"{day}: "
            f"open={schedule[day, 'open']:<5} "
            f"close={schedule[day, 'close']}"
        )

    print("Workload:", workload)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    parser.add_argument(
        "--max-shifts",
        type=int,
        default=2,
    )

    args = parser.parse_args()
    solve_schedule(args.max_shifts)

Run it:

python schedule.py

The model has 60 Boolean assignment variables: six employees multiplied by five days multiplied by two shifts.

Without any restrictions, directly assigning one of six employees to each of ten positions gives 6¹⁰ = 60,466,176 possible assignment combinations.

The actual feasible search space is considerably smaller because qualifications, availability, daily limits, and rest requirements eliminate many possibilities.

What a successful result looks like

With OR-Tools 9.15.6755 and the single search worker set in the script, the run prints this optimal schedule:

Status: OPTIMAL
Penalty: 2
Proven lower bound: 2
Mon: open=Cora  close=Ava
Tue: open=Eli   close=Dev
Wed: open=Ben   close=Cora
Thu: open=Ben   close=Ava
Fri: open=Eli   close=Fay
Workload: {'Ava': 2, 'Ben': 2, 'Cora': 2, 'Dev': 1, 'Eli': 2, 'Fay': 1}

The solver may return another equally optimal schedule because multiple assignments can have the same objective value.

Let’s examine why this one works.

Ava closes Monday and Thursday. Cora opens Monday and closes Wednesday. Ben opens Wednesday and Thursday. Eli opens Tuesday and Friday. Dev closes Tuesday and Fay closes Friday, one shift each.

Every required position is filled, every employee works at least once, and nobody exceeds two assignments.

All closing shifts are staffed by qualified employees. Nobody receives an assignment on an unavailable day, and nobody moves from a closing shift to the following morning’s opening shift.

The total preference penalty is 2 because Cora opens on Monday despite preferring closing shifts. None of the specific assignments people asked to avoid appears.

There’s a particularly elegant reason that 2 is optimal.

Only Ben and Eli prefer opening shifts. Each is allowed at most two weekly assignments. Together, they can cover at most four of the five required opening shifts.

Therefore, at least one opening assignment must go to someone who prefers closing, generating at least 2 penalty points.

We’ve already constructed a schedule with penalty 2, so no better solution is possible.

This gives us a human-readable optimality argument independently of the solver’s own proof.

6. Understand the model line by line

The complete script is easier to understand once you recognize a few recurring patterns.

Boolean assignment variables

Conceptually, each decision variable represents:

assigned[employee, day, shift]

Its value is either 0 or 1.

For example, an assignment variable with value 1 for Ava, Monday, and opening means Ava works that particular shift.

Value 0 means she doesn’t.

These variables do not contain probabilities.

There’s no 0.8 confidence that Ava should work Monday. The solver is making a discrete decision.

Exactly-one constraints

For every shift, we require exactly one employee.

The add_exactly_one() API accepts a collection of Boolean literals and requires exactly one to be true.

This is cleaner and more expressive than writing custom procedural assignment logic.

The key distinction is:

  • add_exactly_one() requires exactly one selection.

  • add_at_most_one() allows zero or one selection.

  • A linear equality can impose an arbitrary required count.

  • A linear inequality can impose a minimum or maximum count.

These patterns cover a surprisingly large number of operational problems.

Qualification and availability constraints

The qualification rule prevents employees without closing training from being assigned to closing shifts.

The availability rules force specific assignment variables to zero.

In a production system, you’d probably avoid creating variables for inherently impossible combinations, especially at larger scales.

For this teaching example, creating every combination and explicitly disabling invalid ones makes the constraints easy to inspect.

Both approaches can represent the same mathematical restrictions.

Rest restrictions

The closing-to-opening rule demonstrates a constraint that links decisions across time.

We require:

Closing assignment today + opening assignment tomorrow ≤ 1

Because both variables are Boolean, they cannot both equal 1.

This expresses a scheduling relationship directly, without a special-purpose scheduling algorithm.

For real operations, minimum rest requirements may depend on actual start times, end times, employment rules, and local regulations. Our example implements one specific incompatible shift transition, not a universal legal-rest policy.

Fairness and workload limits

Every employee must receive between one and two assignments.

This is a simple fairness policy, but it’s not the only possible one.

Real scheduling systems may track:

  • Total hours rather than shift counts.

  • Night and weekend assignments.

  • Preferred working days.

  • Historical workload distribution.

  • Consecutive working days.

  • Contractual minimum and maximum hours.

Fairness is a business decision that must be translated into measurable criteria.

Constraint programming makes those criteria explicit instead of leaving them hidden inside a learned model.

Weighted objective function

The objective multiplies each assignment variable by its corresponding penalty.

In plain terms:

Total penalty = sum of all assigned shift penalties

Since unselected variables equal zero, they contribute nothing.

Since selected variables equal one, their penalties contribute to the objective.

CP-SAT minimizes that sum.

The entire assignment is optimized globally, so one locally undesirable choice may be accepted if it prevents several more expensive choices elsewhere.

This is precisely the kind of interaction that simple employee-by-employee ranking can miss.

7. Make the scheduling problem impossible on purpose

A useful optimization course should teach more than successful solutions.

Let’s make the model infeasible.

Run:

python schedule.py --max-shifts 1

This restricts every employee to at most one assignment per week.

We still have six employees and ten required shifts.

The total available assignment capacity is now only six.

But ten assignments are mandatory.

The model has no possible solution.

The expected result is:

Status: INFEASIBLE
No schedule can be displayed for this status.

This is an important capability.

The solver isn’t merely saying that its preferred heuristic couldn’t find a schedule. When it returns INFEASIBLE, it has proved that the modeled constraints cannot all be satisfied.

However, infeasibility does not automatically tell you which business rule is responsible.

In this case, the arithmetic makes the conflict obvious. In a large real-world model, an infeasibility might arise from a complicated interaction between qualifications, resource availability, deadlines, and demand.

A good application needs a strategy for explaining those conflicts to users.

How to diagnose infeasibility

A practical debugging procedure is:

  1. Verify that every required shift or job has at least one eligible resource.

  2. Check total supply against total demand.

  3. Check availability against mandatory coverage.

  4. Inspect whether individual capacity restrictions are too strict.

  5. Temporarily disable groups of constraints to isolate the conflict.

  6. Reintroduce the groups systematically.

For advanced applications, CP-SAT also supports assumption literals and infeasibility-related diagnostics.

Assumptions can help identify a sufficient subset of assumptions involved in an infeasible model. Such a subset is not necessarily the smallest possible explanation.

When developing these systems, diagnostics deserve almost as much attention as optimization quality.

A schedule generator that says only “no solution” is much less useful than one that explains that Thursday closing coverage requires an additional qualified employee.

8. Understand solver statuses and optimality guarantees

One of the biggest conceptual mistakes is treating a solver’s output as automatically optimal.

CP-SAT has several important statuses.

Status Meaning
OPTIMAL An optimal feasible solution has been established
FEASIBLE A valid solution was found, but optimality has not been proved
INFEASIBLE The mathematical model has been proved infeasible
MODEL_INVALID The model failed validation
UNKNOWN No solution or infeasibility proof was established before search stopped

The FEASIBLE state is particularly important for production applications.

Suppose you’re minimizing scheduling cost.

After an allocated search period, the solver might know:

  • The best valid solution found costs 120 points.

  • No solution can cost less than 105 points.

This establishes that the true minimum lies between 105 and 120.

You have a usable schedule, but you cannot claim it’s globally optimal.

The difference between the incumbent objective and the proven bound gives a measure of remaining uncertainty about optimality.

For minimization problems:

  • The incumbent is the cost of a feasible solution.

  • The best objective bound is a proven lower bound.

  • When they coincide, optimality is established, subject to the solver’s objective tolerances and representation.

For the tiny example in this tutorial, a single search worker and a ten-second limit are sufficient settings for a compact demonstration. Larger applications require benchmarking and appropriate time or resource limits.

You should also distinguish solver guarantees from model correctness.

A solver can prove that an assignment is optimal for a model that accidentally omitted an important labor restriction.

Mathematical optimality never compensates for missing business requirements.

A practical version detail: the OR-Tools 9.15 release notes document an issue with the status_name() convenience method. The tutorial avoids depending on that method by mapping the status constants explicitly.

9. Hard constraints and soft constraints should never be confused

Imagine the fulfillment center manager makes the following requests:

  • Never assign an unqualified employee to closing.

  • Avoid assigning Ava to Friday closing.

  • Distribute assignments fairly.

  • Cover every required shift.

Which are mandatory?

Which are preferences?

This isn’t a purely technical decision. It reflects the organization’s policies.

One particularly dangerous implementation mistake is assigning penalty weights to rules that must never be violated.

For example, suppose assigning an unqualified employee costs 1,000 penalty points.

If enough other penalties accumulate, the optimizer might prefer paying that cost.

That would make an unqualified assignment possible.

The proper approach is to encode truly mandatory qualification requirements as hard constraints.

Use soft penalties only for conditions the business is legitimately willing to relax.

How should you choose penalty weights?

Weights express trade-offs.

If an unwanted shift costs 2 and an especially undesirable request costs 9, the second is more important to avoid according to the model.

But weights should not be arbitrary merely because they’re easy to implement.

Useful approaches include:

  • Stakeholder-defined preference priorities.

  • Contractually agreed preference rules.

  • Historical analysis of scheduling disputes.

  • Cost-based estimates of operational inconvenience.

  • Sensitivity analysis showing how assignments change when weights change.

A weighted objective is appropriate when trading one preference against another makes sense.

When strict priority ordering matters, consider lexicographic optimization instead.

For example, first minimize uncovered demand, then fix that minimum value and minimize employee preference penalties.

This prevents a lower-priority improvement from compensating for a higher-priority deterioration.

The trade-off is additional model complexity and potentially multiple solve stages.

10. Go beyond fixed shifts: interval scheduling

Our example uses fixed opening and closing shift positions.

But CP-SAT can also represent tasks with start times and durations.

Suppose you’re scheduling manufacturing operations rather than fixed employee shifts.

Each operation has:

  • A start time.

  • A duration.

  • An end time.

  • A machine requirement.

The fundamental relationship is:

End time = Start time + Duration

OR-Tools supports interval variables and specialized scheduling constraints for this purpose.

The current Python API includes:

  • new_interval_var() to create an interval.

  • new_optional_interval_var() for an interval that may be inactive.

  • add_no_overlap() to prevent active intervals from overlapping.

  • add_cumulative() to enforce capacity across intervals.

  • add_max_equality() to relate an integer variable to the maximum of other expressions.

These are documented in the current Python API and official job-shop scheduling examples.

A small machine scheduling example

Let’s schedule three production tasks on one machine.

Each task has a different duration, and the machine can process only one task at a time.

We want to minimize the overall completion time, known as the makespan.

from ortools.sat.python import cp_model

durations = {
    "cut": 3,
    "assemble": 4,
    "inspect": 2,
}

model = cp_model.CpModel()

horizon = sum(durations.values())

starts = {}
ends = {}
intervals = []

for name, duration in durations.items():
    starts[name] = model.new_int_var(
        0, horizon, f"start_{name}"
    )

    ends[name] = model.new_int_var(
        0, horizon, f"end_{name}"
    )

    interval = model.new_interval_var(
        starts[name],
        duration,
        ends[name],
        f"interval_{name}",
    )

    intervals.append(interval)

# Only one operation can use the machine at a time.
model.add_no_overlap(intervals)

# Manufacturing sequence.
model.add(
    starts["assemble"] >= ends["cut"]
)

model.add(
    starts["inspect"] >= ends["assemble"]
)

# Minimize the final completion time.
makespan = model.new_int_var(
    0, horizon, "makespan"
)

model.add_max_equality(
    makespan,
    list(ends.values()),
)

model.minimize(makespan)

solver = cp_model.CpSolver()
status = solver.solve(model)

if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
    print("Makespan:", solver.value(makespan))

    for name in durations:
        print(
            name,
            solver.value(starts[name]),
            solver.value(ends[name]),
        )
else:
    print("No schedule found.")

The optimal makespan is 9 time units.

Why?

The durations are 3, 4, and 2, and they must run sequentially on the single machine.

Their total duration is 9.

Adding precedence constraints establishes the required processing order, while add_no_overlap() prevents simultaneous use of the resource.

This small example has an obvious solution, but the same interval modeling patterns can be extended to multiple machines, parallel jobs, and alternative processing routes.

For more complex applications, optional intervals let the model decide which machine handles a particular operation.

Instead of manually constructing a schedule, you describe resource conflicts and processing relationships, and CP-SAT searches for valid sequences.

11. What makes a constraint model production-ready?

A demonstration with six employees is straightforward.

A real scheduling system might involve hundreds of employees, dozens of skill categories, multiple sites, many overlapping requirements, and policies that change over time.

The solver is only one part of the system.

Validate input data before building the model

Bad input data produces bad optimization models.

Before calling CP-SAT, check for:

  • Duplicate employee or job identifiers.

  • Missing qualification information.

  • Invalid time windows.

  • Negative durations or capacities.

  • Contradictory availability records.

  • Unsupported shift types.

  • Missing mandatory business parameters.

Distinguish between malformed data and genuine infeasibility.

An unknown employee identifier is an input validation problem, not proof that a valid schedule is impossible.

Use an independent solution validator

Notice the assertions in the main example.

They recheck important scheduling rules after extracting the solution.

This is intentional.

The model expresses constraints, but the output should also be checked by a separate validation path before operational use.

For production systems, such checks should use an independently implemented validator with clear error reporting rather than relying exclusively on assertions.

The validator should check actual business invariants, not just repeat the same modeling code.

Also test the validator itself with deliberately corrupted assignments.

An excellent solver does not eliminate the need for defensive engineering.

Keep optimization inputs explainable

Avoid hiding policy decisions in unexplained constants.

Instead, maintain a documented configuration describing:

  • Coverage requirements.

  • Eligibility rules.

  • Scheduling limits.

  • Preference penalties.

  • Optimization priorities.

  • Solver resource budgets.

Store a snapshot of the configuration used for every published schedule.

This allows operators to understand why the system selected one assignment over another.

Respect actual time and calendar semantics

Our example uses simple weekday labels.

Real systems should use actual dates and appropriate time-zone handling.

This matters when working with:

  • Overnight shifts.

  • Daylight-saving transitions.

  • Holidays.

  • Cross-location assignments.

  • Rest requirements expressed in hours.

  • Variable-duration operations.

Integer scheduling models frequently represent time using minutes from a defined planning-horizon origin.

Choose that origin and time granularity carefully, and perform time-zone conversions consistently at system boundaries.

Monitor solver performance

Record the following for every optimization run:

  • Problem dimensions.

  • Solver version.

  • Solver status.

  • Objective value.

  • Proven objective bound.

  • Wall-clock execution time.

  • Relevant solver configuration.

  • Input or configuration version.

For large models, CP-SAT also exposes search statistics and optional progress logging.

Tracking these values makes it possible to detect regressions and determine whether a new business requirement significantly increases computational cost.

Manage computational complexity

Constraint solvers are powerful, but their runtime depends heavily on how the problem is modeled.

A model with thousands of well-structured variables might solve quickly, while a smaller model with difficult combinatorial interactions might take much longer.

Useful engineering techniques include:

  • Avoiding unnecessary decision variables.

  • Restricting impossible assignments before model construction.

  • Using native scheduling and Boolean constraints when appropriate.

  • Choosing tighter valid variable bounds.

  • Applying sensible search limits.

  • Testing different worker counts.

  • Reusing feasible schedules as hints when replanning, where appropriate.

  • Benchmarking on realistic difficult instances rather than only average cases.

Don’t assume that doubling the number of employees merely doubles runtime.

Combinatorial problems can become much harder as their size and structure change.

Plan for infeasibility and graceful degradation

A real scheduling service needs a business policy for cases where the hard constraints cannot all be met.

Depending on the domain, reasonable responses might include:

  • Requesting additional resources.

  • Allowing eligible overtime.

  • Moving noncritical jobs.

  • Extending flexible deadlines.

  • Escalating coverage gaps for human review.

These responses should be explicitly authorized by the business.

An optimization system should not silently relax qualification, safety, or contractual restrictions to manufacture a feasible-looking answer.

12. Cherry on the cake: the team that didn’t need its forecasting model

Consider this illustrative case study. It’s a fictional composite of a common engineering anti-pattern, not a claim about a specific company’s deployment.

A fulfillment operations team wants to automate daily task assignments.

The initial architecture consists of several components:

  1. A machine learning model estimates which employee is most likely to be assigned to each task.

  2. A ranking layer sorts the predictions.

  3. A greedy scheduler picks the highest-ranked available employee.

  4. A conflict-resolution script fixes overlapping assignments.

  5. Another script checks qualifications and workload rules.

  6. Operations managers manually repair the remaining conflicts.

The system becomes increasingly difficult to maintain.

Whenever the company introduces a new scheduling restriction, engineers need to modify the repair logic.

Sometimes fixing one conflict introduces another.

An assignment can look individually reasonable but make the remaining tasks impossible to schedule.

Eventually, someone asks a different question:

Why are we predicting the assignments employees historically received when what we actually need is a set of assignments satisfying today’s explicit requirements?

The team discovers that its model was learning past scheduling decisions, not unknown future demand.

Today’s job list is already known. Employee availability is already recorded. Qualifications and deadlines exist in structured data.

The prediction model is unnecessary for that particular decision.

They replace the ranking-plus-repair pipeline with a constraint model and a solver call.

The architecture becomes:

Confirmed jobs
Employee availability
Qualifications and capacity
Business preferences
         |
         v
   CP-SAT model
         |
         v
   Solver invocation
         |
         v
  Validated schedule

This is not a claim that one solver call magically handles all operational software concerns.

The organization still needs input validation, policy management, auditing, observability, and integration with scheduling systems.

But the central decision-making component is dramatically simpler.

Instead of asking a model to imitate good assignments and then repairing the mistakes, the team directly specifies what constitutes a valid assignment.

And unlike a greedy heuristic, the solver can sometimes certify that its preferred solution is globally optimal.

What if the team really did need forecasting?

Then the forecast should remain.

Suppose the team is assigning employees for next week’s uncertain workload.

Historical order volumes may contain predictive information that is unavailable to a constraint solver operating only on deterministic inputs.

The correct architecture is likely:

Forecast demand → Translate forecasts into staffing scenarios → Optimize staffing → Monitor actual demand.

You can even solve multiple demand scenarios to study how robust a staffing plan is.

The key lesson is not that machine learning is bad.

It’s that machine learning should handle uncertainty and learned relationships, while optimization should handle explicit decisions, constraints, and objectives.

Using the right tool for each stage produces systems that are easier to reason about.

13. Common mistakes when adopting CP-SAT

Even experienced engineers make predictable mistakes when moving from conventional application code to mathematical optimization.

Mistake 1: Encoding preferences as mandatory rules

If every employee preference becomes a hard constraint, finding a feasible schedule may become impossible unnecessarily.

Ask stakeholders which requirements are truly mandatory and which can be negotiated.

Mistake 2: Using huge penalty weights without analysis

Arbitrarily choosing enormous coefficients can make the objective difficult to interpret and introduce unnecessary numerical or modeling difficulties.

Use meaningful integer weights, evaluate their effects, and consider hierarchical objectives when business priorities are strict.

Mistake 3: Forgetting that a model can omit reality

A solver only respects constraints you encode.

If mandatory qualifications, physical machine limits, or rest requirements are missing, the resulting schedule may be mathematically valid but operationally unacceptable.

Model reviews with domain experts are essential.

Mistake 4: Treating any returned schedule as optimal

Always inspect the returned status.

A feasible solution found under a time limit can be extremely useful, but it is not necessarily the best possible solution.

Mistake 5: Assuming infeasibility means the solver failed

A proven infeasible result may be the most valuable possible outcome.

It reveals that the requirements cannot all hold simultaneously.

The engineering task then becomes identifying the conflicting requirements and agreeing on an authorized change.

Mistake 6: Optimizing a proxy instead of the actual objective

Minimizing preference penalty is useful only if the penalty reflects scheduling quality.

A system can optimize its mathematical score while disappointing the people who use it.

Validate objectives against real operational outcomes, not merely the solver’s reported numbers.

Mistake 7: Using CP-SAT for every optimization problem

Constraint programming is not automatically the best choice for every mathematical problem.

Consider the structure of your decision:

  • Purely continuous linear optimization may be better suited to linear programming.

  • Large mixed-integer linear models may benefit from specialized MILP solvers.

  • Vehicle routing problems often benefit from dedicated routing methods.

  • Complex discrete scheduling and logical assignment problems are natural candidates for CP-SAT.

OR-Tools includes multiple optimization technologies. The decision should reflect the mathematical structure, constraints, scale, and performance requirements—not loyalty to one solver.

14. Practical exercises to deepen your understanding

The best way to learn constraint programming is to modify a working model.

Here are several progressively harder exercises.

Exercise A: Increase weekly workload capacity

Change the scheduling limit from two shifts per employee to three.

Observe whether the objective improves.

Then investigate why.

Which preferences become easier to accommodate? Does the optimal schedule place more work on fewer employees?

This demonstrates the relationship between resource capacity and solution quality.

Exercise B: Require additional qualifications

Introduce a separate qualification for opening shifts.

Choose which employees possess it and add the appropriate hard constraint.

Experiment until the schedule becomes infeasible.

This demonstrates how eligibility restrictions interact with coverage.

Exercise C: Add unpopular-day fairness

Suppose Friday assignments are considered less desirable.

Add a penalty to Friday assignments for selected employees, then inspect how the schedule changes.

A harder variant is to track undesirable assignments across multiple weeks and minimize long-term imbalance.

Exercise D: Introduce variable coverage

Instead of exactly one employee per shift, require two employees during a high-demand shift.

Replace that shift’s exactly-one constraint with an appropriate required-count constraint.

Then consider whether your existing employee capacity is sufficient.

Exercise E: Implement hierarchical priorities

Make avoiding certain unwanted assignments a higher priority than ordinary shift preferences.

Solve the higher-priority objective first.

Then retain its optimal value as a constraint and optimize the next priority.

Compare this with a single weighted objective.

Exercise F: Extend the planning horizon

Expand the schedule to multiple weeks.

Add weekend shifts, cross-week rest constraints, and longer-term fairness.

Benchmark solver runtime as the model grows.

This exercise is particularly valuable because it exposes the difference between toy problems and production optimization.

15. Where to learn more

The official references are a good starting point for advanced modeling:

For further practice, look at problems involving meeting-room allocation, cloud job scheduling, manufacturing resource assignment, warehouse picking, and constrained staff rostering.

These domains contain real combinatorial decisions and are well suited to learning optimization without relying on training data.

16. Final takeaway: stop predicting what you can solve

A surprising number of engineering problems are presented as artificial intelligence problems simply because they involve choosing among many possibilities.

But complexity alone does not make a problem predictive.

Sometimes the available information already defines a precise decision problem:

  • These are the available resources.

  • These are the jobs that must be completed.

  • These are the restrictions.

  • These are the allowed choices.

  • This is what we want to minimize or maximize.

When that’s the case, a constraint solver may be a much better starting point than a learned model.

In this tutorial, we built a scheduling system that directly enforces coverage, qualifications, availability, workload limits, and rest requirements. We optimized preferences, examined an optimal solution, and deliberately created an impossible problem to see how the solver responds.

No historical schedule dataset was required.

No labels were required.

No neural network needed training.

And the result was not merely a plausible prediction. It was an assignment satisfying explicit mathematical constraints, with a measurable optimization objective.

The larger engineering lesson is about problem formulation: before choosing an algorithm, identify what is unknown and what is actually under your control.

Your next step

Take one scheduling, assignment, allocation, or planning problem from a project you’re working on.

Write down its decision variables, its hard constraints, and one objective you genuinely care about.

Then implement a small CP-SAT model, solve it, deliberately break a constraint, and inspect what happens.

Compare that result against the heuristic, spreadsheet, predictive pipeline, or manual process you’re currently using.

You may discover that the most useful upgrade to your system isn’t another machine learning model.

It’s replacing approximation with an explicit optimization problem—and letting a solver do the deciding.