Google OR-Tools CP-SAT: Slot Rotation constraint bypassed by parallel assignments in Roster
13:33 05 Jan 2026

I am building a guard scheduling system using Google OR-Tools (CP-SAT). I have a "Slot Rotation" constraint designed to ensure that the same user doesn't work consecutive shifts.

The Setup: I have two assignments (Patrol and Checkpoints) running in parallel. The shifts are hourly.

  • Assignment A (Patrol): 08:00, 09:00, 10:00...

  • Assignment B (Checkpoints): 08:00, 09:00, 10:00...

The Problem: Even with a "Strict Rotation" constraint enabled, the solver is assigning the same user to back-to-back hours (e.g., usr_015 works 09:00–10:00 in Patrol and then 10:00–11:00 in Patrol).

My Constraint Logic: My compiler sorts all slots in the roster by startTime. When assignments are parallel, the sequence looks like this: [Patrol_08:00, Checkpoint_08:00, Patrol_09:00, Checkpoint_09:00, Patrol_10:00...]

I am using a window of 2 for my rotation, which logic suggests should prevent a user from appearing in "the next slot."

Python

# Simplified snippet of my SlotRotationCompiler
def compile(self, block, constraint, context):
    # Slots are sorted globally by startTime
    sorted_slots = sorted(context.slots, key=lambda s: s.start_time)
    
    window = block.params.get("window", 2) # consecutive = window 2
    
    for i in range(len(sorted_slots)):
        current_slot = sorted_slots[i]
        # Look ahead within the window
        for j in range(1, window):
            if i + j < len(sorted_slots):
                next_slot = sorted_slots[i + j]
                # Forbid same user in current_slot and next_slot
                for user_id in context.user_ids:
                    model.Add(user_vars[user_id, current_slot] + 
                              user_vars[user_id, next_slot] <= 1)

The Loophole: Because the assignments are parallel, Patrol_09:00 (index 2) and Patrol_10:00 (index 4) are not adjacent in the sorted list. Checkpoint_09:00 (index 3) is between them.

With a window of 2, the solver:

  1. Prevents User A from working Patrol_09:00 AND Checkpoint_09:00.

  2. Allows User A to work Patrol_09:00 AND Patrol_10:00 because they are 2 indices apart, effectively "skipping" over the rotation check.

The Results I'm getting:

JSON

{
    "slotId": "p_2", "startTime": "09:00", "userId": "usr_015",
    "slotId": "p_3", "startTime": "10:00", "userId": "usr_015" 
    // This is a back-to-back hour shift! 
}

Question: How should I structure my constraint or my slot compilation to ensure that "Rotation" accounts for actual temporal adjacency across parallel assignments, rather than just index adjacency in a list? Should I be using a MinRestGap approach instead of SlotRotation, or is there a better way to calculate the window dynamically based on the number of concurrent assignments?

python scheduling or-tools constraint-programming cp-sat