Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Employee Shift Scheduling
  • Shift service constraints
  • Cost management

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
      • Employee contracts: period rules
      • Employee contracts: shift rules
      • 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

Cost management

Managing costs is an important factor in employee shift scheduling.

Costs include the wages paid to employees who are assigned to work the shifts and the operating cost of the shift, for instance, if a locations is hired or supplies are needed for the shift.

Different employees have different salaries, and different shifts have different operating costs.

With Timefold you can define employee cost groups and shift cost groups to help keep shift assignments on budget.

This guide explains how to manage costs with the following examples:

  • Managing shift costs

  • Maximum shift cost

  • Preferred shift cost

  • Control which shifts and employees the cost rules apply to

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. Managing shift costs

Cost rules are defined as global rules:

{
  "globalRules": {
    "costsRules": [
      {
        "id": "max1k",
        "period": "SCHEDULE",
        "employeeShiftCostDetails": [
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
            "shiftCostGroup": "SHIFT_COST_GROUP_A",
            "cost": 500
          }
        ],
        "totalCostsMax": 1000,
        "satisfiability": "REQUIRED"
      }
    ]
  }
}

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

employeeShiftCostDetails includes the employeeCostGroup, shiftCostGroup, and cost.

  • employeeCostGroup: the employee costGroup that is included in this grouping.

  • shiftCostGroup: the shift costGroup that is included in this grouping.

  • cost: the cost of assigning a shift in this pairing of employee costGroup and shift costGroup. Cost does not specify a specific currency.

employeeShiftCostDetails can include multiple groups:

{
  "globalRules": {
    "costsRules": [
      {
        "id": "max1k",
        "period": "SCHEDULE",
        "employeeShiftCostDetails": [
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
            "shiftCostGroup": "SHIFT_COST_GROUP_A",
            "cost": 500
          },
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_B",
            "shiftCostGroup": "SHIFT_COST_GROUP_B",
            "cost": 400
          }
        ],
        "totalCostsMax": 1000,
        "satisfiability": "REQUIRED"
      }
    ]
  }
}

totalCostsMax is the maximum cost of assigning shifts to employees in the solution.

Employees must include which costgroup they belong to:

{
  "id": "Ann",
  "costGroup": "EMPLOYEE_COST_GROUP_A"
}

Shifts must include which costgroup they belong to:

{
  "id": "Mon 1",
  "start": "2027-02-01T09:00:00Z",
  "end": "2027-02-01T17:00:00Z",
  "costGroup": "SHIFT_COST_GROUP_A"
}

The satisfiability can be REQUIRED or PREFERRED.

2. Maximum shift cost

When the satisfiability of the rule is REQUIRED, the Costs per period not in required range hard constraint is invoked, which makes sure the cost of assigning shifts does not exceed the limit specified in totalCostsMax.

Shifts will be left unassigned if assigning them would break the Costs per period not in required range constraint.

In the following example, the costsRules sets a max limit of 1000. There are 3 employees and 3 shifts, assigning 1 employee to 1 shift has a cost of 500. 2 shifts are assigned and 1 is left unassigned.

