Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Employee Shift Scheduling
  • Employee resource constraints
  • Shift rotation and patterns

Employee Shift Scheduling

    • Introduction
    • Planning AI concepts
    • Metrics and optimization goals
    • Getting started with employee shift scheduling
    • Understanding the API
    • Employee shift scheduling user guide
    • Employee resource constraints
      • Employee availability
      • Employee contracts
      • Work limits
        • Work limits
        • Minutes worked per period
        • Minutes worked in a rolling window
        • Minutes logged per period
        • Days worked per period
        • Days worked in a rolling window
        • Consecutive days worked rules
        • Shifts worked per period
        • Shifts worked in a rolling window
      • Time off
        • Time off
        • Days off per period
        • Consecutive days off in a rolling window
        • Consecutive minutes off in a rolling window
        • Shifts to avoid close to day off requests
      • Shift type diversity
      • Shift rotation and patterns
      • Fairness
      • Pairing employees
      • Shift travel and locations
    • Shift service constraints
      • Alternative shifts
      • Cost management
      • Demand and supply
      • Mandatory and optional shifts
      • Shift assignments
      • Shift sequence patterns: single day shifts
      • Shift sequence patterns: multi-day shifts
      • Shift sequence patterns: daily shift pairings
      • Skills and risk factors
    • Recommendations
    • Real-time planning
    • Time zones and Daylight Saving Time (DST)
    • New and noteworthy
    • Upgrading to the latest versions
    • Feature requests

Shift rotation and patterns

Employees have an expectation that the terms of their contracts are honored in the shifts they are scheduled to work.

Employee contracts stipulate the conditions under which the employees work, including:

  • How much time should be between shifts.

  • When employees can be assigned overlapping shifts, for instance, when they are on call.

  • Shift rotations, for instance, a week of morning shifts followed by a week of afternoon shifts.

  • The variation between shift start times.

For any employee shift scheduling solution to be feasible, it must take into account the contractual obligations between the employer and the employee.

This guide explains how to manage employee contract shift rules with the following examples:

  • Minutes between shifts rules

  • Overlapping shift rules

  • Shift rotation rules

  • Keep shift start times within a range

To learn how to define a contract, see Employee contracts.

Prerequisites

To run the examples in this guide, you need to authenticate with a valid API key for the Employee Shift Scheduling model:

  1. Log in to Timefold Platform: app.timefold.ai

  2. From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, then choose API Keys.

  3. Create a new API key or use an existing one. Ensure the list of models for the API key contains the Employee Shift Scheduling model.

In the examples, replace <API_KEY> with the API Key you just copied.

1. Minutes between shifts rules

Employees need time between shifts for their lives outside work.

Contractually, employees could be entitled to a minimum time between shifts, for instance, if an employee works a night shift, it might be included in their contract that they don’t work for the following 12 hours.

You can specify the minimum and maximum time between shifts with minutesBetweenShiftRules in contracts.

Additionally, you can provide a scope for the rule. The scope will limit which pairs of shifts the rule is applied to. The scope excludes shift pairs where the start of the later shift is after the end of the prior shift plus the scope duration.

{
  "contracts": [
    {
      "id": "fullTimeContract",
      "minutesBetweenShiftsRules": [
        {
          "id": "Minimum12HoursBetweenShiftsFullTime",
          "minimumMinutesBetweenShifts": 720,
          "maximumMinutesBetweenShifts": 1440,
          "scope": {
            "type": "duration",
            "duration": "P1D"
          },
          "satisfiability": "REQUIRED"
        }
      ]
    }
  ]
}

minutesBetweenShiftsRules must include an id.

In this instance, the minimumMinutesBetweenShifts is 720 minutes, which ensures employees have 12 hours between assigned shifts.

The maximumMinutesBetweenShifts is 1440 minutes, which means the next shift should be assigned no more than 24 hours later.

The satisfiability of the rule can be REQUIRED or PREFERRED. If omitted, REQUIRED is the default.

1.1. Required satisfiability

When the satisfiability of the rule is REQUIRED, the Minutes between shifts not in required range for employee hard constraint is invoked, making sure the number of minutes between shifts assigned to an employee is not below the minimumMinutesBetweenShifts or above the maximumMinutesBetweenShifts.

