Recommendations
Managing complex schedules for a large number of employees is a difficult task, and when an employee is suddenly unavailable to cover a shift they’ve been scheduled for, it can be difficult to know who to schedule in their place.
Timefold’s recommendations API can provide recommendations ordered by the best fit to cover a shift at the last minute.
This guide explains recommendations with the following examples.
Recommendations for a shift
Learn how to configure an API Key to run the examples in this guide:
In the examples, replace |
In the following example, Ann and Beth have been assigned shifts, but Ann has called in sick for her shift.
Timefold’s recommendations API can provide recommendations for the best employee to assign to Ann’s shift.
The recommendations input dataset
A recommendation input dataset includes the following:
Max number of recommendations
The maximum number of employees to recommend for a shift:
{
"maxNumberOfRecommendations": 2
}
Fit shift ID
The ID of the shift the recommended employee will be assigned.
{
"fitShiftId": "2027-02-01"
}
Model input
The model input must include the employees and the shifts.
{
"modelInput": {
"employees": [
{
"id": "Ann",
"unavailableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
],
"availableTimeSpans": [
{
"start": "2027-02-02T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Beth",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Carl",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Dan",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Elsa",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Flo",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
}
],
"shifts": [
{
"id": "2027-02-01-a",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z"
},
{
"id": "2027-02-01-b",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"employee": "Beth"
}
]
}
}
Ann is unavailable for her shift, so an unavailability time span has been added for Ann:
{
"id": "Ann",
"unavailableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
],
"availableTimeSpans": [
{
"start": "2027-02-02T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
}
Learn more about Employee availability and preferences.
Any shifts that are already assigned must include the employee ID of the employee they are assigned to.
Shifts that have been dropped by an employee don’t include an employee ID.
In this case, shift 2027-02-01-a was assigned to Ann, but Ann has called in sick, so no employee ID is included.
{
"shifts": [
{
"id": "2027-02-01-a",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z"
},
{
"id": "2027-02-01-b",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"employee": "Beth"
}
]
}
Submit the input dataset
Submit the recommendations input dataset to the API endpoint: /v1/schedules/recommendations/recommend-employees.
If you want individual constraint match justifications included in the response, you can specify an optional boolean includeJustifications query parameter:
/v1/schedules/recommendations/recommend-employees?includeJustifications=true.
The HTTP response provides the recommendations.
-
Input
-
Output (without justifications)
-
Output (with justifications)
Try this example in Timefold Platform by saving this JSON into a file called sample.json and make the following API call:
|
To get recommendations without constraint match justifications (default):
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/recommendations/recommend-employees [email protected]
To get recommendations with constraint match justifications:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/recommendations/recommend-employees?includeJustifications=true [email protected]
{
"maxNumberOfRecommendations": 2,
"fitShiftId": "2027-02-01-a",
"modelInput": {
"employees": [
{
"id": "Ann",
"unavailableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
],
"availableTimeSpans": [
{
"start": "2027-02-02T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Beth",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Carl",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Dan",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Elsa",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Flo",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
}
],
"shifts": [
{
"id": "2027-02-01-a",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z"
},
{
"id": "2027-02-01-b",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"employee": "Beth"
}
]
}
}
{
"recommendations": [
{
"employeeId": "Carl",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
},
{
"employeeId": "Dan",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
}
]
}
modelOutput contains two recommendations to cover shift 2027-02-01-a.
{
"recommendations": [
{
"employeeId": "Carl",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchesDiff": [
{
"score": "0hard/1medium/0soft",
"justification": {
"shift": "2027-02-01-a",
"description": "Mandatory shift '2027-02-01-a' at '2027-02-01T09:00Z - 2027-02-01T17:00Z' is unassigned."
}
}
],
"matchCountDiff": -1
}
]
},
{
"employeeId": "Dan",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchesDiff": [
{
"score": "0hard/1medium/0soft",
"justification": {
"shift": "2027-02-01-a",
"description": "Mandatory shift '2027-02-01-a' at '2027-02-01T09:00Z - 2027-02-01T17:00Z' is unassigned."
}
}
],
"matchCountDiff": -1
}
]
}
]
}
modelOutput contains two recommendations to cover shift 2027-02-01-a including constraint match justifications.
The recommendations
There are two recommendations for this input dataset: Dan and Carl.
Each recommendation includes the following:
-
The employee: The employee being recommended.
-
The scoreDiffs: The overall score difference implementing the recommendation will have.
-
The constraintDiffs: The score difference to individual constraints.
Constraint scores without justifications
The score for each constraint that has changed, for instance:
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
Which indicates the medium score has improved by 1, because the shift is no longer unassigned.
Constraint scores with justifications
The score and justifications for each constraint that has changed, for instance:
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchesDiff": [
{
"score": "0hard/1medium/0soft",
"justification": {
"shift": "2027-02-01-a",
"description": "Mandatory shift '2027-02-01-a' at '2027-02-01T09:00Z - 2027-02-01T17:00Z' is unassigned."
}
}
],
"matchCountDiff": -1
}
Recommendations and employee skills
In the previous example, any one of the available employees could have been assigned to the shift Ann dropped.
However, employees generally have different skills and availability which could help determine which employee is considered the best fit.
In the following example, Ann is a shift supervisor and the shift that needs to be filled must be filled by another shift supervisor.
-
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/recommendations/recommend-employees [email protected]
{
"maxNumberOfRecommendations": 2,
"fitShiftId": "2027-02-01-a",
"modelInput": {
"employees": [
{
"id": "Ann",
"skills": [
{
"id": "supervisor"
}
],
"unavailableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
],
"availableTimeSpans": [
{
"start": "2027-02-02T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Beth",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Carl",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Dan",
"skills": [
{
"id": "supervisor"
}
],
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Elsa",
"skills": [
{
"id": "supervisor"
}
],
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Flo",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
}
],
"shifts": [
{
"id": "2027-02-01-a",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"requiredSkills": [
"supervisor"
]
},
{
"id": "2027-02-01-b",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"employee": "Beth"
}
]
}
}
{
"recommendations": [
{
"employeeId": "Dan",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
},
{
"employeeId": "Elsa",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
}
]
}
modelOutput contains two recommendations to cover shift 2027-02-01-a.
This time the recommendations are for Dan and Elsa who both have the "supervisor" skill.
Recommendations with employee preferences
Employees can define a range of preferences including unpreferred work times and who they prefer to work with.
In the following example, Dan prefers not to work on Mondays.
-
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/recommendations/recommend-employees [email protected]
{
"maxNumberOfRecommendations": 2,
"fitShiftId": "2027-02-01-a",
"modelInput": {
"employees": [
{
"id": "Ann",
"skills": [
{
"id": "supervisor"
}
],
"unavailableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
],
"availableTimeSpans": [
{
"start": "2027-02-02T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Beth",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Carl",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Dan",
"skills": [
{
"id": "supervisor"
}
],
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
],
"unpreferredTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-02T00:00:00Z"
}
]
},
{
"id": "Elsa",
"skills": [
{
"id": "supervisor"
}
],
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
},
{
"id": "Flo",
"availableTimeSpans": [
{
"start": "2027-02-01T00:00:00Z",
"end": "2027-02-08T00:00:00Z"
}
]
}
],
"shifts": [
{
"id": "2027-02-01-a",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"requiredSkills": [
"supervisor"
]
},
{
"id": "2027-02-01-b",
"start": "2027-02-01T09:00:00Z",
"end": "2027-02-01T17:00:00Z",
"employee": "Beth"
}
]
}
}
{
"recommendations": [
{
"employeeId": "Elsa",
"scoreDiff": "0hard/1medium/0soft",
"constraintDiffs": [
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
},
{
"employeeId": "Dan",
"scoreDiff": "0hard/1medium/-960soft",
"constraintDiffs": [
{
"score": "0hard/0medium/-960soft",
"constraintName": "Employee works during unpreferred time",
"matchCountDiff": 1
},
{
"score": "0hard/1medium/0soft",
"constraintName": "Unassigned mandatory shift",
"matchCountDiff": -1
}
]
}
]
}
modelOutput contains two recommendations to cover shift 2027-02-01-a.
In this scenario, Dan is still recommended because only he and Elsa have the required skill of supervisor, but this time, Elsa is recommended first as she is the best fit, and Dan is recommended second.
In the second recommendation, we see:
{
"score": "0hard/0medium/-960soft",
"constraintName": "Employee works during unpreferred time"
}
This shows a soft score penalty because the recommendation would involve Dan working during an unpreferred time.
Batch recommendations
The examples above request recommendations for a single shift. When several shifts need to be filled at once, you can request recommendations for all of them in a single call instead of calling the API once per shift.
| All requested shifts are fitted against the provided model input. Accepting any one recommendation invalidates all the others. After you apply an assignment, request batch recommendations again for the remaining shifts, if needed. |
The input is the same as for a single shift, except that fitShiftId is replaced by fitShiftIds, which holds the list of the shift IDs to be fitted.
The maximum number of shift IDs in fitShiftIds parameter is 100.
|
As with the single-shift API, you can add the optional includeJustifications=true query parameter to include individual constraint match justifications in the response.
There are two batch endpoints, which differ only in how the recommendations are ordered: per shift or globally across all shifts.
Per-shift recommendations
The recommend-employees-batch-per-shift endpoint returns a separate list of recommended employees for each requested shift, ordered best fit first.
Use this endpoint to get recommendations for many shifts at once, instead of calling the single-shift endpoint once per shift.
For this endpoint, maxNumberOfRecommendations is the maximum number of recommendations returned per shift.
In the following example, three shifts on 2027-02-01 are unassigned.
Ann, Beth and Carl are all available and each has a preferred time span covering all three shifts, but with different priorities: Ann has priority 10, Beth has priority 9, and Carl has priority 8.
{
"maxNumberOfRecommendations": 3,
"fitShiftIds": ["2027-02-01-a", "2027-02-01-b", "2027-02-01-c"],
"modelInput": {
"employees": [
{
"id": "Ann",
"priority": "10",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
},
{
"id": "Beth",
"priority": "9",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
},
{
"id": "Carl",
"priority": "8",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
}
],
"shifts": [
{ "id": "2027-02-01-a", "start": "2027-02-01T09:00:00Z", "end": "2027-02-01T17:00:00Z" },
{ "id": "2027-02-01-b", "start": "2027-02-01T13:00:00Z", "end": "2027-02-01T21:00:00Z" },
{ "id": "2027-02-01-c", "start": "2027-02-01T17:00:00Z", "end": "2027-02-02T01:00:00Z" }
]
}
}
Submit this dataset to /v1/schedules/recommendations/recommend-employees-batch-per-shift:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/recommendations/recommend-employees-batch-per-shift [email protected]
The response contains one entry per requested shift, in the order the shifts were submitted, each with its own list of recommendations ordered best fit first. Each shift’s 480-minute duration overlaps fully with every employee’s preferred time span. Because the soft reward is multiplied by the employee’s priority weight (Carl: ×3, Beth: ×2, Ann: ×1), Carl always scores highest and Ann lowest:
{
"recommendations": [
{
"shiftId": "2027-02-01-a",
"recommendations": [
{ "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Beth", "scoreDiff": "0hard/1medium/960soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Ann", "scoreDiff": "0hard/1medium/480soft", "constraintDiffs": [ ... ] }
]
},
{
"shiftId": "2027-02-01-b",
"recommendations": [
{ "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Beth", "scoreDiff": "0hard/1medium/960soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Ann", "scoreDiff": "0hard/1medium/480soft", "constraintDiffs": [ ... ] }
]
},
{
"shiftId": "2027-02-01-c",
"recommendations": [
{ "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Beth", "scoreDiff": "0hard/1medium/960soft", "constraintDiffs": [ ... ] },
{ "employeeId": "Ann", "scoreDiff": "0hard/1medium/480soft", "constraintDiffs": [ ... ] }
]
}
]
}
Global recommendations
The recommend-employees-batch-global endpoint returns a single list of (shift, employee) assignment candidates ordered across all requested shifts, best fit first.
The first entry answers the question "which shift should I assign next?".
For this endpoint, maxNumberOfRecommendations is the maximum number of recommendations in total across all shifts.
Reusing the same scenario, three shifts on 2027-02-01 are unassigned, and Ann (priority 10), Beth (priority 9) and Carl (priority 8) are all available, each with a preferred time span covering all three shifts:
{
"maxNumberOfRecommendations": 4,
"fitShiftIds": ["2027-02-01-a", "2027-02-01-b", "2027-02-01-c"],
"modelInput": {
"employees": [
{
"id": "Ann",
"priority": "10",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
},
{
"id": "Beth",
"priority": "9",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
},
{
"id": "Carl",
"priority": "8",
"availableTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
],
"preferredTimeSpans": [
{ "start": "2027-02-01T00:00:00Z", "end": "2027-02-08T00:00:00Z" }
]
}
],
"shifts": [
{ "id": "2027-02-01-a", "start": "2027-02-01T09:00:00Z", "end": "2027-02-01T17:00:00Z" },
{ "id": "2027-02-01-b", "start": "2027-02-01T13:00:00Z", "end": "2027-02-01T21:00:00Z" },
{ "id": "2027-02-01-c", "start": "2027-02-01T17:00:00Z", "end": "2027-02-02T01:00:00Z" }
]
}
}
Submit this dataset to /v1/schedules/recommendations/recommend-employees-batch-global:
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/employee-scheduling/v1/schedules/recommendations/recommend-employees-batch-global [email protected]
The response is a single list of assignment candidates ordered across all shifts, best fit first. Each entry identifies both the shift and the recommended employee, so the first entry tells you which shift to assign next. Because Carl has the highest priority (lowest number), his preferred time spans carry the greatest weight and he scores highest for every shift. The first three entries are therefore all Carl; Beth appears in the fourth position:
{
"recommendations": [
{ "shiftId": "2027-02-01-a", "recommendation": { "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] } },
{ "shiftId": "2027-02-01-b", "recommendation": { "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] } },
{ "shiftId": "2027-02-01-c", "recommendation": { "employeeId": "Carl", "scoreDiff": "0hard/1medium/1440soft", "constraintDiffs": [ ... ] } },
{ "shiftId": "2027-02-01-a", "recommendation": { "employeeId": "Beth", "scoreDiff": "0hard/1medium/960soft", "constraintDiffs": [ ... ] } }
]
}
The scoreDiff and constraintDiffs of each recommendation have the same meaning as for single-shift recommendations.
Next
-
See the full API spec or try the online API.
-
Learn more about employee shift scheduling from our YouTube playlist.