Shift rotations
Shift rotations occur when employees work one type of shift for a set period followed by a shift of a different type for a set period, for instance, 1 week of morning shifts followed by 1 week of afternoon shifts.
Prerequisites
Learn how to configure an API Key to run the examples in this guide:
-
Log in to Timefold Platform: app.timefold.ai
-
From the Dashboard, click your tenant, and from the drop-down menu select Tenant Settings, then choose API Keys.
-
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. Defining shift rotation rules
shiftRotationRules
are defined in contracts
:
{
"contracts": [
{
"id": "fullTimeContract",
"shiftRotationRules": [
{
"id": "rotateMorningAndAfternoonShiftsWeekly",
"rotationPeriod": {
"type": "WEEKLY"
},
"rotationGroups": [
{
"id": "morningShifts",
"includeShiftTags": [
"Morning"
],
"shiftTagMatches": "ANY"
},
{
"id": "afternoonShifts",
"includeShiftTags": [
"Afternoon"
],
"shiftTagMatches": "ANY"
}
],
"satisfiability": "REQUIRED"
}
]
}
]
}
shiftRotationRules
must include an id
.
rotationPeriod
specifies the length each rotation lasts for and must include the type
. Currently, WEEKLY
and CUSTOM
are supported.
rotationGroups
contains multiple groups.
Each group must include an id
and the shift tags to be included in the group for a rotation.
Shifts are included or excluded from the rotations groups with includeShiftTags
or excludeShiftTags
.
The rule can define either includeShiftTags or excludeShiftTags, not both.
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 Morning
and Afternoon
, 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.
2. Required weekly shift rotations
When the satisfiability of the rule is REQUIRED
and the rotation period is WEEKLY
, 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.
-
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": "Required shift rotations example"
}
},
"modelInput": {
"contracts": [
{
"id": "fullTimeContract",
"periodRules": [
{
"id": "Max8HoursPerDay",
"period": "DAY",
"satisfiability": "REQUIRED",
"minutesWorkedMax": 480
}
],
"shiftRotationRules": [
{
"id": "rotateMorningAndAfternoonShiftsWeekly",
"rotationPeriod": {
"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>
{
"metadata": {
"id": "ID",
"parentId": null,
"originId": "ID",
"name": "Required shift rotations example",
"submitDateTime": "2025-06-19T06:39:05.474547181Z",
"startDateTime": "2025-06-19T06:39:20.839372083Z",
"activeDateTime": "2025-06-19T06:39:21.017286426Z",
"completeDateTime": "2025-06-19T06:39:51.974011807Z",
"shutdownDateTime": "2025-06-19T06:39:52.299836975Z",
"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,
"disruptionPercentage": 0.0,
"activatedEmployees": 2,
"assignedMandatoryShifts": 20,
"assignedOptionalShifts": 0,
"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,
"activatedEmployees": 2,
"assignedMandatoryShifts": 20
}
3. Preferred weekly shift rotations
When the satisfiability of the rule is PREFERRED
and the rotation period is WEEKLY
, the Preferred shift rotation not met for employee
soft constraint is invoked.
By default, every constraint has a weight of Learn about changing the weight of this constraint:Use the constraint configuration’s
|
When employees are assigned shifts that break the shiftRotationRules
, this constraint adds a soft penalty to the dataset score, incentivizing Timefold to find an alternative solution.
This rule is more likely to be satisfied for employees with a higher employee priority. |
{
"contracts": [
{
"id": "fullTimeContract",
"shiftRotationRules": [
{
"id": "rotateMorningAndAfternoonShiftsWeekly",
"rotationPeriod": {
"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 the shifts are morning shifts.
Ann is assigned 2 weeks of morning shifts, which breaks the soft constraint.
-
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": "Preferred shift rotations example"
}
},
"modelInput": {
"contracts": [
{
"id": "fullTimeContract",
"shiftRotationRules": [
{
"id": "rotateMorningAndAfternoonShiftsWeekly",
"rotationPeriod": {
"type": "WEEKLY"
},
"rotationGroups": [
{
"id": "morningShifts",
"includeShiftTags": [
"Morning"
],
"shiftTagMatches": "ANY"
},
{
"id": "afternoonShifts",
"includeShiftTags": [
"Afternoon"
],
"shiftTagMatches": "ANY"
}
],
"satisfiability": "PREFERRED"
}
]
}
],
"employees": [
{
"id": "Ann",
"contracts": [
"fullTimeContract"
]
}
],
"shifts": [
{
"id": "Mon 1 AM",
"start": "2027-02-01T06:00:00Z",
"end": "2027-02-01T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Tue 1 AM",
"start": "2027-02-02T06:00:00Z",
"end": "2027-02-02T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Wed 1 AM",
"start": "2027-02-03T06:00:00Z",
"end": "2027-02-03T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Thu 1 AM",
"start": "2027-02-04T06:00:00Z",
"end": "2027-02-04T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Fri 1 AM",
"start": "2027-02-05T06:00:00Z",
"end": "2027-02-05T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Mon 2 AM",
"start": "2027-02-08T06:00:00Z",
"end": "2027-02-08T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Tue 2 AM",
"start": "2027-02-09T06:00:00Z",
"end": "2027-02-09T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Wed 2 AM",
"start": "2027-02-10T06:00:00Z",
"end": "2027-02-10T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Thu 2 AM",
"start": "2027-02-11T06:00:00Z",
"end": "2027-02-11T14:00:00Z",
"tags": ["Morning"]
},
{
"id": "Fri 2 AM",
"start": "2027-02-12T06:00:00Z",
"end": "2027-02-12T14:00:00Z",
"tags": ["Morning"]
}
]
}
}
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>
{
"metadata": {
"id": "ID",
"parentId": null,
"originId": "ID",
"name": "Preferred shift rotations example",
"submitDateTime": "2025-06-19T07:11:29.173961222Z",
"startDateTime": "2025-06-19T07:11:40.3443435Z",
"activeDateTime": "2025-06-19T07:11:40.503308602Z",
"completeDateTime": "2025-06-19T07:12:11.278117228Z",
"shutdownDateTime": "2025-06-19T07:12:11.521967195Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/-24000soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"shifts": [
{
"id": "Mon 1 AM",
"employee": "Ann"
},
{
"id": "Tue 1 AM",
"employee": "Ann"
},
{
"id": "Wed 1 AM",
"employee": "Ann"
},
{
"id": "Thu 1 AM",
"employee": "Ann"
},
{
"id": "Fri 1 AM",
"employee": "Ann"
},
{
"id": "Mon 2 AM",
"employee": "Ann"
},
{
"id": "Tue 2 AM",
"employee": "Ann"
},
{
"id": "Wed 2 AM",
"employee": "Ann"
},
{
"id": "Thu 2 AM",
"employee": "Ann"
},
{
"id": "Fri 2 AM",
"employee": "Ann"
}
]
},
"inputMetrics": {
"employees": 1,
"shifts": 10,
"pinnedShifts": 0
},
"kpis": {
"assignedShifts": 10,
"unassignedShifts": 0,
"disruptionPercentage": 0.0,
"activatedEmployees": 1,
"assignedMandatoryShifts": 10,
"assignedOptionalShifts": 0,
"travelDistance": 0
}
}
modelOutput
contains the schedule with Ann assigned to all 10 morning shifts.
inputMetrics
provides a breakdown of the inputs in the input dataset.
KPIs
provides the KPIs for the output including:
{
"assignedShifts": 10,
"activatedEmployees": 1,
"assignedMandatoryShifts": 10
}
4. Required custom shift rotations
When the satisfiability of the rule is REQUIRED
and the rotation period is CUSTOM
, the Custom rotation duration in days exceeds required maximum 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. The constraint restricts the length of such periods.
Shifts will be left unassigned if assigning them would break the Required shift rotation not met for employee
constraint.
requiredRotationPeriodMaxSizeInDays
specifies the maximum duration of the rotation period in days.
With requiredRotationPeriodMaxSizeInDays
set to 30
, employees will be assigned shifts from one of the rotationGroups for periods of at most 30 days.
{
"rotationPeriod": {
"type": "CUSTOM",
"requiredRotationPeriodMaxSizeInDays": 30
}
}
4.1. Length of the maximum rotation period
Consider the following shifts matching either rotation group morningShifts
, or rotation group afternoonShifts
:
-
Shift 1 morning: 1st Jan 2025
-
Shift 2 morning: 2nd Jan 2025
-
Shift 3 morning: 3rd Jan 2025
-
Shift 4 morning: 5th Jan 2025
-
Shift 5 afternoon: 8th Jan 2025
-
Shift 6 afternoon: 10th Jan 2025
-
Shift 7 afternoon: 11th Jan 2025
-
Shift 8 afternoon: 13th Jan 2025
Rotation period for group morningShifts
:
-
starts on 1st Jan 2025 (Shift 1)
-
ends on 5th Jan 2025 (Shift 4)
-
has length of 5 days.
Rotation period for group afternoonShifts
:
-
starts on 8th Jan 2025 (Shift 5)
-
ends on 13th Jan 2025 (Shift 8)
-
has length of 6 days.
Free days in between different rotation periods (6th Jan 2025, 7th Jan 2025) are not included in the period’s length.
5. Preferred custom shift rotations
When the rotation period is CUSTOM
, the Custom rotation duration in days not in preferred range for employee
soft constraint is invoked, which makes sure employees are assigned shifts from 1 rotationGroups
during the first period, then a different rotationGroups
the next period. The constraint restricts the length of such periods.
By default, every constraint has a weight of Learn about changing the weight of this constraint:Use the constraint configuration’s
|
When employees are assigned shifts that break the shiftRotationRules
, this constraint adds a soft penalty to the dataset score, incentivizing Timefold to find an alternative solution.
This rule is more likely to be satisfied for employees with a higher employee priority. |
preferredRotationPeriodMinSizeInDays
specifies the preferred minimum duration of the rotation period in days.
preferredRotationPeriodMaxSizeInDays
specifies the preferred maximum duration of the rotation period in days.
With preferredRotationPeriodMinSizeInDays
set to 7
, employees will be preferably assigned shifts from one of the rotationGroups for periods of at least 7 days.
With preferredRotationPeriodMaxSizeInDays
set to 14
, employees will be preferably assigned shifts from one of the rotationGroups for periods of at most 14 days.
{
"rotationPeriod": {
"type": "CUSTOM",
"preferredRotationPeriodMinSizeInDays": 7,
"preferredRotationPeriodMaxSizeInDays": 14
}
}
5.1. Length of the maximum rotation period
Consider the following shifts matching either rotation group morningShifts
, or rotation group afternoonShifts
:
-
Shift 1 morning: 1st Jan 2025
-
Shift 2 morning: 2nd Jan 2025
-
Shift 3 morning: 3rd Jan 2025
-
Shift 4 morning: 5th Jan 2025
-
Shift 5 afternoon: 8th Jan 2025
-
Shift 6 afternoon: 10th Jan 2025
-
Shift 7 afternoon: 11th Jan 2025
-
Shift 8 afternoon: 13th Jan 2025
Rotation period for group morningShifts
:
-
starts on 1st Jan 2025 (Shift 1)
-
ends on 5th Jan 2025 (Shift 4)
-
has length of 5 days.
Rotation period for group afternoonShifts
:
-
starts on 8th Jan 2025 (Shift 5)
-
ends on 13th Jan 2025 (Shift 8)
-
has length of 6 days.
Free days in between different rotation periods (6th Jan 2025, 7th Jan 2025) are not included in the period’s length.
5.2. Length of the minimum rotation period
Consider the following shifts matching either rotation group A, or rotation group B:
-
Shift 1 morning: 1st Jan 2025
-
Shift 2 morning: 2nd Jan 2025
-
Shift 3 morning: 3rd Jan 2025
-
Shift 4 morning: 5th Jan 2025
-
Shift 5 afternoon: 8th Jan 2025
-
Shift 6 afternoon: 10th Jan 2025
-
Shift 7 afternoon: 11th Jan 2025
-
Shift 8 afternoon: 13th Jan 2025
Rotation period for group morningShifts
:
-
starts on 1st Jan 2025 (Shift1)
-
ends on 7th Jan 2025 (free day)
-
has length of 7 days.
Rotation period for group afternoonShifts
:
-
starts on 6th Jan 2025 (free day)
-
ends on 12th Jan 2025 (Shift 8)
-
has length of 8 days.
Free days in between different rotation periods (6th Jan 2025, 7th Jan 2025) are included in the period’s length.
Next
-
See the full API spec or try the online API.
-
Learn more about employee shift scheduling from our YouTube playlist.
-
See other options for Shift rotations and patterns.