This constraint will leave shifts unassigned if assigning the shift broke the constraint.

1.2. Required satisfiability and minimumConsecutivePriorShifts

The rule can optionally define a minimumConsecutivePriorShifts attribute to work with a daily sequence of prior shifts. minimumConsecutivePriorShifts can be either 1 or 2. If the value is greater than 1, requiredPriorShiftTags need to be specified to identify the prior sequence, for example a sequence of night shifts.

When the satisfiability of the rule is REQUIRED, minimumConsecutivePriorShifts is 2, and the employee is assigned to a consecutive daily sequence of two shifts, the Minutes between shifts not in required range for employee hard constraint is invoked, making sure the number of minutes between the end of the shift sequence and the next shift assigned to an employee is not below the minimumMinutesBetweenShifts or above the maximumMinutesBetweenShifts.

{
  "contracts": [
    {
      "id": "fullTimeContract",
      "minutesBetweenShiftsRules": [
        {
          "id": "Minimum12HoursBetweenShiftsFullTime",
          "minimumMinutesBetweenShifts": 720,
          "satisfiability": "REQUIRED",
          "minimumConsecutivePriorShifts": 2,
          "requiredPriorShiftTags": ["night"]
        }
      ]
    }
  ]
}

1.3. Preferred satisfiability

When the satisfiability of the rule is PREFERRED, the Minutes between shifts not in preferred range for employee soft constraint is invoked. Employees might be assigned shifts that are below the minimumMinutesBetweenShifts or above the maximumMinutesBetweenShifts, but the constraint adds a soft penalty to the run score for any matches to the constraint, incentivizing Timefold to find an alternative solution.

1.4. Minutes between shift rules example

If Carl works the night shift between 01:00 and 09:00, he will not be eligible for another shift until 12 hours later at or after 21:00.

  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules [email protected]
{
  "config": {
    "run": {
      "name": "Minutes between shifts rules example",
      "tags": []
    }
  },
  "modelInput": {
    "contracts": [
      {
        "id": "fullTimeContract",
        "minutesBetweenShiftsRules": [
          {
            "id": "Minimum12HoursBetweenShiftsFullTime",
            "minimumMinutesBetweenShifts": 720,
            "maximumMinutesBetweenShifts": 1440,
            "scope": {
              "type": "duration",
              "duration": "P1D"
            }
          }
        ]
      }
    ],
    "employees": [
      {
        "id": "Carl",
        "contracts": [
          "fullTimeContract"
        ]
      }
    ],
    "shifts": [
      {
        "id": "Mon night",
        "start": "2027-02-01T01:00:00Z",
        "end": "2027-02-01T09:00:00Z"
      },
      {
        "id": "Mon day",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z"
      },
      {
        "id": "Tue night",
        "start": "2027-02-02T01:00:00Z",
        "end": "2027-02-02T09:00:00Z"
      },
      {
        "id": "Tue day",
        "start": "2027-02-02T09:00:00Z",
        "end": "2027-02-02T17:00:00Z"
      },
      {
        "id": "Wed night",
        "start": "2027-02-03T01:00:00Z",
        "end": "2027-02-03T09:00:00Z"
      },
      {
        "id": "Wed day",
        "start": "2027-02-03T09:00:00Z",
        "end": "2027-02-03T17:00:00Z"
      }
    ]
  }
}
To request the solution, locate the 'ID' from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/<ID>
{
  "run": {
    "id": "ID",
    "name": "Minutes between shifts rules example",
    "submitDateTime": "2025-03-19T09:18:47.323655439Z",
    "startDateTime": "2025-03-19T09:19:01.715923815Z",
    "activeDateTime": "2025-03-19T09:19:01.913383827Z",
    "completeDateTime": "2025-03-19T09:24:02.756166615Z",
    "shutdownDateTime": "2025-03-19T09:24:03.171847565Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/-3medium/0soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon night",
        "employee": "Carl"
      },
      {
        "id": "Mon day",
        "employee": null
      },
      {
        "id": "Tue night",
        "employee": "Carl"
      },
      {
        "id": "Tue day",
        "employee": null
      },
      {
        "id": "Wed night",
        "employee": "Carl"
      },
      {
        "id": "Wed day",
        "employee": null
      }
    ]
  },
  "inputMetrics": {
    "employees": 1,
    "shifts": 6,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 3,
    "unassignedShifts": 3,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 1,
    "assignedMandatoryShifts": 3,
    "assignedOptionalShifts": 0,
    "travelDistance": 0
  }
}

