Skip to content

Scheduling

The Scheduling client answers a question most schedulers can’t: not “give me one valid rota” but “across every valid rota, which cells are forced, which are ruled out, and which are still open?”.

It does this by treating staff rostering — and any other “assign capacity-limited resources to demand” problem — as a capacitated bipartite b-matching with role stratification. Internally it’s a Dinic max-flow plus a Dulmage–Mendelsohn residual decomposition; on the wire, the engine returns a single status per (agent, day, shift) cell.

The same machinery handles two questions:

EndpointQuestionTrichotomy describes…
feasibility()”Is there a valid rota? Which cells are forced by the rules?“every rota that satisfies the hard rules
optimize()”Among the optimal rotas, which cells are forced?“every optimal rota under your soft preferences

Both endpoints are stateless — you pass the entire problem inline on each call.

Three states for every cell

Whatever endpoint you call, every (agent, day, shift) cell in the response has exactly one status:

  • confirmed_true — the agent must work this slot. Every rota under consideration assigns them here.
  • confirmed_false — the agent cannot work this slot. No rota under consideration assigns them here.
  • free — the agent could work this slot. Some rotas assign them; others don’t. The choice is open.

This trichotomy is the answer shape — not a single rota the engine prefers, but a complete map of where the rules (or rules + preferences) leave you no choice and where they leave you free to decide.

Feasibility

Use client.scheduling.feasibility() when you only have hard rules — no preferences, no scoring. The trichotomy describes the space of rotas that satisfy the rules.

import { ReasoningLayerClient } from '@kortexya/reasoninglayer';
const client = new ReasoningLayerClient({
baseUrl: 'https://platform.ovh.reasoninglayer.ai',
tenantId: '...',
auth: { mode: 'cookie' },
});
const report = await client.scheduling.feasibility({
agents: [
{ id: 'aisha', roles: ['intensive_care', 'emergency'], maxAssignments: 4 },
{ id: 'anna', roles: ['intensive_care'], maxAssignments: 4 },
{ id: 'bob', roles: ['general'], maxAssignments: 4 },
],
days: 7,
shiftsPerDay: 3,
demands: [
{ day: 0, shift: 0, total: 3, roleMinimums: { intensive_care: 1 } },
// night shift also needs an emergency-trained nurse
{ day: 0, shift: 2, total: 3, roleMinimums: { intensive_care: 1, emergency: 1 } },
// ... one entry per (day, shift) you care about
],
pins: [
// optional — cells already committed by the manager
{ agentId: 'aisha', day: 4, shift: 0 },
],
});
if (report.status === 'feasible') {
for (const cell of report.assignments) {
console.log(cell.agentId, cell.day, cell.shift, '', cell.status);
}
}

status === 'infeasible' is a valid response, not an error. It means the rules cannot be satisfied with the agents and demands you provided (e.g. role minima exceed total agent capacity). The HTTP call is 200 OK; only malformed input (duplicate IDs, out-of-range references, ambiguous pin role) raises.

Request shape

FieldTypeDescription
agentsAgentSpec[]Who can work, with their roles and max-shifts cap.
daysnumberHow many days in the roster.
shiftsPerDaynumberHow many shifts per day (typically 2 or 3).
demandsShiftDemand[]Per-(day, shift) total bodies + role minimums.
pinsPin[]Optional — hard commitments.

Each agent gets an opaque caller-chosen id (employee number, UUID, display name — anything). The engine round-trips it unchanged in the response, so agents, pins, preferences, and assignments all reference the same identifier space.

Pinning cells

Pins are non-negotiable assignments. Once a cell is pinned, the trichotomy is computed given that commitment — every other cell’s status reflects what’s possible after the pin.

const pins = [
{ agentId: 'aisha', day: 4, shift: 0 }, // unroled — auto-routed
{ agentId: 'aisha', day: 5, shift: 2, role: 'emergency' }, // covers an emergency slot
{ agentId: 'aisha', day: 6, shift: 0, role: 'any' }, // unroled pool, explicit
];

