# Build a roster

A roster is a versioned plan for one date range. You can fill it manually, ask Fivexer to solve it, or pin the decisions you care about and solve the rest.

## Create the planning period

```
tsconst roster = await fivexer.rosters.create({
  name: 'September 2026',
  periodStart: '2026-09-01',
  periodEnd: '2026-09-30',
  notify: true,
});
```

Omit `templateIds` to include every active shift template. `notify: true` tells workers that planning has opened so they can submit time-off requests; use `false` for imports and bulk scripts where a notification would be noise.

`rosters.get(id)` returns the expanded dated instances. Each instance has an ID such as `day_ward_a@2026-09-03`, its actual duration, working minutes after unpaid breaks, qualification requirements, site, and current assignees.

## Assign manually

Saving assignments replaces the complete draft set and creates a new version.

```
tsconst detail = await fivexer.rosters.get(roster.id);

const saved = await fivexer.rosters.setAssignments(roster.id, {
  baseVersion: detail.version,
  note: 'Supervisor draft',
  assignments: [
    { employeeId: 'worker_anna', shiftInstanceId: 'day_ward_a@2026-09-03' },
    { employeeId: 'worker_ben', shiftInstanceId: 'night_ward_a@2026-09-03' },
  ],
});
```

Always send `baseVersion` from the roster you read. If another editor saved first, the API returns `409 roster_version_conflict` instead of overwriting their work. Re-read, reconcile, and save again.

The response includes a fresh lint result. A save may preserve a non-compliant draft; publishing is where you deliberately decide whether the plan is ready.

## Ask who can take a shift

```
tsconst candidates = await fivexer.rosters.candidates(roster.id, {
  shiftInstanceId: 'night_ward_a@2026-09-03',
});
```

Candidates include eligible and blocked people. Each row carries rank, rationale, marginal cost, fairness debt, travel context, and blockers. Use `rosters.explain` when you need every rule verdict for one person and one shift.

## Solve automatically

```
tsconst started = await fivexer.rosters.solve(roster.id, {
  timeBudgetMs: 10_000,
});

let job = (await fivexer.rosters.solveStatus(roster.id)).job;
while (job?.status === 'queued' || job?.status === 'running') {
  await new Promise((resolve) => setTimeout(resolve, 500));
  job = (await fivexer.rosters.solveStatus(roster.id)).job;
}
```

Solving runs asynchronously and creates a result version. It does not publish. The time budget controls how long improvement may continue, not whether constraints apply: hard requirements cannot be traded for a cheaper or fairer plan.

If no complete plan is possible, call `rosters.diagnose(id)`. It reports structural causes such as zero eligible workers, missing qualifications, or capacity shortfalls. A partial answer is evidence about the gap, not a successful roster.

## Compare versions

`rosters.versions(id)` lists manual, solved, repaired, and published versions with their assignment count and lint summary. That makes experimentation reversible: compare a manager’s draft with a solver result, then publish the version you actually approve.

Before publishing, understand the [working-time rules](/docs/rostering/rules/) and run [validation and preflight](/docs/rostering/publish/).