modelOutput contains the employee schedule with Carl assigned shifts with more than 12 hours between each shift.

2. Overlapping shift rules

There are situations where employees need to be assigned to multiple shifts that occur at the same time, for instance, their regular shift and an on call shift that overlaps their regular shift.

Overlapping shifts rules are defined in contracts.

{
  "contracts": [
    {
      "id": "fullTimeContract",
      "allowOverlappingShiftsRules": [
        {
          "id": "allowOverlapWithOnCallShift",
          "includeShiftTags": [
            "On call"
          ],
          "shiftTagMatches": "ALL"
        }
      ]
    }
  ]
}

An allowOverlappingShiftsRules must include an id.

The Overlapping shifts hard constraint makes sure only shifts with tags defined by the rule’s includeShiftTags can overlap with other shifts. Alternatively, only shifts without tags defined by the rule’s excludeShiftTags can overlap with other shifts.

Further information about including or excluding shifts with shift tags:

Shifts with specific tags can be included or excluded by the rule. Tags are defined in shifts:

{
  "shifts": [
    {
      "id": "2027-02-01",
      "start": "2027-02-01T09:00:00Z",
      "end": "2027-02-01T17:00:00Z",
      "tags": ["Part-time"]
    }
  ]
}

Use includeShiftTags to include shifts with specific tags or excludeShiftTags to exclude shifts with specific tags.

shiftTagMatches can be set to ALL or ANY. The default behavior for shiftTagMatches is ALL, and if omitted, the default ALL will be used.

The rule can define either includeShiftTags or excludeShiftTags, but not both.

{
  "includeShiftTags": ["Part-time", "Weekend"],
  "shiftTagMatches": "ALL"
}

With shiftTagMatches set to ALL, all tags defined by the rule’s includeShiftTags attribute must be present in the shift. With shiftTagMatches set to ANY, at least one tag defined by the rule’s includeShiftTags attribute must be present in the shift.

{
  "excludeShiftTags": ["Part-time", "Weekend"],
  "shiftTagMatches": "ALL"
}

With shiftTagMatches set to ALL, all tags defined by the rule’s excludeShiftTags attribute cannot be present in the shift. This is useful when you want to exclude things in combination with each other. For instance, excluding the shift tags Part-time and Weekend with shiftTagMatches set to All, will exclude shifts that include the tags Part-time and Weekend from the rule. Shifts tagged only Part-time or only Weekend will not be excluded.

With shiftTagMatches set to ANY, any of the tags defined by the rule’s excludeShiftTags attribute cannot be present in the shift. This is useful when you need to exclude tags regardless of their relationship to other tags. For instance, excluding the shift tags Part-time and Weekend with shiftTagMatches set to ANY, will exclude any shift that includes the tags Part-time or Weekend, whether they occur together or not.

2.1. Overlapping shift rules example

In the following example, Carl is assigned to a 24 hour on call shift that overlaps with his regular 8 hour shift.

  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules [email protected]
{
  "config": {
    "run": {
      "name": "Overlapping shifts rules example"
    }
  },
  "modelInput": {
    "contracts": [
      {
        "id": "fullTimeContract",
        "allowOverlappingShiftsRules": [
          {
            "id": "allowOverlapWithOnCallShift",
            "includeShiftTags": [
              "On call"
            ],
            "shiftTagMatches": "ALL"
          }
        ]
      }
    ],
    "employees": [
      {
        "id": "Carl",
        "contracts": [
          "fullTimeContract"
        ]
      }
    ],
    "shifts": [
      {
        "id": "Mon on-call",
        "start": "2027-02-01T00:00:00Z",
        "end": "2027-02-02T00:00:00Z",
        "tags": ["On call"]
      },
      {
        "id": "Mon regular shift",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z"
      }
    ]
  }
}
To request the solution, locate the 'ID' from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/<ID>
{
  "run": {
    "id": "ID",
    "name": "Overlapping shifts rules example",
    "submitDateTime": "2025-03-21T04:48:22.193823297Z",
    "startDateTime": "2025-03-21T04:48:33.625081572Z",
    "activeDateTime": "2025-03-21T04:48:33.773882421Z",
    "completeDateTime": "2025-03-21T04:53:34.494085854Z",
    "shutdownDateTime": "2025-03-21T04:53:34.766113937Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/0soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon on-call",
        "employee": "Carl"
      },
      {
        "id": "Mon regular shift",
        "employee": "Carl"
      }
    ]
  },
  "inputMetrics": {
    "employees": 1,
    "shifts": 2,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 2,
    "unassignedShifts": 0,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 1,
    "assignedMandatoryShifts": 2,
    "assignedOptionalShifts": 0,
    "travelDistance": 0
  }
}