cost management required
  • 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": "Cost management - required"
    }
  },
  "modelInput": {
    "globalRules": {
      "costsRules": [
        {
          "id": "max1k",
          "period": "SCHEDULE",
          "employeeShiftCostDetails": [
            {
              "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
              "shiftCostGroup": "SHIFT_COST_GROUP_A",
              "cost": 500
            }
          ],
          "totalCostsMax": 1000,
          "satisfiability": "REQUIRED"
        }
      ]
    },
    "employees": [
      {
        "id": "Ann",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      },
      {
        "id": "Beth",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      },
      {
        "id": "Carl",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      }
    ],
    "shifts": [
      {
        "id": "Mon 1",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      },
      {
        "id": "Mon 2",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      },
      {
        "id": "Mon 3",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      }
    ]
  }
}
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": "Cost management - required",
    "submitDateTime": "2025-05-02T05:25:28.786489636Z",
    "startDateTime": "2025-05-02T05:25:45.683503799Z",
    "activeDateTime": "2025-05-02T05:25:45.872076311Z",
    "completeDateTime": null,
    "shutdownDateTime": null,
    "solverStatus": "SOLVING_ACTIVE",
    "score": "0hard/-1medium/0soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon 1",
        "employee": "Ann"
      },
      {
        "id": "Mon 2",
        "employee": "Beth"
      },
      {
        "id": "Mon 3",
        "employee": null
      }
    ]
  },
  "inputMetrics": {
    "employees": 3,
    "shifts": 3,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 2,
    "unassignedShifts": 1,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 2,
    "assignedMandatoryShifts": 2,
    "assignedOptionalShifts": 0,
    "assignedShiftGroups": null,
    "unassignedShiftGroups": null,
    "travelDistance": 0
  }
}

modelOutput contains the schedule which has shifts assigned and keeps the cost under the totalCostsMax.

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

KPIs provides the KPIs for the output including:

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

3. Preferred shift cost

With a PREFERRED satisfiability, a totalCostsMin can be included to set the preferred minimum cost of assigning the shifts.

When the satisfiability of the rule is PREFERRED, the Costs per period not in preferred range soft constraint is invoked, which adds a soft penalty to the run score when the cost of the schedule is below the limit set in totalCostsMin or exceeds the limit set in totalCostsMax.

Timefold is incentivized to use solutions with the best score.

Shifts will still be assigned even if the cost is not within the specified range.

{
  "globalRules": {
    "costsRules": [
      {
        "id": "max1k",
        "period": "SCHEDULE",
        "employeeShiftCostDetails": [
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
            "shiftCostGroup": "SHIFT_COST_GROUP_A",
            "cost": 500
          }
        ],
        "totalCostsMin": 500,
        "totalCostsMax": 1000,
        "satisfiability": "PREFERRED"
      }
    ]
  }
}

In the following example, the costsRules sets a preferred max limit of 1000. There are 3 employees and 3 shifts, assigning 1 employee to 1 shift has a cost of 500. All 3 shifts are assigned and a soft penalty is applied to the run score.

cost management preferred
  • 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": "Cost management - preferred"
    }
  },
  "modelInput": {
    "globalRules": {
      "costsRules": [
        {
          "id": "max1k",
          "period": "SCHEDULE",
          "employeeShiftCostDetails": [
            {
              "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
              "shiftCostGroup": "SHIFT_COST_GROUP_A",
              "cost": 500
            }
          ],
          "totalCostsMin": 500,
          "totalCostsMax": 1000,
          "satisfiability": "PREFERRED"
        }
      ]
    },
    "employees": [
      {
        "id": "Ann",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      },
      {
        "id": "Beth",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      },
      {
        "id": "Carl",
        "costGroup": "EMPLOYEE_COST_GROUP_A"
      }
    ],
    "shifts": [
      {
        "id": "Mon 1",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      },
      {
        "id": "Mon 2",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      },
      {
        "id": "Mon 3",
        "start": "2027-02-01T09:00:00Z",
        "end": "2027-02-01T17:00:00Z",
        "costGroup": "SHIFT_COST_GROUP_A"
      }
    ]
  }
}
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": "Cost management - preferred",
    "submitDateTime": "2025-05-02T08:20:48.926748573Z",
    "startDateTime": "2025-05-02T08:21:00.990412532Z",
    "activeDateTime": "2025-05-02T08:21:01.153267224Z",
    "completeDateTime": null,
    "shutdownDateTime": null,
    "solverStatus": "SOLVING_ACTIVE",
    "score": "0hard/0medium/-500soft",
    "tags": [
      "system.profile:default"
    ],
    "validationResult": {
      "summary": "OK"
    }
  },
  "modelOutput": {
    "shifts": [
      {
        "id": "Mon 1",
        "employee": "Ann"
      },
      {
        "id": "Mon 2",
        "employee": "Beth"
      },
      {
        "id": "Mon 3",
        "employee": "Carl"
      }
    ]
  },
  "inputMetrics": {
    "employees": 3,
    "shifts": 3,
    "pinnedShifts": 0
  },
  "kpis": {
    "assignedShifts": 3,
    "unassignedShifts": 0,
    "workingTimeFairnessPercentage": null,
    "disruptionPercentage": 0.0,
    "averageDurationOfEmployeesPreferencesMet": null,
    "minimumDurationOfPreferencesMetAcrossEmployees": null,
    "averageDurationOfEmployeesUnpreferencesViolated": null,
    "maximumDurationOfUnpreferencesViolatedAcrossEmployees": null,
    "activatedEmployees": 3,
    "assignedMandatoryShifts": 3,
    "assignedOptionalShifts": 0,
    "assignedShiftGroups": null,
    "unassignedShiftGroups": null,
    "travelDistance": 0
  }
}

