Skip to main content
All posts
June 9, 20267 min readReasoning Layer team

Interactive scheduling with ReasoningLayer, a nurse scheduling use case

schedulinghealthcareuse-case

It's Tuesday morning. Aisha asks for Friday off.

Her manager pulls up the rota and stares. Will Friday still work without her? Does anyone else have ICU clearance and an open slot? If Aisha goes, who covers the night shift two days later — the one she was the only emergency-trained nurse left for? And if Friday works, what does that force everywhere else?

Today, the answer takes ten minutes of mental arithmetic, an awkward "let me get back to you," or a coffee-break wait while the scheduler regenerates the whole roster and the manager scans for what changed.

It should take a fraction of a second. And it shouldn't give back one rota — it should tell the manager which cells are now forced, which are now impossible, and which are still up to them.

That's what we built.

See it before you read about it#

Each click is a real-time question to the engine; each recolour is the answer.

Every recolour is the engine's complete answer — not a sample, not a guess. No batch job. No "regenerating, please wait." No black-box "the AI said so." The manager moves, the rota responds, and what's left on the grid is exactly what's still achievable.

Don't take our word for the recolouring — drive it yourself. ▶ Open the live grid and start pinning nurses: every click runs a real solve and repaints the board in front of you.

Three states, for every cell#

Every (nurse, day, shift) cell on the grid sits in exactly one of three states:

  • 🟢 Confirmed — this nurse must work this slot. Every rota that satisfies your rules has them here. Removing them would break the whole schedule.
  • 🔴 Impossible — this nurse cannot work this slot. No rota that satisfies your rules places them here, given everything else that's already pinned.
  • 🟡 Free — this nurse could work this slot. Some valid rotas place them here, some don't. The choice is still open.

That's the full picture: not one rota out of millions of possibilities, but a clean three-colour map showing where the manager has freedom, where they have no choice, and where they have no chance.

When the manager clicks Aisha for Friday morning, the whole grid recomputes. Cells that were yellow on day 12 might turn red because Aisha was the only one who could have covered emergency that night. Cells that were yellow on Tuesday afternoon might turn green because pinning Aisha now forces Bob to fill a slot she was previously eligible for.

Every click is an instant snapshot of "what's still possible, given this." No commitment. No need to generate a full rota first and then unpick the bits you don't like. The schedule emerges from the manager's choices, with the engine confirming each step is sound.

When the manager hovers over a green cell that says "must work," they know it's not the engine's opinion — it's a fact about every rota that satisfies their rules.

The shape lands on the wire too. Each cell in the response is a single, unambiguous record:

{
  "agentId": "aisha",
  "day": 0,
  "shift": 0,
  "status": "confirmed_true",
  "roles": ["intensive_care"]
}

status is exactly one of confirmed_true, confirmed_false, or free — the three colours, named.

This three-colour answer is the shape of Reasoning Layer, the engine underneath the demo. Ask it a structured question and it doesn't return one answer for you to second-guess; it returns the full space of answers that hold up against your rules, sorted into what's settled, what's ruled out, and what's still open. The schedule grid is just one face of that.

And when you have preferences#

Hard rules — "every shift needs an ICU nurse," "nobody works more than four nights" — define what's possible. Soft preferences — "Anna prefers mornings," "we'd rather not put anyone on Friday if we can help it" — define what's good.

The grid handles both. Turn preferences on and the engine no longer just tells you which rotas satisfy the rules; it tells you which rotas score the highest, and which cells are filled the same way in every highest-scoring rota. Anna's name still locks solid green on Wednesday morning even when ten alternative rotas were technically valid, because she's in every one of the best ones.

From the integrator's side, the call is the same shape regardless of whether you have preferences — you just hand the engine the extra list. (Not wiring this up yourself? Skip the code block — the one line to keep is the takeaway right after it.)

// Who can work, what they're trained on, max shifts in the roster
const agents = [
  { id: 'aisha', roles: ['intensive_care', 'emergency'], maxAssignments: 4 },
  { id: 'anna',  roles: ['intensive_care'],              maxAssignments: 4 },
  { id: 'bob',   roles: ['general'],                     maxAssignments: 4 },
];
 
// Per-(day, shift) demand: total bodies + role-specific minimums
const 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 slot you care about
];
 
// Hard commitments — cells the manager has already locked in
const pins = [
  { agentId: 'aisha', day: 4, shift: 0 }, // Aisha pinned to Friday morning
];
 
// Soft preferences — bias the optimum without changing what's possible
const 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 },
];
 
const report = await client.scheduling.optimize({
  agents,
  days: 7,
  shiftsPerDay: 3,
  demands,
  pins,
  preferences,
});

You get the optimum without ever having to pick an optimum. You see what's forced by your preferences the same way you see what's forced by your rules — on the same grid, in the same instant.

The full request and response shapes, the role-disambiguation rules for pins, and the feasibility() vs optimize() trade-offs are documented in the Scheduling guide of the TypeScript SDK reference.

Two managers, one rota#

Pull up the schedule, click a few nurses, copy the URL, send it to a colleague. They open the link and see the same grid — same pins, same colours, same numbers — and pick up where you left off.

That works because the rota isn't a private document on the manager's machine; it's a live record both of you are looking at, shared by URL the way you'd share a Google Doc. Pin a cell on your screen, switch tabs, switch back: the engine has already absorbed your colleague's last edit, and the grid in front of you reflects it.

The conversational, "let's try this" feel of the demo isn't because we wrapped a slick front end around a static spreadsheet. It's because the rota is one shared object — nurses, requests, pins, preferences, derived assignments — that both of you, and the engine, are looking at and changing in real time.

What ReasoningLayer does differently#