modelOutput contains the schedule with overlapping shifts assigned to Carl.

inputMetrics provides a breakdown of the inputs in the input dataset.

KPIs provides the KPIs for the output including:

{
  "assignedShifts": 2,
  "unassignedShifts": 0
}

3. Shift rotation rules

Shift rotations rules specify which shifts are part of the weekly rotation that employees are assigned. For instance, 1 week of morning shifts followed by 1 week of afternoon shifts.

shiftRotationRules are defined in contracts:

{
  "contracts": [
    {
      "id": "fullTimeContract",
      "shiftRotationRules": [
        {
          "id": "rotateMorningAndAfternoonShiftsWeekly",
          "builtInRotationPeriod": {
            "type": "WEEKLY"
          },
          "rotationGroups": [
            {
              "id": "morningShifts",
              "includeShiftTags": [
                "Morning"
              ],
              "shiftTagMatches": "ANY"
            },
            {
              "id": "afternoonShifts",
              "includeShiftTags": [
                "Afternoon"
              ],
              "shiftTagMatches": "ANY"
            }
          ],
          "satisfiability": "REQUIRED"
        }
      ]
    }
  ]
}

shiftRotationRules must include an id.

builtInRotationPeriod specifies the length each rotation lasts for and must includes the type. Currently, only WEEKLY is supported.

rotationGroups contains multiple groups. Each group must include an id and the shift tags to be included in the group for a rotation.

shiftTagMatches for each rotation group must be set to ANY to ensure only one of the tags in a rotation group is required. For instance, if a rotation group includes the tags Afternoon and Late, only one of the tags should be required to assign a shift to an employee.

The satisfiability of the rule can be REQUIRED or PREFERRED. If omitted, REQUIRED is the default.

3.1. Required satisfiability

When the satisfiability of the rule is REQUIRED, the Required shift rotation not met for employee hard constraint is invoked, which makes sure employees are assigned shifts from 1 rotationGroups during the first period, then a different rotationGroups the next period.

Shifts will be left unassigned if assigning them would break the Required shift rotation not met for employee constraint.

In the following example, shiftRotationRules specifies that employees work 1 week of morning shifts followed by 1 week of afternoon shifts. There are 2 employees and 20 shifts over a 2 week period.

Ann is assigned 1 week of morning shifts followed by 1 week of afternoon shifts. Beth is assigned 1 week of afternoon shifts followed by 1 week of morning shifts.

shift rotation rules required

3.2. Preferred satisfiability

When the satisfiability of the rule is PREFERRED, the Preferred shift rotation not met for employee soft constraint is invoked. Every time employees are assigned shifts that break the shiftRotationRules, this constraint adds a soft penalty to the run score, incentivizing Timefold to find an alternative solution.

{
  "contracts": [
    {
      "id": "fullTimeContract",
      "shiftRotationRules": [
        {
          "id": "rotateMorningAndAfternoonShiftsWeekly",
          "builtInRotationPeriod": {
            "type": "WEEKLY"
          },
          "rotationGroups": [
            {
              "id": "morningShifts",
              "includeShiftTags": [
                "Morning"
              ],
              "shiftTagMatches": "ANY"
            },
            {
              "id": "afternoonShifts",
              "includeShiftTags": [
                "Afternoon"
              ],
              "shiftTagMatches": "ANY"
            }
          ],
          "satisfiability": "PREFERRED"
        }
      ]
    }
  ]
}