If an agent has multiple roles that all match role-minimums at the slot (e.g. an ICU + emergency nurse on a night that needs both), the engine returns HTTP 400 with AmbiguousPinRole unless you specify role explicitly. Pass the role you want the pin to cover, or 'any' to route through the unroled pool (no role minimum is decremented).

Personal availability

Per-agent constraints — days off, shift-only restrictions — are encoded directly on the agent:

{
id: 'priya',
roles: ['emergency'],
maxAssignments: 3,
unavailableDays: [2, 3], // off Wednesday and Thursday
restrictedToShift: 0, // mornings only
}

These are honoured by the structural network layout — you don’t need to add separate constraints. The flow engine simply doesn’t route flow through impossible cells.

Optimize

client.scheduling.optimize() adds soft preferences on top of hard rules. The trichotomy in the response describes every cell that’s the same across every optimal rota.

const report = await client.scheduling.optimize({
agents,
days: 7,
shiftsPerDay: 3,
demands,
pins,
preferences: [
{ agentId: 'anna', day: 2, shift: 0, score: 5 }, // Anna likes Wed mornings
// Bob would rather skip Fri — one entry per shift, since preferences
// are per-(day, shift) cell on the wire
{ agentId: 'bob', day: 4, shift: 0, score: -3 },
{ agentId: 'bob', day: 4, shift: 1, score: -3 },
{ agentId: 'bob', day: 4, shift: 2, score: -3 },
],
});
console.log(report.status); // 'feasible' | 'infeasible'
console.log(report.totalScore); // sum of preference scores at the optimum
for (const cell of report.assignments) {
console.log(cell.agentId, cell.day, cell.shift, '', cell.status);
}

Trichotomy under preferences

The output statuses are the same three words but with a stricter meaning:

  • confirmed_true — appears in every optimal rota.
  • confirmed_false — appears in no optimal rota.
  • free — varies across the optima; the optimizer is indifferent between equally-good choices.

This is strictly stronger than the feasibility-only trichotomy: a cell that’s free under feasibility may become confirmed_true under optimize, because preferences narrow the space of “valid” rotas to “best” rotas.

Preference semantics

ScoreEffect
PositivePulls the agent toward the cell (preference).
NegativePushes the agent away from the cell (penalty).
ZeroNo effect — same as omitting the preference.

Multiple preferences on the same cell accumulate. If two entries give Anna +5 for Wednesday morning, the cell’s effective score is +10.

Preferences targeting structurally-fixed cells are silently ignored — they have no decision to influence. That includes:

  • pinned cells (the optimizer has no choice),
  • cells the agent is ineligible for (day-off, shift-only),
  • agents not in the agents list.

totalScore

totalScore is the sum of preference scores over the assigned cells in the optimum:

  • 0 when no preferences are supplied or the problem is infeasible.
  • Positive when the optimum picks up net-positive preferences.
  • Negative when the optimum has to honour penalties to remain feasible.

It’s a single number — read it as “how well the optimum honoured what you asked for,” not as a probability or a rank.

When to use feasibility() vs optimize()

  • No preferences? Always use feasibility(). It’s faster and produces the same answer that optimize() would with an empty preferences list.
  • Preferences? Use optimize(). The trichotomy you get back is strictly more informative.

Beyond rostering

Anything that fits the shape “assign capacity-limited resources to demand under role/skill constraints” maps onto these endpoints:

  • Call-centre agents to time slots and skill queues.
  • Vehicles to delivery routes, with capacity limits per vehicle.
  • On-call blocks across sub-specialties in a clinical department.
  • Patients to clinical-trial sites under eligibility constraints.

The wire vocabulary — agents, demands, pins, preferences, assignments — stays the same; the meaning of each maps onto your domain.

Next steps

  • See the Scheduling namespace API reference for full type-level documentation of every request and response field.
  • Try the interactive playground demo at /demo/hospital-scheduling — every click is a feasibility or optimize call against the engine.
  • Read the engineering blog post for the product-level perspective on what the trichotomy unlocks.