# Shift Scheduler: Employee Scheduling & Staff Rota Generator (`jumblemog/shift-scheduler`) Actor

Make staff schedules (rotas) with Google OR-Tools: shifts with roles and headcounts, availability, time off, weekly hours, rest rules, fairness and preferences. Every gap explained. Rota image, PDF, Excel and calendar files. From text, JSON, CSV or Google Sheets. For AI agents.

- **URL**: https://apify.com/jumblemog/shift-scheduler.md
- **Developed by:** [Jumble Mog](https://apify.com/jumblemog) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Shift Scheduler: Employee Scheduling & Staff Rota Generator

**Make a fair staff schedule in seconds, with every shift covered as far as your team allows and a plain reason for every gap.** Tell it who works for you (their roles, hours, days off and preferences) and which shifts need people, as short text lines, JSON, CSV or a Google Sheet, and it works out who works when, keeping to **roles and headcounts, availability and time off, weekly hours, rest between shifts, days in a row, fixed shifts, requests, fairness and labor cost**. Every schedule comes back with **each assignment, each person's hours, the staffing of every shift, why any place stayed empty and what would fill it**, plus a **rota image, a PDF, a web page, an Excel workbook, a CSV and calendar files for the whole team and for each person**. It uses Google's [OR-Tools](https://developers.google.com/optimization/scheduling/employee_scheduling) CP-SAT solver, the open-source engine behind many workforce planning systems. Made for managers of cafés, restaurants, shops, clinics, care homes, call centers and warehouses, for HR and workforce developers, and for AI agents that need a real schedule from one API call.

![A week's rota made by this Actor for a café with nine employees: a row per person and a column per day, each shift a colored block (Morning, Evening or Rush) with the role worked, each person's hours on the right, and a subtitle saying every shift is fully staffed](https://api.apify.com/v2/key-value-stores/Km5b4PG3kYHdZSb1E/records/shift-scheduler-example.png?signature=1nSBAn3yxViqhnQLzWreE)

### What it does

- **Covers every shift as fully as the rules allow**: the people needed per shift, in total or by role (2 baristas and a shift lead), changing by day if you like (more on Fridays and Saturdays). More important shifts are staffed first, and roles few people can do are kept for those people.
- **Keeps the hard rules**: skills, availability, approved time off, the most hours, days and shifts a week, rest between shifts (no closing at 23:00 and opening at 07:00), the most days in a row, one shift a day or split shifts, and no overlaps, with overnight shifts handled across midnight.
- **Gives people their hours**: minimum hours (contracts), target hours, and maximum hours, per person or for everyone.
- **Respects requests**: times people would rather not work, shifts they prefer and fixed shifts they must work. Requests are kept whenever staffing and hours allow, and the result says which weren't.
- **Shares the work fairly**: hours in proportion to each person's limit, weekend and night shifts spread evenly, days off kept together and start times kept steady from day to day.
- **Keeps labor cost down**: give hourly costs and choose the lowest-cost goal, or let cost break ties.
- **Explains every gap**: for each place nobody could fill, who could work that role and the rule that stopped each of them, such as "Only one person can work as charge nurse: Teo (36 of 40 h) would go over the weekly hours limit." Then advice on what would close the gaps: training another person for a role, allowing more hours, or more availability on the busiest days.
- **Files people can use**: a rota image (PNG) and PDF to print or post, a web page that reads well on phones, an Excel workbook (rota grid, every shift, coverage, people and gaps), a CSV of every assignment, JSON, and calendar files (.ics) for Google Calendar, Outlook and Apple Calendar, one for the team and one per person.
- **Reads what you have**: short text lines like "Ana: barista, shift lead; 30-40 h; off Sun", JSON objects with many spellings of each field, CSV or TSV, Excel files, or a Google Sheet link.
- **Many schedules in one run**: one per location, team or week, up to 50, each with its own people, shifts and rules.

### Who it's for

- **Restaurants, cafés, bars and shops**: weekly rotas with opening and closing shifts, rush hours, weekend peaks and part-timers.
- **Clinics, care homes and hospitals**: 24/7 cover with day and night shifts, 12-hour shifts, charge nurses, rest rules and contracted hours.
- **Call centers, support teams and warehouses**: staffing that follows demand through the day and week, with skills and fair weekends.
- **Security, cleaning, events and volunteers**: many sites, shifts at odd hours, people with limited availability.
- **HR, workforce and payroll developers**: a scheduling API for apps, spreadsheets and no-code flows (Make, n8n, Zapier), billed per schedule with no monthly plan.
- **AI agents and LLM apps**: language models can't build a schedule that keeps dozens of rules at once. An agent can pass the team and shifts in plain text and get back a checked schedule, with reasons it can explain.

### How to use it

1. List your people in **Employees**, one per line, such as `Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings`. The form starts with a nine-person café, so you can click **Start** right away.
2. List the shifts in **Shifts**, such as `Morning 07:00-15:00: 1 shift lead, 2 barista; daily`.
3. Set the **Start date** (empty means next Monday), the **Number of days** and any **Rules** that differ from the defaults (40 hours a week, 11 hours rest, 6 days in a row).
4. Click **Start**. The **Schedules** view shows the rota image and summary, **Assignments** every shift worked, **People** each person's hours and calendar link, and **Gaps** any place left empty with the reason.

You can also run it through the [Apify API](https://docs.apify.com/api/v2), on a schedule (every Thursday for next week, say), or from Make, n8n, Zapier and other integrations.

### Writing employees and shifts

**An employee as text**: the name, a colon, the roles they can work, then any of these parts separated by semicolons:

| Part | Examples |
|---|---|
| Weekly hours | `36 h` (target), `20-32 h` (minimum and maximum), `max 30 h`, `min 16 h` |
| Times they can't work | `off Sun`, `no Tue after 17:00`, `unavailable 2026-11-05`, `never nights` |
| The only times they can work | `available weekdays`, `only Mon-Fri after 15:00, weekends` |
| Requests | `prefers mornings`, `avoid weekends`, `would rather not work Fri nights` |
| Fixed shifts | `always Mon Morning`, `fixed: Sat Evening as shift lead` |
| Limits | `max 5 days`, `max 4 shifts`, `4 days in a row` |
| Cost | `$18/h`, `€15.50` |

**An employee as an object** (field names are matched loosely, so `Name`, `employee`, `roles`, `wage` or `availability` work too):

```json
{"name": "Ana", "skills": ["shift lead", "barista"], "minHours": 30, "maxHours": 40, "targetHours": 36,
 "unavailable": "Sun, 2026-11-05", "available": "weekdays", "prefer": "mornings", "avoid": "weekends",
 "fixed": "Mon Morning", "maxDaysPerWeek": 5, "maxConsecutiveDays": 5, "minRestHours": 12, "cost": 18}
```

**A shift as text**: the name and times, then the people needed, in total (`3`) or by role (`1 shift lead, 2 barista`), and the days it runs (`daily`, `Mon-Fri`, `weekends`, `Sat`, `2026-12-24`), with optional parts such as `30 min break` (unpaid), `max 4` (extra people allowed) or `priority high`.

**A shift as an object**:

```json
{"name": "Evening", "start": "15:00", "end": "23:00", "roles": {"shift lead": 1, "barista": 2},
 "days": "Mon-Sat", "break": 30, "maxStaff": 4, "priority": "high"}
```

The headcount can change by day with `"staff": {"Mon-Thu": 2, "Fri-Sat": 4, "default": 3}` or seven numbers, Monday first. Places beyond the named roles can go to anyone on the team.

**Times** can be weekdays (`Mon`, `Mon-Fri`, `weekends`), dates (`2026-11-05`, `Nov 5-9`, `2026-11-05 to 2026-11-09`), shift names (`Morning`), parts of the day (`mornings`, `evenings`, `nights`) or clock times (`after 17:00`, `before 10am`, `08:00-12:00`), alone or together, such as `Tue after 17:00` or `Sat Evening`.

### Examples

**A café week as text** (the form's example):

```json
{
    "title": "Corner Café",
    "employees": [
        "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings",
        "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
        "Cara: barista; 20-32 h; avoid weekends",
        "Dev: shift lead, barista; 32-40 h",
        "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h",
        "Finn: shift lead, barista; max 5 days; $19/h",
        "Gia: barista; 16-24 h",
        "Hal: barista; max 32 h; off Sat, Sun",
        "Ivy: shift lead, barista; 24-32 h; prefers evenings"
    ],
    "shifts": [
        "Morning 07:00-15:00: 1 shift lead, 2 barista; daily",
        "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
        "Rush 11:00-14:00: 1 barista; Mon-Fri"
    ]
}
```

**A ward around the clock for four weeks**, with 12-hour shifts, a senior nurse on every shift and contracted hours:

```json
{
    "title": "Ward 4B, November",
    "startDate": "2026-11-02",
    "days": 28,
    "minRestHours": 12,
    "maxHoursPerWeek": 48,
    "maxConsecutiveDays": 4,
    "employees": [
        "Rosa: nurse, senior; 36 h",
        "Uma: nurse, senior; 36 h",
        "Wren: nurse, senior; 24-36 h",
        "Zoe: nurse, senior; 36 h; avoid nights",
        "Sam: nurse; 24-36 h; off Sun",
        "Teo: nurse; 36 h; avoid nights",
        "Vic: nurse; 36 h",
        "Xia: nurse; 36 h; prefers nights",
        "Yusuf: nurse; 24-36 h",
        "Ava: nurse; 36 h"
    ],
    "shifts": [
        {"name": "Day", "start": "07:00", "end": "19:00", "roles": {"senior": 1, "nurse": 2}, "break": 60},
        {"name": "Night", "start": "19:00", "end": "07:00", "roles": {"senior": 1, "nurse": 1}, "break": 60}
    ],
    "timeLimitSeconds": 60
}
```

**A restaurant where Friday and Saturday need more people**, with approved time off, a fixed shift and the lowest labor cost:

```json
{
    "employees": [
        "Marco: chef; 40 h; $24/h",
        "Lena: chef, line cook; 30-40 h; $19/h",
        "Omar: line cook; max 32 h; $16/h",
        "Priya: server; 20-30 h; $13/h",
        "Jules: server; max 25 h; prefers evenings; $13/h",
        "Kim: server, host; 25-35 h; $14/h"
    ],
    "shifts": [
        {"name": "Lunch", "start": "11:00", "end": "15:00", "roles": {"chef": 1, "server": 1}, "days": "Tue-Sun"},
        {"name": "Dinner", "start": "17:00", "end": "23:00", "roles": {"chef": 1, "line cook": 1},
         "staff": {"Tue-Thu": 3, "Fri-Sat": 5, "Sun": 3}, "days": "Tue-Sun"}
    ],
    "startDate": "2026-11-02",
    "closedDates": ["Mon"],
    "maxShiftsPerDay": 2,
    "timeOff": [{"employee": "Lena", "from": "2026-11-06", "to": "2026-11-07"}],
    "fixedShifts": ["Marco: Sat Dinner"],
    "goal": "cost"
}
```

**From a spreadsheet**: put employees in a Google Sheet with columns such as name, skills, minHours, maxHours, unavailable, prefer and cost, share it as "Anyone with the link", and pass the link:

```json
{
    "employeesCsv": "https://docs.google.com/spreadsheets/d/YOUR_SHEET/edit#gid=0",
    "shiftsCsv": "name,start,end,roles,days\nOpen,06:00,14:00,\"supervisor: 1, picker: 4\",Mon-Sat\nClose,14:00,22:00,\"supervisor: 1, picker: 3\",Mon-Sat",
    "days": 14
}
```

**Several schedules in one run**, sharing the rules:

```json
{
    "maxHoursPerWeek": 38,
    "schedules": [
        {"title": "Downtown", "employees": ["..."], "shifts": ["..."]},
        {"title": "Airport", "employeesCsv": "https://docs.google.com/spreadsheets/d/YOUR_SHEET/edit#gid=1", "shifts": ["..."]}
    ]
}
```

### How-to examples

Each example below is a complete input. The result line is what the run says in `summary`.

#### How to make a staff rota for a café or shop

Write each person and each shift as a line of text:

```json
{"title": "Corner Café",
 "employees": ["Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings", "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
               "Cara: barista; 20-32 h; avoid weekends", "Dev: shift lead, barista; 32-40 h",
               "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h", "Finn: shift lead, barista; max 5 days; $19/h",
               "Gia: barista; 16-24 h", "Hal: barista; max 32 h; off Sat, Sun", "Ivy: shift lead, barista; 24-32 h; prefers evenings"],
 "shifts": ["Morning 07:00-15:00: 1 shift lead, 2 barista; daily", "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
            "Rush 11:00-14:00: 1 barista; Mon-Fri"]}
```

Result: "Scheduled 38 shifts (279 hours) for 9 of 9 people, Mon 2 Nov to Sun 8 Nov. Every shift is fully staffed. Hours per person range from 24 to 40. Everyone got their minimum hours. 3 of 3 shifts people asked to avoid were kept free." The rota comes as a picture, a PDF, a phone-friendly web page, an Excel workbook, a CSV and calendar files.

#### How to find out why a shift can't be filled

```json
{"employees": ["Ana: off Sat, Sun", "Ben", "Cara: max 16 h"], "shifts": ["Open 08:00-16:00: 2 people; daily"],
 "startDate": "2026-11-02", "days": 7}
```

Result: "Scheduled 12 shifts (96 hours) for 3 of 3 people, Mon 2 Nov to Sun 8 Nov. 2 places in 2 shifts couldn't be filled; see gaps for why." Each gap says why, such as "Ana (Sat) can't work then; Cara (16 of 16 h) would go over the weekly hours limit", and `advice` says what would fill it: more hours or more staff.

#### How to schedule nurses around the clock with 12-hour shifts

```json
{"title": "Ward 4B, November", "startDate": "2026-11-02", "days": 28, "minRestHours": 12, "maxHoursPerWeek": 48, "maxConsecutiveDays": 4,
 "employees": ["Rosa: nurse, senior; 36 h", "Uma: nurse, senior; 36 h", "Sam: nurse; 24-36 h; off Sun", "Xia: nurse; 36 h; prefers nights", "..."],
 "shifts": [{"name": "Day", "start": "07:00", "end": "19:00", "roles": {"senior": 1, "nurse": 2}, "break": 60},
            {"name": "Night", "start": "19:00", "end": "07:00", "roles": {"senior": 1, "nurse": 1}, "break": 60}]}
```

Result: a four-week rota with a senior nurse on every shift, at least 12 hours of rest between shifts and no more than 4 days in a row. The full ten-person example is in **Examples** above.

#### How to let an AI agent make a schedule

Add this Actor to the [Apify MCP server](https://mcp.apify.com) with `https://mcp.apify.com/?tools=jumblemog/shift-scheduler`. The agent writes the team and shifts as the short lines above, straight from what the manager said, and passes on the `summary`, the `gaps` and the rota's links.

### FAQ

**What rules does it follow?** Availability and days off, minimum and maximum hours, roles and skills, rest between shifts, days in a row, approved time off and fixed shifts. Preferences such as "prefers mornings" and "avoid weekends" are kept where they can be.

**Is the schedule fair?** Hours are shared in proportion to each person's limit, and weekend and night shifts are spread evenly. It can also aim for the lowest labor cost.

**What if there aren't enough people?** It still makes the best schedule it can and lists each gap with the reason and what would fill it.

**Can I import it into Google Calendar or Excel?** Yes. Each run gives an Excel workbook, a CSV, and calendar files (.ics) for Google Calendar, Outlook and Apple Calendar, one for the team and one per person.

**Does it work with Zapier, Make or n8n?** Yes. Call the Actor with its Apify app or an HTTP request and send the PDF or picture to the team.

### Input

| Field | What it's for |
|---|---|
| `employees` | The people: text lines or objects with `name`, `skills`, `minHours`, `maxHours`, `targetHours` (or `hours`), `unavailable`, `available`, `prefer`, `avoid`, `fixed`, `maxDaysPerWeek`, `maxShiftsPerWeek`, `maxConsecutiveDays`, `minRestHours`, `cost`. |
| `shifts` | The shifts: text lines or objects with `name`, `start`, `end` (or `time`), `staff`, `roles`, `days`, `skip`, `break`, `maxStaff`, `priority`, `night`. |
| `employeesCsv`, `shiftsCsv` | The same as CSV or TSV text, or a link to a CSV, Excel or JSON file or a Google Sheet. |
| `startDate`, `days`, `endDate`, `closedDates` | The period: a start date (default next Monday), up to 92 days, and days with no shifts. |
| `maxHoursPerWeek`, `minHoursPerWeek` | Weekly hours for everyone (default at most 40), unless a person's own limits say otherwise. |
| `minRestHours`, `maxConsecutiveDays`, `maxDaysPerWeek`, `maxShiftsPerWeek`, `maxShiftsPerDay` | Rest between shifts (default 11 hours), days in a row (default 6), days and shifts a week, and shifts a day (default 1). |
| `weekendDays`, `fairness`, `daysOffTogether` | Which days are the weekend, sharing hours, weekends and nights fairly, and keeping days off together. |
| `goal` | `balanced` (default), `cost` or `preferences`: what matters most once shifts are staffed and minimum hours met. |
| `timeOff`, `fixedShifts` | Approved time off (or `"soft": true` requests) and shifts someone must work. |
| `title`, `timeFormat`, `timezone` | The schedule's name, 24-hour or 12-hour times, and the time zone for calendar files. |
| `timeLimitSeconds` | The most time the solver spends improving the schedule (default 20). |
| `schedules` | Several schedules in one run. |

### Output

One dataset item per schedule (shortened here):

```json
{
    "schedule": 1,
    "title": "Corner Café",
    "status": "complete",
    "summary": "Scheduled 38 shifts (279 hours) for 9 of 9 people, Mon 2 Nov to Sun 8 Nov. Every shift is fully staffed. Hours per person range from 24 to 40. Everyone got their minimum hours. 3 of 3 shifts people asked to avoid were kept free.",
    "startDate": "2026-11-02",
    "endDate": "2026-11-08",
    "placesNeeded": 38,
    "placesFilled": 38,
    "placesShort": 0,
    "hoursScheduled": 279,
    "laborCost": 1177,
    "assignments": [
        {"date": "2026-11-02", "weekday": "Mon", "shift": "Morning", "start": "07:00", "end": "15:00", "hours": 8, "employee": "Ana", "role": "shift lead"},
        {"date": "2026-11-02", "weekday": "Mon", "shift": "Morning", "start": "07:00", "end": "15:00", "hours": 8, "employee": "Gia", "role": "barista"}
    ],
    "employees": [
        {"name": "Ana", "skills": ["barista", "shift lead"], "hours": 40, "shifts": 5, "daysWorked": 5, "weekendShifts": 0, "nightShifts": 0, "minHours": 30, "maxHours": 40, "belowMinimumHours": 0, "shiftList": ["Mon 2 Nov Morning (shift lead)", "Tue 3 Nov Morning (shift lead)", "Wed 4 Nov Morning (barista)"], "requestsNotMet": [], "calendarUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe-Ana-1.ics"}
    ],
    "coverage": [
        {"date": "2026-11-02", "weekday": "Mon", "shift": "Morning", "start": "07:00", "end": "15:00", "needed": 3, "scheduled": 3, "short": 0, "roles": {"barista": {"needed": 2, "scheduled": 2}, "shift lead": {"needed": 1, "scheduled": 1}}, "staff": ["Ana (shift lead)", "Gia (barista)", "Hal (barista)"]}
    ],
    "gaps": [],
    "advice": [],
    "requests": {"avoidAsked": 3, "avoidKept": 3, "preferredShiftsGiven": 9, "notMet": [], "belowMinimumHours": [], "fixedNotKept": []},
    "table": "| Employee | Mon 2 | Tue 3 | Wed 4 | Thu 5 | Fri 6 | Sat 7 | Sun 8 | Hours |\n|---|---|---|---|---|---|---|---|---:|\n| Ana | Morning | Morning | Morning | Morning | Morning | · | · | 40 |\n...",
    "solver": {"status": "feasible", "seconds": 4.1, "stoppedWhenNoLongerImproving": true, "ranOutOfTime": false},
    "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe.png",
    "pdfUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe.pdf",
    "htmlUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe.html",
    "xlsxUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe.xlsx",
    "csvUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe.csv",
    "teamCalendarUrl": "https://api.apify.com/v2/key-value-stores/.../records/schedule-001-Corner-Cafe-everyone.ics",
    "warnings": [],
    "notes": ["laborCost counts the 2 people with an hourly cost; 7 people have none set."]
}
```

When a place can't be filled, the schedule is `short-staffed` and `gaps` explains it:

```json
{"date": "2026-11-02", "weekday": "Mon", "shift": "Day", "start": "07:00", "end": "19:00", "role": "charge nurse", "needed": 1, "scheduled": 0, "short": 1, "reason": "Only one person can work as charge nurse: Teo (36 of 40 h) would go over the weekly hours limit."}
```

`table` is the rota as Markdown, ready for a chat reply, an email or a wiki. The run's `OUTPUT` record holds every schedule as one JSON object, so the `run-sync` API endpoint answers with them directly.

### How it works

The schedule is a constraint programming model solved by OR-Tools' CP-SAT solver in stages, so a more important goal is never traded for a lesser one: first keep the fixed shifts, then staff every shift as fully as the rules allow (more important shifts and scarcer roles first), then, without losing any of that, give people their minimum and target hours and respect their requests and preferences (and labor cost, with the cost goal), and last share hours, weekends and nights fairly, keep days off together, keep start times steady and keep cost down. The hard rules hold at every stage. A quick schedule built by hand rules gives the solver its starting point, and each stage stops once its answer has stopped improving, since proving an answer best can take far longer than finding it.

Gaps are explained after solving: for each empty place, every person who can work the role is checked against the finished schedule, and the first rule that stops them is reported.

### Checks

Every finished schedule in the tests is checked against the hard rules by separate code from the solver's: skills, availability, time off, one shift a day, overlaps, rest between shifts (across midnight too), weekly hours, days and shifts, days in a row and the places in each role. The tests also check that hours follow each person's limit, that preferences choose which shifts someone gets rather than how many, that fixed shifts are kept or explained, that the cheapest people get the hours under the cost goal, and that every gap has a reason. 29 tests pass.

### Pricing

You pay per **schedule made**, at the price shown on this page, with every file included. Schedules that can't be made are never charged. Solving past the first 30 seconds of a schedule adds a small **solver time** charge per started 10 seconds, so with the default 20-second time limit it never applies. If you set a maximum cost per run, the Actor checks before each schedule that it fits and shortens the time limit to stay within it.

### Speed

A small team's week (up to about 15 people) solves in a few seconds, and a run through the API answers in about 15 seconds, including the cloud container starting and the files being saved. Larger schedules use the time limit: 30 people over four weeks are fully staffed within the default 20 seconds, and 60 to 120 seconds give better hours and a fairer spread. The result says when the solver was still improving as its time ran out.

### Tips and limits

- **Weeks** are 7-day blocks from the start date, so start on the day your pay week starts. In a short last week, maximum hours apply in full, and minimum and target hours in proportion to its days.
- **Overnight shifts** (22:00-06:00) belong to the day they start. A shift counts as a night shift when at least half of it falls between 22:00 and 06:00, or when you mark it `night`.
- **Availability versus requests**: `unavailable`, `available` and time off are rules; `avoid` and `prefer` are requests the schedule keeps when it can. Time off with `"soft": true` is a request.
- **Roles**: names are matched without case or a plural s, so "Baristas" matches "barista". A shift's headcount beyond its named roles can go to anyone.
- **Costs**: people without a cost count at the team's average cost when comparing schedules, so leaving a wage out doesn't make someone look free; `laborCost` adds up only the costs given.
- **Calendar files** use local times as written unless you set a time zone such as Europe/London.
- **Limits**: 500 employees, 60 shifts and 92 days per schedule, 4,000 shift occurrences, 50 schedules per run, and up to 600 seconds of solving per schedule. A run stops once schedules that can't be made have taken 3 minutes.
- **Links**: CSV, TSV, JSON and Excel files, GitHub file links and Google Sheets links (shared as "Anyone with the link" or published to the web). Every site's robots.txt is checked first.

### For AI agents and developers

This Actor works with the [Apify MCP server](https://mcp.apify.com), so AI agents can call it as a tool: send the team and shifts as text lines and read `summary`, `table`, `gaps` and `advice` from the result. A direct API call that waits for the answer:

```bash
curl -X POST "https://api.apify.com/v2/acts/jumblemog~shift-scheduler/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"employees": ["Ana: cashier; max 30 h", "Ben: cashier; off Sun", "Cara: cashier, supervisor"], "shifts": ["Open 08:00-14:00: 1 supervisor, 1 cashier; daily", "Close 14:00-20:00: 2; daily"]}'
```

`run-sync` instead returns the run's `OUTPUT`: the status, how many schedules were made and every schedule as JSON.

In Python, with the `apify-client` package:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("jumblemog/shift-scheduler").call(run_input={
    "employees": [
        {"name": "Ana", "skills": ["cashier"], "maxHours": 30},
        {"name": "Ben", "skills": ["cashier"], "unavailable": "Sun"},
        {"name": "Cara", "skills": ["cashier", "supervisor"], "prefer": "mornings"},
    ],
    "shifts": [
        {"name": "Open", "start": "08:00", "end": "14:00", "roles": {"supervisor": 1, "cashier": 1}},
        {"name": "Close", "start": "14:00", "end": "20:00", "staff": 2},
    ],
    "startDate": "2026-11-02",
})
for schedule in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(schedule["summary"])
    for shift in schedule["assignments"]:
        print(shift["date"], shift["shift"], shift["employee"], shift["role"])
```

### Credits

- Scheduling: [Google OR-Tools](https://developers.google.com/optimization) CP-SAT (Apache License 2.0).
- Files: [openpyxl](https://openpyxl.readthedocs.io) (MIT) for Excel, and [vl-convert](https://github.com/vega/vl-convert) (BSD-3-Clause) for the PNG and PDF, with the Inter font (SIL Open Font License).
- Time zones: [tzdata](https://github.com/python/tzdata) (Apache License 2.0).

# Actor input Schema

## `employees` (type: `array`):

The people to schedule, one per entry, as text or an object. Text: "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings; $18/h": the name, the roles they can work, then any of: weekly hours as a target (36 h), a range (20-32 h) or a limit (max 30 h); times they can't work (off Sun, no Tue after 17:00); the only times they can (available weekdays); times they prefer or want to avoid; fixed shifts (always Mon Morning); max 5 days; max 4 shifts; their hourly cost. Object: {"name": "Ana", "skills": \["shift lead", "barista"], "minHours": 30, "maxHours": 40, "targetHours": 36, "unavailable": "Sun, 2026-11-05", "available": "weekdays", "prefer": "mornings", "avoid": "weekends", "fixed": "Mon Morning", "maxDaysPerWeek": 5, "maxShiftsPerWeek": 5, "maxConsecutiveDays": 5, "minRestHours": 12, "cost": 18}. Times can be weekdays (Mon, Mon-Fri, weekends), dates (2026-11-05, Nov 5-9), shift names (Morning), parts of the day (mornings, evenings, nights) or clock times (after 17:00, before 10am), alone or together, such as "Tue after 17:00" or "Sat Evening". Up to 500 people.

## `shifts` (type: `array`):

The shifts to fill, as text or objects. Text: "Morning 07:00-15:00: 1 shift lead, 2 barista; daily": the name and times, then the people needed, in total (3) or by role, and the days it runs (Mon-Fri, weekends, Sat, 2026-12-24), with optional parts such as "30 min break" (unpaid), "max 4" (extra people allowed) or "priority high". Object: {"name": "Morning", "start": "07:00", "end": "15:00", "roles": {"shift lead": 1, "barista": 2}, "days": "Mon-Fri", "break": 30, "maxStaff": 4, "priority": "high"}. The headcount can change by day: "staff": {"Mon-Thu": 2, "Fri-Sat": 4, "default": 3}, or seven numbers, Monday first. Overnight shifts such as 22:00-06:00 work. Places beyond the named roles can go to anyone. Up to 60 shifts.

## `employeesCsv` (type: `string`):

Employees as CSV or TSV text with a header row, such as name,skills,minHours,maxHours,unavailable,prefer,cost (the same fields as above), or a link to a CSV, Excel or JSON file, or a Google Sheet shared as "Anyone with the link". Added to Employees.

## `shiftsCsv` (type: `string`):

Shifts as CSV text with a header row, such as name,start,end,roles,days,break, or a link to a file or Google Sheet. Added to Shifts.

## `startDate` (type: `string`):

The schedule's first day, such as 2026-11-02, or today, tomorrow or a weekday name. Empty: next Monday.

## `days` (type: `integer`):

How many days to schedule, up to 92 (13 weeks). Weekly limits count in 7-day blocks from the first day.

## `endDate` (type: `string`):

The last day to schedule, such as 2026-11-29, instead of Number of days.

## `closedDates` (type: `array`):

Days with no shifts at all, such as 2026-12-25 or Sun.

## `maxHoursPerWeek` (type: `number`):

The most hours anyone works in a week. 0: no limit.

## `minHoursPerWeek` (type: `number`):

Hours everyone should get each week. The schedule gives them whenever the shifts and rules allow, before fairness and preferences, and lists anyone who falls short.

## `minRestHours` (type: `number`):

The least time between the end of one shift and the start of the next, such as 11 hours under EU working time rules, so nobody closes late and opens early.

## `maxConsecutiveDays` (type: `integer`):

The most days in a row anyone works. 0: no limit.

## `maxDaysPerWeek` (type: `integer`):

The most days anyone works in a week, such as 5.

## `maxShiftsPerWeek` (type: `integer`):

The most shifts anyone works in a week.

## `maxShiftsPerDay` (type: `integer`):

How many shifts one person may work on the same day. Above 1, split shifts are allowed when they don't overlap.

## `weekendDays` (type: `array`):

The days that count as the weekend, for sharing weekend shifts fairly and for "weekends" in availability.

## `fairness` (type: `boolean`):

Spread hours in proportion to each person's limit, and weekend and night shifts evenly.

## `daysOffTogether` (type: `boolean`):

Avoid single days off between work days, such as working Monday and Wednesday but not Tuesday.

## `goal` (type: `string`):

What matters most once every shift is staffed as fully as the rules allow and everyone's minimum hours are met.

## `timeOff` (type: `array`):

Approved time off, one entry per person and period: \[{"employee": "Ana", "from": "2026-11-05", "to": "2026-11-07"}, "Ben: Fri", {"employee": "Cara", "dates": \["2026-11-03"], "soft": true}]. Time off is a rule; with "soft": true it's a request the schedule keeps when it can.

## `fixedShifts` (type: `array`):

Shifts someone must work: \["Dev: Mon Morning as shift lead", {"employee": "Ana", "shift": "Sat Evening"}]. Kept unless a rule makes that impossible, in which case the result says which rule.

## `title` (type: `string`):

The schedule's name on the rota and in the results, such as Corner Café, week 45.

## `timeFormat` (type: `string`):

How times are written on the rota and in the files.

## `timezone` (type: `string`):

Such as America/New_York or Europe/London. Empty: the calendar files use local times as written.

## `timeLimitSeconds` (type: `integer`):

The most time the solver spends improving the schedule; it stops sooner once the schedule stops improving. Small teams take a few seconds; for 30 or more people over several weeks, 60 to 120 seconds gives better hours. The first 30 seconds come with the schedule's price.

## `schedules` (type: `array`):

Many schedules in one run, each an object with its own employees, shifts or settings: \[{"title": "Downtown", "employees": \[...], "shifts": \[...]}, {"title": "Airport", "employeesCsv": "https://...", "days": 14}]. Settings outside schedules apply to every schedule, and a schedule with its own employees or shifts doesn't use the shared ones. Up to 50.

## Actor input object example

```json
{
  "employees": [
    "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings",
    "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
    "Cara: barista; 20-32 h; avoid weekends",
    "Dev: shift lead, barista; 32-40 h",
    "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h",
    "Finn: shift lead, barista; max 5 days; $19/h",
    "Gia: barista; 16-24 h",
    "Hal: barista; max 32 h; off Sat, Sun",
    "Ivy: shift lead, barista; 24-32 h; prefers evenings"
  ],
  "shifts": [
    "Morning 07:00-15:00: 1 shift lead, 2 barista; daily",
    "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
    "Rush 11:00-14:00: 1 barista; Mon-Fri"
  ],
  "days": 7,
  "maxHoursPerWeek": 40,
  "minRestHours": 11,
  "maxConsecutiveDays": 6,
  "maxShiftsPerDay": 1,
  "weekendDays": [
    "Sat",
    "Sun"
  ],
  "fairness": true,
  "daysOffTogether": true,
  "goal": "balanced",
  "title": "Corner Café",
  "timeFormat": "24h",
  "timeLimitSeconds": 20
}
```

# Actor output Schema

## `results` (type: `string`):

One row per schedule: the rota image, the summary, the staffing and the files.

## `assignments` (type: `string`):

Every assignment: who works which shift, when, in which role.

## `people` (type: `string`):

Each person's hours, shifts, requests not met and calendar file.

## `gaps` (type: `string`):

Each place left empty and why nobody could take it.

## `output` (type: `string`):

The OUTPUT record: every schedule in one JSON object.

## `files` (type: `string`):

The rota images, PDFs, web pages, Excel, CSV, JSON and calendar files in the run's key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "employees": [
        "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings",
        "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
        "Cara: barista; 20-32 h; avoid weekends",
        "Dev: shift lead, barista; 32-40 h",
        "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h",
        "Finn: shift lead, barista; max 5 days; $19/h",
        "Gia: barista; 16-24 h",
        "Hal: barista; max 32 h; off Sat, Sun",
        "Ivy: shift lead, barista; 24-32 h; prefers evenings"
    ],
    "shifts": [
        "Morning 07:00-15:00: 1 shift lead, 2 barista; daily",
        "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
        "Rush 11:00-14:00: 1 barista; Mon-Fri"
    ],
    "title": "Corner Café"
};

// Run the Actor and wait for it to finish
const run = await client.actor("jumblemog/shift-scheduler").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "employees": [
        "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings",
        "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
        "Cara: barista; 20-32 h; avoid weekends",
        "Dev: shift lead, barista; 32-40 h",
        "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h",
        "Finn: shift lead, barista; max 5 days; $19/h",
        "Gia: barista; 16-24 h",
        "Hal: barista; max 32 h; off Sat, Sun",
        "Ivy: shift lead, barista; 24-32 h; prefers evenings",
    ],
    "shifts": [
        "Morning 07:00-15:00: 1 shift lead, 2 barista; daily",
        "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
        "Rush 11:00-14:00: 1 barista; Mon-Fri",
    ],
    "title": "Corner Café",
}

# Run the Actor and wait for it to finish
run = client.actor("jumblemog/shift-scheduler").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "employees": [
    "Ana: shift lead, barista; 30-40 h; off Sun; prefers mornings",
    "Ben: barista; max 24 h; off Mon; no Tue after 17:00",
    "Cara: barista; 20-32 h; avoid weekends",
    "Dev: shift lead, barista; 32-40 h",
    "Eli: barista; available Mon-Fri after 15:00, weekends; $16/h",
    "Finn: shift lead, barista; max 5 days; $19/h",
    "Gia: barista; 16-24 h",
    "Hal: barista; max 32 h; off Sat, Sun",
    "Ivy: shift lead, barista; 24-32 h; prefers evenings"
  ],
  "shifts": [
    "Morning 07:00-15:00: 1 shift lead, 2 barista; daily",
    "Evening 15:00-23:00: 1 shift lead, 1 barista; Mon-Sat",
    "Rush 11:00-14:00: 1 barista; Mon-Fri"
  ],
  "title": "Corner Café"
}' |
apify call jumblemog/shift-scheduler --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jumblemog/shift-scheduler"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QmzO9UHSVfIEh27N6/builds/Lw19ZUwYayw3PyoSM/openapi.json