In the following example, a shiftRotationRules specifies that employees work 1 week of morning shifts followed by 1 week of afternoon shifts. There is 1 employee and 10 shifts over a 2 week period. All of the shifts are morning shifts.

Ann is assigned 2 weeks of morning shifts, which breaks the soft constraint.

shift rotation rules preferred

3.3. Shift rotation rules example

In the following example, shiftRotationRules specifies that employees work 1 week of morning shifts followed by 1 week of afternoon shifts. There are 2 employees and 20 shifts over a 2 week period.

Ann is assigned 1 week of morning shifts followed by 1 week of afternoon shifts. Beth is assigned 1 week of afternoon shifts followed by 1 week of morning shifts.

  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules [email protected]
{
  "config": {
    "run": {
      "name": "Shift rotation rules example"
    }
  },
  "modelInput": {
    "contracts": [
      {
        "id": "fullTimeContract",
        "periodRules": [
          {
            "id": "Max8HoursPerDay",
            "period": "DAY",
            "satisfiability": "REQUIRED",
            "minutesWorkedMax": 480
          }
        ],
        "shiftRotationRules": [
          {
            "id": "rotateMorningAndAfternoonShiftsWeekly",
            "builtInRotationPeriod": {
              "type": "WEEKLY"
            },
            "rotationGroups": [
              {
                "id": "morningShifts",
                "includeShiftTags": [
                  "Morning"
                ],
                "shiftTagMatches": "ANY"
              },
              {
                "id": "afternoonShifts",
                "includeShiftTags": [
                  "Afternoon"
                ],
                "shiftTagMatches": "ANY"
              }
            ],
            "satisfiability": "REQUIRED"
          }
        ]
      }
    ],
    "employees": [
      {
        "id": "Ann",
        "contracts": [
          "fullTimeContract"
        ]
      },
      {
        "id": "Beth",
        "contracts": [
          "fullTimeContract"
        ]
      }
    ],
    "shifts": [
      {
        "id": "Mon 1 AM",
        "start": "2027-02-01T06:00:00Z",
        "end": "2027-02-01T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Mon 1 PM",
        "start": "2027-02-01T14:00:00Z",
        "end": "2027-02-01T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Tue 1 AM",
        "start": "2027-02-02T06:00:00Z",
        "end": "2027-02-02T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Tue 1 PM",
        "start": "2027-02-02T14:00:00Z",
        "end": "2027-02-02T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Wed 1 AM",
        "start": "2027-02-03T06:00:00Z",
        "end": "2027-02-03T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Wed 1 PM",
        "start": "2027-02-03T14:00:00Z",
        "end": "2027-02-03T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Thu 1 AM",
        "start": "2027-02-04T06:00:00Z",
        "end": "2027-02-04T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Thu 1 PM",
        "start": "2027-02-04T14:00:00Z",
        "end": "2027-02-04T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Fri 1 AM",
        "start": "2027-02-05T06:00:00Z",
        "end": "2027-02-05T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Fri 1 PM",
        "start": "2027-02-05T14:00:00Z",
        "end": "2027-02-05T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Mon 2 AM",
        "start": "2027-02-08T06:00:00Z",
        "end": "2027-02-08T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Mon 2 PM",
        "start": "2027-02-08T14:00:00Z",
        "end": "2027-02-08T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Tue 2 AM",
        "start": "2027-02-09T06:00:00Z",
        "end": "2027-02-09T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Tue 2 PM",
        "start": "2027-02-09T14:00:00Z",
        "end": "2027-02-09T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Wed 2 AM",
        "start": "2027-02-10T06:00:00Z",
        "end": "2027-02-10T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Wed 2 PM",
        "start": "2027-02-10T14:00:00Z",
        "end": "2027-02-10T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Thu 2 AM",
        "start": "2027-02-11T06:00:00Z",
        "end": "2027-02-11T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Thu 2 PM",
        "start": "2027-02-11T14:00:00Z",
        "end": "2027-02-11T22:00:00Z",
        "tags": ["Afternoon"]
      },
      {
        "id": "Fri 2 AM",
        "start": "2027-02-12T06:00:00Z",
        "end": "2027-02-12T14:00:00Z",
        "tags": ["Morning"]
      },
      {
        "id": "Fri 2 PM",
        "start": "2027-02-12T14:00:00Z",
        "end": "2027-02-12T22:00:00Z",
        "tags": ["Afternoon"]
      }
    ]
  }
}
To request the solution, locate the 'ID' from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/<ID>
{
  "run": {
    "id": "ID",
    "name": "Shift rotation rules example",
    "submitDateTime": "2025-05-12T04:25:59.848509636Z",
    "startDateTime": "2025-05-12T04:26:11.428482441Z",
    "activeDateTime": "2025-05-12T04:26:11.568391902Z",
    "completeDateTime": "2025-05-12T04:56:12.067722194Z",
    "shutdownDateTime": "2025-05-12T04:56:12.212070768Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/0soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon 1 AM",
        "employee": "Ann"
      },
      {
        "id": "Mon 1 PM",
        "employee": "Beth"
      },
      {
        "id": "Tue 1 AM",
        "employee": "Ann"
      },
      {
        "id": "Tue 1 PM",
        "employee": "Beth"
      },
      {
        "id": "Wed 1 AM",
        "employee": "Ann"
      },
      {
        "id": "Wed 1 PM",
        "employee": "Beth"
      },
      {
        "id": "Thu 1 AM",
        "employee": "Ann"
      },
      {
        "id": "Thu 1 PM",
        "employee": "Beth"
      },
      {
        "id": "Fri 1 AM",
        "employee": "Ann"
      },
      {
        "id": "Fri 1 PM",
        "employee": "Beth"
      },
      {
        "id": "Mon 2 AM",
        "employee": "Beth"
      },
      {
        "id": "Mon 2 PM",
        "employee": "Ann"
      },
      {
        "id": "Tue 2 AM",
        "employee": "Beth"
      },
      {
        "id": "Tue 2 PM",
        "employee": "Ann"
      },
      {
        "id": "Wed 2 AM",
        "employee": "Beth"
      },
      {
        "id": "Wed 2 PM",
        "employee": "Ann"
      },
      {
        "id": "Thu 2 AM",
        "employee": "Beth"
      },
      {
        "id": "Thu 2 PM",
        "employee": "Ann"
      },
      {
        "id": "Fri 2 AM",
        "employee": "Beth"
      },
      {
        "id": "Fri 2 PM",
        "employee": "Ann"
      }
    ]
  },
  "inputMetrics": {
    "employees": 2,
    "shifts": 20,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 20,
    "unassignedShifts": 0,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 2,
    "assignedMandatoryShifts": 20,
    "assignedOptionalShifts": 0,
    "assignedShiftGroups": null,
    "unassignedShiftGroups": null,
    "travelDistance": 0
  }
}

