Fivexer

People & terms

Roster workers are the same workers used by task routing, with additional planning facts. Keep identity and qualifications on the worker; keep contract, pay, and site-planning facts in roster terms.

Qualifications

Shift eligibility is checked against a worker’s date-valid tags and qualifications. Use hard requirements for licences, clearances, languages, or role qualifications that must never be waived by scoring.

A qualification may carry a validity window. Both ends are inclusive ISO days and both are optional, so validUntil is the last day the certificate counts — a shift on that day is fine and the next one is not.

ts
await fivexer.workers.upsert({
  id: 'worker_nora',
  skills: [{ skillId: 'skl_sia_licence', level: 3, validUntil: '2027-03-31' }],
});

Important semantics:

  • The window is checked against each shift’s own date, never against today. A licence lapsing mid-period bars the shifts after it and leaves the earlier ones standing.
  • A refusal says which of the two it was — never held, or held and expired on a stated date. They lead to different fixes: one is a training booking, the other a renewal somebody already owes.
  • validFrom/validUntil omitted means unbounded in that direction. A qualification nobody dated behaves exactly as it did before windows existed.
  • The window gates the roster, not task routing. Routing weights are projected when the worker record is written and the matcher has no date model, so an expired qualification still receives tasks until the record is saved again. Removing the tag is the way to stop that today.
  • GET /skills/expiring lists what has lapsed or lapses within withinDays (default 30). Pass asOf as your own calendar day so the window is not cut on the server’s UTC clock.

Requirements can apply to:

  • every person on a post or shift (requiredTags);
  • the composition of the whole shift (tagRequirements);
  • a maximum number in one category (tagMaximums).

Candidate explanations return failed requirements explicitly, so “not eligible” never has to be a black box.

Working terms

ts
await fivexer.roster.setTerms('worker_anna', {
  weeklyMinutes: 2_400,
  maxDaysInPeriod: 5,
  rateCents: 2_250,
  overtimeConsent: true,
  homeSiteId: 'site_north',
  siteIds: ['site_north', 'site_south'],
});

Important semantics:

  • weeklyMinutes: null means the contractual week is not stated.
  • fte is part-time as a fraction of the full-time week — 0.5 is half-time. It resolves against contract.fullTimeWeeklyMinutes in the rules (or the overtime rule's ordinary week), so a change to the full-time week moves every part-timer with it. A stated weeklyMinutes wins over it. The API refuses a fraction until the rules say what full time is.
  • maxDaysInPeriod: null means no personal period cap is stated.
  • rateCents: null inherits the workspace default rate when one exists.
  • overtimeConsent is tri-state: true, false, or null for never recorded. When consent is required, null is treated like no consent.
  • homeSiteId is a preference, not permission.
  • siteIds: [] means the person may work at every site. A non-empty list is a hard allow-list.

Pay configuration

Pay settings price a plan; they do not invent legal rules.

ts
await fivexer.roster.setPay({
  defaultRateCents: 2_000,
  bands: [
    { id: 'night', label: 'Night', startTime: '22:00', endTime: '06:00', multiplier: 1.25 },
  ],
  sundayMultiplier: 1.5,
  holidayMultiplier: 2,
  // Extra dates on top of the calendar the workspace's holiday location supplies.
  holidays: ['2026-12-25'],
  overtimeMultiplier: 1.5,
  stacking: 'max',
});

stacking: 'max' uses the largest applicable premium for a minute; add combines premiums. Personal rates override the workspace default. If no rate resolves for a worker, their planned time remains visible but no invented cost is returned.

Use roster.plannedHours({ from, to }) to read shift counts, ordinary time, premium-band minutes, overtime, and cost by worker and day. The response says whether it came from a published or draft plan.

Time off

Only approved leave constrains the solver. Pending requests appear in publishing preflight because publishing over one may be a legitimate manager decision, but it should never happen invisibly.

ts
const request = await fivexer.timeOff.create({
  workerId: 'worker_anna',
  startDate: '2026-09-14',
  endDate: '2026-09-18',
  kind: 'vacation',
});

const impact = await fivexer.timeOff.impact(request.id);
await fivexer.timeOff.approve(request.id);

Check impact before deciding: it identifies affected rosters and conflicting shifts. For sickness or a no-show after publication, use the cover workflow instead of rewriting historical leave.

Was this page helpful?