modelOutput contains the schedule with all 3 shifts assigned.

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

KPIs provides the KPIs for the output including:

{
  "assignedShifts": 3,
  "activatedEmployees": 3,
  "assignedMandatoryShifts": 3
}

4. Control which shifts and employees the cost rules apply to

Cost rules can be limited to apply to only certain shifts or employees by using tags.

4.1. Include or exclude 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.

4.1.1. Include shift tags

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

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.

4.1.2. Exclude shift tags

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

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, would 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, would exclude any shift that includes the tags Part-time or Weekend, whether they occur together or not.

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

4.2. Example include shift tags

{
  "globalRules": {
    "costsRules": [
      {
        "id": "max1k",
        "period": "SCHEDULE",
        "includeShiftTags": ["Part-time"],
        "employeeShiftCostDetails": [
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
            "shiftCostGroup": "SHIFT_COST_GROUP_A",
            "cost": 500
          }
        ],
        "totalCostsMax": 1000,
        "satisfiability": "REQUIRED"
      }
    ]
  }
}

4.3. Include or exclude employees with employee tags

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

{
  "employees": [
    {
      "id": "Ann",
      "tags": "Part-time"
    }
}

Use includeEmployeeTags to include employees with specific tags or excludeEmployeeTags to exclude employees with specific tags.

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

4.3.1. Include employee tags

{
  "includeemployeeTags": ["Part-time", "Relief"],
  "employeeTagMatches": "ALL"
}
Using ALL or ANY:

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

4.3.2. Exclude employee tags

{
  "excludeEmployeeTags": ["Part-time", "Weekend"],
  "employeeTagMatches": "ALL"
}
Using ALL or ANY:

With employeeTagMatches set to ALL, all tags defined by the rule’s excludeEmployeeTags attribute cannot be present in the employee. This is useful when you want to exclude things in combination with each other. For instance, excluding the employee tags Part-time and Relief with employeeTagMatches set to All, would exclude employees that include the tags Part-time and Relief from the rule. Employees tagged only Part-time or only Relief will not be excluded.

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

The rule can define either includeEmployeeTags or excludeEmployeeTags, but not both.

4.3.3. Example include employee tags

{
  "globalRules": {
    "costsRules": [
      {
        "id": "max1k",
        "period": "SCHEDULE",
        "includeEmployeeTags": ["Relief"],
        "employeeShiftCostDetails": [
          {
            "employeeCostGroup": "EMPLOYEE_COST_GROUP_A",
            "shiftCostGroup": "SHIFT_COST_GROUP_A",
            "cost": 500
          }
        ],
        "totalCostsMax": 1000,
        "satisfiability": "REQUIRED"
      }
    ]
  }
}

Next

  • See the Employee shift scheduling user guide

  • Understand the constraints of the Employee Shift Scheduling model.

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

  • Manage schedules with Time zones and Daylight Saving Time (DST) changes.

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