modelOutput contains the schedule with the shifts with Ann and Beth assigned shifts that match the shift rotation rules.

inputMetrics provides a breakdown of the inputs in the input dataset.

KPIs provides the KPIs for the output including:

{
  "assignedShifts": 20,
  "disruptionPercentage": 0.0,
  "activatedEmployees": 2,
  "assignedMandatoryShifts": 20
}

4. Keep shift start times within a range

To prevent employees from having shift start times that vary too much, shiftStartTimeDifferenceInMinutesMax sets on a limit how much difference there can be between shift start times.

shiftStartTimeDifferenceInMinutesMax is defined in period rules:

{
  "periodRules": [
    {
      "id": "shiftsNotCloseTogether",
      "period": "WEEK",
      "shiftStartTimeDifferenceInMinutesMax": 45,
      "satisfiability": "REQUIRED"
    }
  ]
}

PeriodRules must include an ID.

period sets the period the rule applies to, for instance, DAY, WEEK, MONTH, SCHEDULE.

Further information about period:

DAY spans a single day and occurs every day in the schedule.

WEEK spans 7 days and occurs every week (including partial weeks) in the schedule. The default start of the week is Monday, but this can be overridden to any day in the week:

{
  "scheduleParameterization": {
    "weekStart": "THURSDAY"
  }
}

MONTH spans the entire month. MONTH has a variable number of days depending on the days in the month and occurs every month (including partial months) in the schedule.