Most scheduling tools fall into one of three buckets. Each is well-suited to what it was built for; ReasoningLayer is built for a different question — what's still possible? — and answers it on every click.

Spreadsheets and manual rotas — full human control, zero propagation. The manager has to keep every rule in their head. Mistakes compound silently until the schedule goes out and someone discovers the night shift has no ICU coverage.

"Generate then edit" schedulers — the dominant pattern. Press a button, wait, get a rota. Want to swap Aisha to Tuesday? Edit the cell, hope nothing broke, run validation, fix what broke, repeat. The tool never tells you up front which other cells your edit just made impossible — you find out when validation flags them after the fact.

Heavy-duty optimisers — they can produce a schedule under any number of rules, eventually. They are not built to answer "across every valid schedule, which cells are forced?" — and when you ask, you wait. Long enough that "click and watch the grid update" stops being possible.

What we built is something different in kind: a tool whose job is not to draft the schedule for you, but to keep the entire space of valid schedules visible, in real time, while you make decisions one cell at a time. The schedule is the by-product; the visibility is the product.

We measured it against the industry standard#

"Recolour on every click" only works if the answer comes back fast. So we benchmarked the engine against Google OR-Tools (CP-SAT) — the industry-standard, open-source constraint solver — on the same nurse-rostering problem. Every engine solved a byte-identical instance, and we cross-checked the three-colour verdicts cell-for-cell so the comparison can't be gamed.

Finding one valid rota is milliseconds for everyone — not the interesting axis. The hard question is the three-colour map: across all valid rotas, which cells are forced, which are impossible, which are free. Here is the wall-clock to compute that full map:

InstanceCellsReasoning LayerOR-Tools (textbook)OR-Tools (optimised)
small2940.29 s3.6 s0.60 s
medium7200.78 s21.2 s10.9 s
regional (multi-shift)1,7646.4 s>180 s (22 cells unsolved)27.0 s
regional (zero-slack)1,76421.8 s (1 cell left)>180 s (1,763 unsolved)>180 s (1,664 unsolved)

Two comparisons matter here.

The cleanest holds the strategy fixed and swaps only the engine underneath. The textbook way to build the three-colour map is to ask, for every cell, the same two yes/no questions — is there a valid rota with this cell filled? is there one with it empty? — and read the colour off the answers. Run those identical questions on both engines, and Reasoning Layer answers them 12.5× faster on the small roster and 27× faster on the medium one. Nothing differs but the engine doing the answering.

The second bar is harder on us. OR-Tools' optimised column doesn't ask the questions that simple way at all — it uses a smarter, problem-specific approach: enumerate feasible rotas to settle the easy free cells cheaply, then probe only the hard remainder. Even against that, our straightforward ask-twice-per-cell probe is 2× to 14× faster across the tractable instances.

On the hardest case — a real zero-slack regional hospital, 1,764 cells — Reasoning Layer certified 1,763 of 1,764 cells in 22 seconds. OR-Tools, given a full three minutes, certified at most 100.

One honest caveat: computing the complete three-colour map is NP-hard in the worst case — for every engine, ours included; a single forced cell can demand an exponentially long proof, which is why even Reasoning Layer left one cell uncertified on the zero-slack instance. The benchmark is reproducible, the problem instances are committed, and the engine's answers are checked against OR-Tools' on every cell.

What this means for your team#

For the manager, the change is concrete. The "let me get back to you" disappears. So does the second-guessing — "did I just break Wednesday?" — because the grid told them the moment they clicked. Mistakes that used to surface a week into the rota now surface at the click. Negotiations that used to require the scheduler's spreadsheet ("hold on, let me see if that works") happen live, with the staff member in the room, with confidence on both sides.

For the team, the rota is no longer a black-box pronouncement they have to trust. It's a running conversation between people and a tool that's transparent about why a request can or can't be honoured. That changes the texture of the work week.

For the people running the operation, the cycle time for "what if?" questions collapses from hours to seconds. Senior staff stop being the bottleneck for every minor change. The rota becomes something the unit can iterate on through the week, not a fragile artifact that has to be regenerated end-to-end whenever anything moves.

And because the engine is exact, what shows on the grid is what will hold. No surprise infeasibilities at hand-off. No phantom schedules that look fine until the second-pass validator runs.

Built around your rules#

Every hospital, every unit, every clinic has its own version of "the rules." Mandatory ratios. Skill mixes. Rest requirements. Local agreements. Seasonal demand swings. Roles you have that nobody outside your walls would recognise. We've designed Reasoning Layer to accept those rules at the level you actually think about them, not at the level a database schema thinks about them.

If your scheduling problem is shaped a little differently from the next hospital's — and it always is — we want to hear about it. The fastest path to a tool that fits your team is to start a conversation with us about what makes your rota hard. We'll bring the engine; you bring the rules; together we'll show you the rota you've been trying to get to all along.

Beyond the rota#

Once you've watched the grid recolour for nurses, you start spotting the same shape elsewhere. A call centre matching agents to time slots and skill queues. A logistics team assigning vehicles to routes with capacity caps. A radiology department balancing on-call blocks across sub-specialties. A clinical-trial unit placing patients into sites with eligibility constraints. Underneath, they're the same problem: capacity meeting demand, with rules.

Reasoning Layer is the engine for any of those — not just rotas. The schedule is the demo we picked because it makes the value visible in a glance, but the same machinery answers the same kind of question wherever your team is being asked it.

Try it#

You don't have to wait for us. ▶ Click around the live grid now — pin nurses, watch cells lock solid and grey out, and feel the recompute on every click.

When you're ready to see it against your roster — your roles, your ratios, your local rules — 👉 sign up for a demo. Fifteen minutes, your numbers, your rules.

Real-time scheduling shouldn't be magic. It should just be the answer, when you ask.