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:
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:
-
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. 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.
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.

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.

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

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