SCHEDULE spans the entire schedule.

You can define custom periods to apply to this rule in the following way:
{
  "scheduleParameterization": {
    "periods": [
      {
        "id": "PAY_PERIOD",
        "dateSpans": [
          {
            "start": "2023-01-01",
            "end": "2023-01-15"
          }
        ]
      }
    ]
  }
}

The start and end dates are inclusive.

shiftStartTimeDifferenceInMinutesMax defines the maximum allowed difference between shift start times in minutes.

The satisfiability of the rule can be REQUIRED or PREFERRED. If omitted, REQUIRED is the default.

4.1. Required satisfiability

When the satisfiability of the rule is REQUIRED, the Shift start time difference in minutes per period not in required range for employee hard constraint is invoked, which makes sure the start times of shifts in the period do not vary more than the limit specified in shiftStartTimeDifferenceInMinutesMax.

Shifts will be left unassigned if assigning them would break the Shift start time difference in minutes per period not in required range for employee constraint.

In the following example, there are 5 shifts and 1 employee.

The shifts have the following start times:

  • Monday: 0900

  • Tuesday: 1000

  • Wednesday: 0930

  • Thursday: 0930

  • Friday: 0900

4 shifts are assigned. The shift on Tuesday is not assigned because the difference between its start time and the earliest start time of the other shifts is 60 minutes.

start time required example
  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules [email protected]
{
  "config": {
    "run": {
      "name": "Shift start times required example"
    }
  },
  "modelInput": {
    "contracts": [
      {
        "id": "fullTimeContract",
        "periodRules": [
          {
            "id": "shiftsNotCloseTogether",
            "period": "WEEK",
            "shiftStartTimeDifferenceInMinutesMax": 45,
            "satisfiability": "REQUIRED"
          }
        ]
      }
    ],
    "employees": [
      {
        "id": "Ann",
        "contracts": [
          "fullTimeContract"
        ]
      }
    ],
    "shifts": [
      {
        "id": "Mon",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z"
      },
      {
        "id": "Tue",
        "start": "2027-02-02T10:00:00Z",
        "end": "2027-02-02T18:00:00Z"
      },
      {
        "id": "Wed",
        "start": "2027-02-03T09:30:00Z",
        "end": "2027-02-03T17:30:00Z"
      },
      {
        "id": "Thu",
        "start": "2027-02-04T09:30:00Z",
        "end": "2027-02-04T17:30:00Z"
      },
      {
        "id": "Fri",
        "start": "2027-02-05T09:00:00Z",
        "end": "2027-02-05T17:00:00Z"
      }
    ]
  }
}
To request the solution, locate the 'ID' from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/<ID>
{
  "run": {
    "id": "ID",
    "name": "Shift start times required example",
    "submitDateTime": "2025-05-15T07:09:39.245875276Z",
    "startDateTime": "2025-05-15T07:09:51.402495633Z",
    "activeDateTime": "2025-05-15T07:09:51.592766191Z",
    "completeDateTime": "2025-05-15T07:10:22.491774661Z",
    "shutdownDateTime": "2025-05-15T07:10:22.707922044Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/-1medium/0soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon",
        "employee": "Ann"
      },
      {
        "id": "Tue",
        "employee": null
      },
      {
        "id": "Wed",
        "employee": "Ann"
      },
      {
        "id": "Thu",
        "employee": "Ann"
      },
      {
        "id": "Fri",
        "employee": "Ann"
      }
    ]
  },
  "inputMetrics": {
    "employees": 1,
    "shifts": 5,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 4,
    "unassignedShifts": 1,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 1,
    "assignedMandatoryShifts": 4,
    "assignedOptionalShifts": 0,
    "assignedShiftGroups": null,
    "unassignedShiftGroups": null,
    "travelDistance": 0
  }
}

modelOutput contains the schedule with 1 shift not assigned to avoid breaking a hard constraint.

inputMetrics provides a breakdown of the inputs in the input dataset.

KPIs provides the KPIs for the output including:

{
  "assignedShifts": 4,
  "unassignedShifts": 1,
  "activatedEmployees": 1,
  "assignedMandatoryShifts": 4
}

4.2. Preferred satisfiability

When the satisfiability of the rule is PREFERRED, the Shift start time difference in minutes per period not in preferred range for employee soft constraint is invoked.

{
  "periodRules": [
    {
      "id": "shiftsNotCloseTogether",
      "period": "WEEK",
      "shiftStartTimeDifferenceInMinutesMax": 45,
      "satisfiability": "PREFERRED"
    }
  ]
}

Shifts will still be assigned to employees even if assigning the shifts breaks the Shift start time difference in minutes per period not in preferred range for employee soft constraint and shift start times vary by more than the limit set in shiftStartTimeDifferenceInMinutesMax. This constraint adds a soft penalty to the run score for any matches to the constraint, incentivizing Timefold to find an alternative solution.

In the following example, there are 5 shifts and 1 employee.

The shifts have the following start times:

  • Monday: 0900

  • Tuesday: 1000

  • Wednesday: 0930

  • Thursday: 0930

  • Friday: 0900

All 5 shifts are assigned. The shift on Tuesday has a start time that is more than the 45 minutes difference limit that set in shiftStartTimeDifferenceInMinutesMax and a soft penalty is added to the run score.

start time preferred example
  • Input

  • Output

Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules [email protected]
{
  "config": {
    "run": {
      "name": "Shift start times preferred example"
    }
  },
  "modelInput": {
    "contracts": [
      {
        "id": "fullTimeContract",
        "periodRules": [
          {
            "id": "shiftsNotCloseTogether",
            "period": "WEEK",
            "shiftStartTimeDifferenceInMinutesMax": 45,
            "satisfiability": "PREFERRED"
          }
        ]
      }
    ],
    "employees": [
      {
        "id": "Ann",
        "contracts": [
          "fullTimeContract"
        ]
      }
    ],
    "shifts": [
      {
        "id": "Mon",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z"
      },
      {
        "id": "Tue",
        "start": "2027-02-02T10:00:00Z",
        "end": "2027-02-02T18:00:00Z"
      },
      {
        "id": "Wed",
        "start": "2027-02-03T09:30:00Z",
        "end": "2027-02-03T17:30:00Z"
      },
      {
        "id": "Thu",
        "start": "2027-02-04T09:30:00Z",
        "end": "2027-02-04T17:30:00Z"
      },
      {
        "id": "Fri",
        "start": "2027-02-05T09:00:00Z",
        "end": "2027-02-05T17:00:00Z"
      }
    ]
  }
}
To request the solution, locate the 'ID' from the response to the post operation and append it to the following API call:
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/<ID>
{
  "run": {
    "id": "ID",
    "name": "Shift start times preferred example",
    "submitDateTime": "2025-05-15T09:14:08.34754595Z",
    "startDateTime": "2025-05-15T09:14:29.808163485Z",
    "activeDateTime": "2025-05-15T09:14:30.054858995Z",
    "completeDateTime": "2025-05-15T09:15:01.19306088Z",
    "shutdownDateTime": "2025-05-15T09:15:01.518181802Z",
    "solverStatus": "SOLVING_COMPLETED",
    "score": "0hard/0medium/-30soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon",
        "employee": "Ann"
      },
      {
        "id": "Tue",
        "employee": "Ann"
      },
      {
        "id": "Wed",
        "employee": "Ann"
      },
      {
        "id": "Thu",
        "employee": "Ann"
      },
      {
        "id": "Fri",
        "employee": "Ann"
      }
    ]
  },
  "inputMetrics": {
    "employees": 1,
    "shifts": 5,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 5,
    "unassignedShifts": 0,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 1,
    "assignedMandatoryShifts": 5,
    "assignedOptionalShifts": 0,
    "assignedShiftGroups": null,
    "unassignedShiftGroups": null,
    "travelDistance": 0
  }
}

modelOutput contains the schedule with all shifts assigned.

inputMetrics provides a breakdown of the inputs in the input dataset.

KPIs provides the KPIs for the output including:

{
  "assignedShifts": 5,
  "activatedEmployees": 1,
  "assignedMandatoryShifts": 5
}

Next

  • See the full API spec or try the online API.

  • Learn more about employee shift scheduling from our YouTube playlist.

  • Working with Employee availability.

  • © 2025 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default