Skip to content

Agent and solver ​

Two decisions: the solver for the first version (2 October 2026) and how the tools reach the agent (5 October 2026) · the first solver built in the API on 19 October 2026: tested on small weeks worked out by hand and on the seeded demo stores, its three tools in the agent's catalog, checked once against a local Agentfy, not deployed · people's constraints from their descriptions added on 20 October 2026 (owner's decision that day): tested on small weeks worked out by hand and over the real /mcp against an in-memory Agentfy, checked once against a local Agentfy, not deployed · the descriptions considered on every plan without being asked, since 20 October 2026 (owner's decision that day): the solver's tools hand them to the agent themselves, tested over the real /mcp against an in-memory Agentfy, checked once against a local Agentfy, not deployed · the store's own rules from its description added the same day (owner): tested on small weeks worked out by hand and over the real /mcp against an in-memory Agentfy, checked once against a local Agentfy, not deployed · library facts checked 19 October 2026 · no manager-on-duty tier in a store without a manager, 23 October 2026, tested on a small week worked out by hand and on a store written by the test, not deployed · custom hours — the solver sets a shift's hours itself where no template or usual shift fits — added 26 October 2026 (owner's decision that day): tested on small weeks worked out by hand, on the seeded demo stores and through the tool with an in-memory Agentfy, not checked against a local Agentfy, not deployed · every opening hour covered by default — at least one person on shift — added 28 October 2026 (owner's decision that day): tested on small weeks worked out by hand, on the seeded demo stores and through the tool on a store written by the test, not checked against a local Agentfy, not deployed · store rules that count only the holders of a position, or the managers, added 30 October 2026 (owner's request that day): tested on small weeks worked out by hand and through the tool on a store written by the test like the owner's Test, not checked against a local Agentfy, not deployed · days a week (maxDaysPerWeek, "five shifts a week and two days off") added the same day (owner's request): tested on small weeks worked out by hand, through the tool on a store written by the test like Test, and on the seeded demo stores, not checked against a local Agentfy, not deployed · one rule for every holder of a position, and a position's usual shifts (usualShifts), added the same day (owner's request): tested on small weeks worked out by hand and through the tool on stores written by the test like Test with 11 and with 21 people, the seeded demo stores unchanged, not checked against a local Agentfy, not deployed · the spread over the day and the week added the same day (owner's decision): tested on small weeks worked out by hand, through the tool on stores written by the test like Test with 11 and 21 people, and on the seeded demo stores, not checked against a local Agentfy, not deployed · constraints only with the fingerprint of the descriptions handed over, also when the list is not empty, since 30 October 2026 (owner's decision that day): tested on small cases written by the test and through the tools on the seeded demo stores, not checked against a local Agentfy, not deployed · since 5 November 2026 (owner's decision that day) breaks covered by overlapping usual shifts, usual shifts instead of the store's templates, ties broken so the simplex no longer cycles, overtime taken off after the search, and each rule's numbers in the answer: tested on small weeks worked out by hand, on a store written by the test like MyTest and through the tool on a copy of the owner's local data, not checked against a local Agentfy, not deployed · since the same day (owner's decision) no constraint dropped silently — one that cannot be used stops the plan, a person's rule with a position too is the person's own — and the solver within the tool's 15 seconds: tested on small cases worked out by hand, through the tool on a store written by the test like Test and on a copy of the owner's local data, not checked against a local Agentfy, not deployed · since the same day (owner's decision) every description accounted for, one shift a day unless split shifts are allowed, and managers on duty: tested on small weeks worked out by hand, through the tool on a store written by the test and on a copy of the owner's local data, not checked against a local Agentfy, not deployed · since 10 November 2026 (owner's request that day) HiGHS solves the week, YALPS kept as a fallback: tested on small weeks worked out by hand, through the tool on stores written by the test — Test, MyTest, MyCity, the seeded demo stores and made-up stores of 50, 100 and 120 people — and on a copy of the owner's local data, not checked against a local Agentfy, not deployed · since 10 November 2026 (owner's request that day) the rules read from descriptions stored next to them (save_rules, needsRules), and the kind onlyDays: tested on small cases worked out by hand, through the tools on a store written by the test, and on a copy of the owner's local data (MyMega, MyCity) with a script in place of the assistant, not checked with a live agent, not deployed

The arithmetic stays away from the language model: a solver — an ordinary, deterministic program inside Shiffty's API — builds and checks the week, and the agent only translates between people and that program. Since 19 October 2026 a first version exists: it proposes a week for a store's team and weighs a change request, and a manager confirms either in the app.

The agent talks, the solver counts, and the API has the last word on the rules.

What the solver is ​

A program that assigns people to shifts under given rules. The problem is a classic one — the same kind as scheduling nurses in a hospital, which Google’s optimisation library uses as its textbook example — and it is solved with existing optimisation libraries, not with a language model.

  • It takes data, not text. The team with roles and contract hours, the store’s hours, shift templates, and how many tills each hour needs. These are the fields on Input data.
  • It returns a week, and says what stays open — which hours stay short, which rule is in the way and what would fix it.
  • Hard rules are never broken by a shift it adds.
  • Soft rules are weighed. Coverage first, then overtime, then how much the week changes, then a fair spread of hours.

A language model is poor at this kind of arithmetic and gives no guarantee that every rule holds. Its part is different: understand what a person asked for, call the solver and explain the result in plain words.

What the agent asks of it ​

The four tools proposed on 2 October 2026, and what answers each now:

Proposed toolWhat answers it
Build a schedulesolve_week, the solver (built 19 October 2026)
Check a changecheck_change, the API's request checks (since 13 October 2026); for a manager deciding, evaluate_request
Find coverfind_cover for a person's own shift; evaluate_request names the cover a request would need
Explain a refusalexplain_refusal; and every plan explains each stretch it leaves short

Two rules matter more than how the tools are wired:

  • The agent fills in a fixed form; it does not write the rules itself. Otherwise it can simply forget the rest rule, and nothing would notice.
  • The API re-checks every change with the same rules, whether it came from the agent, the employee assistant or the manager. A confirmed plan and a confirmed decision are checked again when they are confirmed.

What is built ​

Built on 19 October 2026 in the API, as its own part (solver). Tested; not deployed; no manager has used it.

  • Build a week. For a store and a week, the solver proposes the shifts of every person the asker may plan, and says what changes against the week as it is: shifts added, removed and kept, till-hours short now and with the plan, stretches without a manager on duty, everyone's hours against their contract, and, for every stretch left short, why and what would fix it. It writes nothing.
  • Write it into a draft. Once the manager confirms, the plan is written into the week — only a draft week, or a week that does not exist yet, which is then created empty as a draft. It never publishes: publishing stays a person's action in the app. The write re-checks the rights and the rules as they are at that moment and happens only if nobody changed the week since the plan was made; the new shifts carry the origin "assistant".
  • Weigh a request. For a change request that waits for the manager, the solver applies the change to its week and compares: tills short, manager-on-duty gaps, rest and contract hours of the people concerned. When the change leaves something short, it looks for cover on those days only. It returns a recommendation — approve or decline — with the reasons and the cover it would use. Approving does not add the cover.
  • Decide a request. A manager's approve or decline, with a reason for a decline, prepared as a card; confirming it runs the ordinary approve or decline with the manager's rights.

Who may use it: the owner and a store's managers, with the same rights as the schedule screens. A manager's plan leaves the managers' rows alone — they are the owner's — and so a manager-on-duty gap is reported, not filled. An employee cannot use it.

How the agent reaches it is on The agent's tools. On 19 October 2026, in a working copy against a local Agentfy, Daniel (a demo manager) asked for next week (published: advice, no card), then for the draft week after it (a card; confirmed; the draft took the plan), and asked whether to approve Maya's swap (the solver recommended approving; the card, confirmed, approved it as Daniel).

How it builds a week ​

The week as yes/no decisions. For every person who may be planned, every day and every shape — a shift template of the store, or a shift that person works this week or worked the week before, and since 26 October 2026 hours the solver sets itself where none of those fits (custom hours) — the solver has one decision: give it or not. Shifts it may not change are fixed and only counted: a manager's for a manager, a shift an open request is on, anyone's who no longer works there.

Hard rules: a shift the solver adds never breaks one.

  • inside the opening hours — the store's hours as they are when the plan is made, the same every day: a store has one opening and one closing time, and every day counts as open (whether a store can be closed on some days is not decided);
  • one shift a person a day; since 5 November 2026 up to 3 only where a description allows split shifts, and never two that touch or overlap (below) — by hand the editor still allows 3 that do not overlap;
  • no new shift on a day off — an approved time-off request, or one still waiting;
  • at least 11 h rest from one day's last shift to the next day's first, the days around the week included. By hand the same rule is only a warning; the solver keeps it as a hard rule (a decision for the owner, below);
  • the manager-on-duty area only for managers;
  • since 20 October 2026, the hard constraints the agent read from people's descriptions (below).

Tills needed are the day view's: an hour's usual receipts divided by the receipts one cashier handles in an hour, rounded up, at least 1 while the store is open. People count on the tills for the share of the hour they are on a checkout shift, minus a break with a known start. A template without an area may become a checkout or a floor shift: the solver puts it on the tills only where the tills need it.

Soft rules, by weight. The solver looks for the week with the smallest total:

WhatWeight
A till-hour short on checkout10,000
An hour without a manager on duty (only in a store with an active manager, since 23 October 2026: a store without one has no duty to fill)10,000
A person-hour short of the store's rules: at least one person on shift every opening hour, in every store, since 28 October 2026 (below), and the rules from its description (below)10,000
An hour over a person's contract100
A shift added or removed against the week as it is (only when the week already has shifts the asker may change)40
The largest share of a contract anyone is left under8
A shift with custom hours, since 26 October 2026 (below)3
A shift against a person's soft preference from their description (below)1.5
An hour under a contract1
Every shift-hour (so nobody gets a shift for nothing)0.01

The weights keep the order the owner asked for: one till-hour short outweighs any overtime a single shift can bring, and one changed shift outweighs anything one shift does for fairness. They are weights, not strict priorities, and their numbers are our choice, not measured. Below all of them, since 30 October 2026, the spread over the day and the week: not a weight but a last step after the search, which never makes any of these worse.

Deterministic where it is proven. The solver's budget is 10 seconds (8 until 5 November 2026, the owner's decision that day: a better plan over a faster one, inside the 15 seconds the platform gives a tool call). With YALPS every search budget is a number of steps and the time limit a safety net. With HiGHS, the engine since 10 November 2026 (below), the limit is time: a week it proves the best comes out the same every time; a week it stops on may come out a little different from run to run, and the plan says so.

The engine: HiGHS ​

Since 10 November 2026, on the owner's request that day ("let's move to a stronger solver; I want to see how the system works for a store of 100–120 people"). Tested on small weeks worked out by hand, through the tool on stores written by the test (Test, MyTest, MyCity, the seeded demo stores, and made-up stores of 50, 100 and 120 people) and on a copy of the owner's local data; not checked against a local Agentfy; not deployed; no manager has used it.

What changed. The same program, solved by HiGHS instead of YALPS. HiGHS is an open-source optimisation solver from the University of Edinburgh. It runs inside the API through highs-js, which compiles it to WebAssembly: no second language and no separate service. It solves the whole week at once, every possible shift a yes/no decision, with its own branch and cut (presolve, cuts, heuristics). A week it finishes is proven the best under the weights. A week it does not finish in time gets the best plan it found, and the plan says how far from the best that plan can be at most (the gap). YALPS stays as a fallback: SOLVER_ENGINE=yalps in the API's environment, highs by default.

The package, checked on 10 November 2026: npm highs 1.15.4 (16 October 2026), with HiGHS 1.15.1 inside; MIT licence, as HiGHS's own. 4.0 MB unpacked, of which the WebAssembly file is 3.5 MB; no dependencies. One maintainer (lovasoa), releases on 11 September and 16 October 2026. Single-threaded. It loads in about 20 ms on the first solve and runs in jest as it does in the API: unlike YALPS (#87), it needs no loading outside jest's sandbox.

How it is run. HiGHS may take 75 % of the 10-second budget; building the program before it and the spread after it need the rest (120 people: 0.3 s and 0.1 s; 360 people: 1.1 s and 0.8 s). It stops once its plan is proven within 0.04 of the best. That is the cost of the hours of a 4-hour shift, below every other weight; HiGHS's own default, 0.01 % of the total, would be a whole preference or a changed shift on a week with a few hours short. The plan records the engine, whole as the search step, and the gap.

What the YALPS workarounds became:

WorkaroundWith HiGHS
The limit of 600 possible shifts10,000 for HiGHS (360 people were 8,406, planned in 2.7–3.2 s); YALPS keeps 600
Exact search first, then the narrowed search, diving, batches of fixed shiftsNot used: HiGHS searches the whole program. Kept for YALPS
The tie-break nudge (5 November 2026)Kept. HiGHS does not cycle, but the nudge decides which of two equally good plans comes back: without it Test kept 20 of the 55 reference shifts with 11 people (38 with it) and 16 with 21 people (36 with it)
Overtime taken off after the search (above)Kept. It runs only when HiGHS stops before proving its plan; in the measurements below that never happened
The spread (above)Kept, unchanged
YALPS loaded outside jest's sandbox (#87)Still there for YALPS; HiGHS does not need it

Today's stores: the same or better. The solver's own tests make 29 weeks: Test with 11 and 21 people, MyTest, MyCity, the four seeded stores' next and draft weeks, and the description tests. All of the API's solver tests pass with either engine. Both engines then solved each of those weeks, and each plan was priced the same way: its shifts fixed, the program's cost without the nudges. HiGHS was cheaper on 16 and the same on 13, never dearer. It proved 28 of them the best (YALPS 7); one has no plan with either engine, on purpose (a test with a 1 ms limit). Time in plain node: HiGHS 22 ms to 6.6 s, median 0.15 s; YALPS 16 to 774 ms.

WeekYALPSHiGHS
Test, 21 people, a new weekcost 17.9; Галина 8 h under contract; 35 of 55 reference shifts keptcost 8.4, proven; nobody under contract; 36 of 55 kept
Test, 11 peoplecost 4.4; 38 of 55 keptthe same plan, proven
MyTest, first wordingcost 1,035; 10 h overtime; 56 shiftscost 525, proven; 5 h overtime; 53 shifts
Harbor Street, draft weekcost −232; 9 shifts added, 9 removedcost −330, proven; 9 added, 7 removed
Riverside Market, draft weekcost −759; 10 added, 10 removedcost −810, proven; 11 added, 8 removed
Central Station, next weekcost −242, best foundcost −292, proven, 1.3 s
MyCity (the e2e store, 9 people)cost 3.36, proventhe same, proven

Nothing was left short with either engine where the other covered it. The expectations of the existing tests did not change, with one exception: the custom-hours test now checks that YALPS drops custom hours at its limit of 600 and HiGHS takes all of them.

Seeded weeks not made yet, and five days for everyone, both slow until now (Not decided). Measured again on 10 November 2026 on the four seeded stores' next and draft weeks, with today's code. A week with no shifts yet: HiGHS was cheaper on all eight (Old Town 23.4 against 44.2, Harbor Street 23.0 against 39.9). YALPS took 0.06–0.3 s, HiGHS 0.09–5.1 s, except Central Station: there HiGHS stopped at its limit, 7.5 s, with a gap of 5.4 and 5.7 %. Its plan then cost 64.15 and 64.19, while the best plan, proven in 28–30 s, costs 64.15; YALPS's cost 77.6 and 73.0. So the gap there is what HiGHS could not yet prove, not what the plan lacks. With five days a week for everyone, YALPS took 0.05–0.12 s (the 172 s of 30 October 2026 no longer happen on today's code) and HiGHS 0.08–1.9 s; HiGHS was cheaper on six weeks and the same on two.

A store of 50, 100 and 120 people. A made-up store built through the API's routes like MyCity, by api/test/bench/storeScale.bench.ts: open 07:00–23:00; a manager on duty and managers in the store all day; cashiers on the checkout all day, more 17:00–21:00 on weekdays; a sales-floor minimum 09:00–21:00; loaders 07:00–15:00 only, more on Tuesday and Friday 07:00–11:00; usual shifts 07:00–15:00 and 15:00–23:00; five days a week per position. Two of every five people have a personal rule in their description, the kinds in turn: noDays, timeWindow, maxHoursPerDay, maxDaysPerWeek and prefer. Contracts are 20 to 40 hours. The owner's solve_week plans an empty next week with every rule the agent would pass. A script then checks every rule from the plan's shifts, not from the solver's summary: one shift a day, 11 h rest, every personal and position rule, and each store rule hour by hour. Two loads: the rules as above, and the same rules asking 1.6 times as many people, about what the contracts can give.

PeoplePossible shiftsYALPS as it runs (limit 600)YALPS without the limitHiGHS
501,180refused1.4 s, best found, the same cost0.5 s, proven
1002,345refused6.4 s, best found, cost 139.9, 96 h under contract0.8 s, proven, cost 132.0, 88 h under contract
1202,802refused6.8 s, best found, the same cost1.1 s, proven
50, 1.6×1,180refused2.0 s, the same cost0.5 s, proven
100, 1.6×2,345refused8.1 s, the same cost0.8 s, proven
120, 1.6×2,802refused10.3 s, 4 h more overtime and 4 h more under contract0.9 s, proven

Times are the solver in plain node, as the API runs it; the whole tool call through jest took 0.9, 1.4 and 1.9 s with HiGHS. In every HiGHS plan the script found no rule broken. With the first load nothing is short, nobody works overtime, and 9, 16 and 19 people stay under contract by 52, 88 and 104 hours: those whose own rules (four days, days off, mornings or evenings only) leave no more. With 1.6× the sales floor stays short by 66, 204 and 192 person-hours, the same as the solver's own count. 9, 18 and 22 people then work 100, 216 and 232 hours of overtime: the store's rules weigh above overtime (Not decided). The same 120-person week planned again after ten people get a day off: HiGHS 0.6–0.7 s, proven (4 shifts added and 4 removed; with 1.6×, 13 added and 5 removed). Two and three copies of that store (240 and 360 people, 5,604 and 8,406 possible shifts) were proven the best in 2.0 and 2.7–3.2 s. So 100–120 people fit the 10 seconds with room to spare, and no background solve is needed for them.

The owner's MyCity, week of 16 November 2026. On a copy of the owner's local data restored into this working copy's own database, through the real solve_week as the owner, with the full set: the store's six rules, the four positions' (usual shifts, five days, loaders 07:00–15:00, a cashier and a manager all day), and the personal ones — Степан noDays Mon, Wed, Thu, Sat, Sun. 30 constraints applied and 2 ignored; 20 people, 272 possible shifts. The week did not exist yet; the week before had 90 shifts.

YALPSHiGHS
The tool call / the solver127 ms / 74 ms198 ms / 143 ms
Resultbest found (narrowed search)proven the best
Cost22.8622.86
Shifts92: 45 × 07–15, 42 × 15–23, 3 × 17–23, 2 × 07–13the same
Rules broken (the script)nonenone
Short: tills, store rules, manager on duty, nobody on shift00
Overtimenonenone
Under contractАлина −6 h, Дарья −4 h, Степан −4 hthe same

The same plan; HiGHS proves it is the best. The three under contract are the ones their own rules hold back (four days and not before 14:00; weekends only; Tuesday and Friday only). The owner's data was not changed: nothing was confirmed, and the copy is this working copy's own.

Not deterministic in the same way. YALPS's budgets are steps. HiGHS's limit is time: a week it proves comes out the same every time, but a week it stops on (Central Station's empty week above) can come out a little different on a slower machine or a busier API. The plan then says "the best plan the solver found in the time it has", with the gap.

People's constraints from their descriptions ​

Added on 20 October 2026, on the owner's decision that day. Tested on small weeks worked out by hand and over the real /mcp against an in-memory Agentfy, checked once against a local Agentfy; not deployed; no manager has used it.

The store and each person have a description in plain words. The solver does not read text: the agent reads the descriptions and turns what one clearly says into a constraint, with the words it came from, and passes it to solve_week (and to evaluate_request and decide_request, so cover keeps to it). The manager sees every constraint applied, with its source, and what the agent left out because no kind expresses it, with why, on the card before confirming. A wrong reading is the manager's to correct: ask again without it, or with it fixed.

KindExample in a descriptionWhat the solver does
Days off (noDays)"Does not work on Wednesdays"Hard: no new shift on those weekdays
Only these days (onlyDays)"Only Tuesday and Friday"Hard: no new shift on the other weekdays (since 10 November 2026, below)
One area (onlyArea)"Only on the sales floor"Hard: no new shift in another area; a template without an area becomes that one
Hours (timeWindow)"Not before 10:00", "leaves by 15:00 on Fridays"Hard: no new shift starts earlier or ends later, on every day or on the days named
Hours a day (maxHoursPerDay)"At most 6 hours a day"Hard: the person's shifts of one day, net of breaks, never add up to more
Days a week (maxDaysPerWeek)"Five shifts a week and two days off" (5)Hard: no new shift on a day past that many days with a shift in the week (below)
A wish (prefer)"Prefers mornings", "rather no weekends"Soft: a shift against it costs 1.5 — mornings (a shift starting before 12:00), afternoons and evenings, fewer weekends, or named days
Usual shifts (usualShifts)"Usual shifts 07:00–15:00, 10:00–18:00"Not a limit: each is offered on every day like a template, also to someone with no shifts yet, instead of the store's templates since 5 November 2026 (below, breaks)

Since 30 October 2026 each of these may name a position instead of a person: it then applies to every holder of the position (below).

  • A wish only breaks a tie. Its weight is below a changed shift (40) and below the contract hours of a 2-hour shift, so it never leaves a till short, never leaves anyone under contract for it, and never moves a shift the week already has. It chooses between shifts that do the same for the tills and the contracts.
  • A current shift against a hard constraint is removed by the plan, and a warning on the card names it ("Removes Maya Wed 08:00–16:00 (checkout): Maya: not on Wed (description)"). A shift the plan may not change — an open request is on it, or it is a manager's row for a manager — stays, and the warning says why. Nothing is removed silently.
  • Checked one by one; one that cannot be used stops the plan (since 5 November 2026, below). Someone not on the store's active team, a person whose shifts the asker may not plan (a manager's row for a manager), a time that is not a time, a window that ends before it starts: the solver plans nothing and sends each such constraint back to the agent with why, to be fixed and sent again in the same turn. Until that day it was ignored with its reason and the plan was made without it.
  • Not store rules. Nothing stores them: they are the manager's choice for this plan, and confirming the card does not check them again — only the shift rules (hours, rest, rights) are re-checked, as before; the same holds for the store's rules from its description. The card's record keeps them, with the plan. Since 10 November 2026 the rules read from descriptions are stored next to them (below); confirming a card still does not check them again.
  • The same data gives the same week, constraints included.
  • The reading is the language model's, and it can be wrong. On 20 October 2026, in a working copy against a local Agentfy, Maya's description was set to "Does not work on Wednesdays" and Daniel asked for the draft week: the card listed "Maya: not on Wed — from “Does not work on Wednesdays”", the plan removed her Wednesday shift and said so, and after confirming the draft had no Wednesday shift for her. In the same check the agent first did not read the descriptions at all, and then read too much: "leads the Friday delivery" as "not on Fridays", "good with self-checkout faults" as "checkout only". After the instructions were made stricter, those two did not come back; "rarely before 14:00" still became a hard "not before 14:00". That is why every constraint is on the card with its source.
  • The descriptions are considered every time, without being asked (owner, 20 October 2026; the store's description counts as much as a person's, below). In the first check the agent often skipped them unless the manager said "take the descriptions into account". Since that day the API sees to it, not only the agent's instructions: when the people a plan would cover have descriptions and the agent's call carries no constraints at all, the solver does not plan. It hands the agent those descriptions, and the store's, and asks again — with the constraints read from them, or with an explicit "none" when nothing in them limits when or where anyone works. "The people it would plan" follows the asker's rights: for a manager the store's employees, never a manager's or their own description; for the owner everyone, the managers too. The same goes for weighing and deciding a request, for the people the cover search may plan. People without descriptions are planned as before, without the extra step.
  • The card shows that the descriptions were read. With constraints, each is listed as above, and a line names the people whose descriptions limit nothing; with "none", the card says "Descriptions read: nothing in them limits when or where people work", so the manager sees they were considered.
  • "None" has to show the descriptions were read. The descriptions come with a short fingerprint. An empty list of constraints counts only together with that fingerprint; without it the solver hands the descriptions over again. In the first live check the agent sent "none" on its very first call, before it had read anything, and the card claimed the descriptions were read; this rule closes that gap.
  • Never from memory (owner, 30 October 2026). Until that day constraints that were not empty were taken without the fingerprint, since they name people and quote their descriptions. In the owner's store Test a later message ("I don't see the card") made the company assistant plan the week again: it no longer saw its earlier call, retyped the constraints from memory without the fingerprint — positions nobody holds, one position's rule only, no limit on hours a day — and the solver planned one person seven days in a row. Since then any list, empty or not, counts only with the fingerprint of the descriptions as they are now; without it, or with an old one, the solver plans nothing and hands the descriptions over again — with the store, the week, each position's description with who holds it, each person's and the store's — and one line saying why: constraints must be read from these descriptions in this turn. The assistants' instructions say the same: every time a week is built or built again, start from the descriptions, and when the person did not see the card, point to it instead of planning again.
  • A reading made before an edit is caught by the fingerprint: if a description has changed since, the solver hands the new ones over instead of planning.
  • Checked on 20 October 2026, in a working copy against a local Agentfy, with "Does not work on Wednesdays" for Maya and "Only on the checkout" for Jonas, and the demo's other descriptions as they were. Daniel asked "Plan the draft week starting 2026-11-02" without mentioning descriptions. The agent called the solver, got the descriptions back, and called again with constraints. The card listed "Maya: not on Wed", "Jonas: checkout only", "Leo: not before 14:00 (Mon–Fri)" with their sources, and "Descriptions read: nothing in the description of Chloe limits when or where they work". The owner's request for the same store gave the same three, and named Chloe and Hannah — a manager — as limited by nothing. Leo's "rarely before 14:00" still became a hard rule, against the instructions; the card shows it, and the manager corrects it.

The store's rules from its description ​

Added on 20 October 2026, on the owner's decision that day. Tested on small weeks worked out by hand, on the seeded stores and over the real /mcp against an in-memory Agentfy, checked once against a local Agentfy; not deployed; no manager has used it.

The store's description is read like a person's: the agent turns what it clearly says about how many people the store needs, or how many tills it has, into a rule of the store's, with the words it came from. The store's description alone is enough for the solver to hand the descriptions over and ask, as above; with no description anywhere, nothing is asked. Someone without a description is never listed and needs nothing.

KindExample in a descriptionWhat the solver does
People in an area (minStaff)"There must always be one person on the sales floor", "at least 3 people from 17:00 to 20:00 on weekdays"At least that many on checkout, on the sales floor, or anywhere (the manager on duty counts as anyone), in the hours and on the days named; all day when no hours are named
Tills the store has (maxTills)"4 tills"The tills needed from the hourly load never exceed it
Extra people (extraStaff)"Delivery on Tuesday and Friday mornings 08:00–11:00 needs one extra person"That many more than are otherwise needed, in those hours and on those days, anywhere unless an area is named
  • Weighed as the tills, never refused. An hour short of a rule costs as much as a till-hour short (10,000 per person-hour), so a week never becomes impossible. What stays short is listed with the rule and why — "1 person short in the store (Store: at least 3 people 17:00–20:00 (Mon–Fri)). Nobody can take it under the rules: …" — on the card's warnings and in the plan's explanation. Extra people sit at the same weight as the rest: the store says it needs them, as the load says it needs tills, and a lower weight would let overtime or a changed shift outweigh them. That is our choice, for the owner's review.
  • Counted once. Per hour, people on checkout are the tills needed or a rule's number, whichever is larger, plus any extra on checkout; on the floor, the rule's number plus any extra there; anywhere, the larger of a rule's number and the two before, plus any extra. A missing person is reported in one place: on the tills first, then checkout, then the sales floor, then anywhere. A template without an area is two choices when the floor needs people — checkout or floor — so one shift never counts for both.
  • What is already the store's data is not a rule. Opening hours (the agent proposes the store's hours instead), a busy time without a number of people ("peak 17–20": at most a suggestion for the hourly load), a shift's shape (a template). A rule's hours outside the opening hours ("deliveries before opening") are sent back with the reason (ignored with it until 5 November 2026); the agent leaves the rule out and says so.
  • On the card, apart from the people's. The store's rules come first, each with its source; then the people's; then what the agent left out and why; and a line when the store's description sets nothing ("Descriptions read: nothing in the store's description sets how many people it needs.").
  • Requests too. Weighing a request counts the store's rules: a change that leaves them shorter is weighed like one that leaves the tills short, and the cover the solver names keeps to them.
  • Checked on 20 October 2026, in a working copy against a local Agentfy, with Riverside's description set to "There must always be one person on the sales floor. 4 tills. At least 3 people from 17:00 to 20:00 on weekdays. Delivery on Tuesday and Friday mornings 08:00–11:00 needs one extra person." A manager of the store asked to plan the draft week, without mentioning descriptions. The card listed, first, "Store: at least 1 person on the sales floor 08:00–22:00", "Store: 4 tills at most", "Store: at least 3 people on the checkout 17:00–20:00 (Mon–Fri)" and "Store: 1 person extra 08:00–11:00 (Tue, Fri)", each with its source, then the people's constraints. Its summary read "Store's own rules: 32 → 14 person-hours short with the plan", and the hours still short were among its warnings with why. The agent read "at least 3 people" as three on the checkout rather than anywhere — a wrong reading, shown on the card for the manager to correct. The check was made as Hannah, the store's other manager: Daniel's chat was refused by the local Agentfy (409 on his chat membership) after the copy's database had been reset, which has nothing to do with the rules.

Rules that count a position ​

Added on 30 October 2026, on the owner's request that day. Tested on small weeks worked out by hand and through the tool on a store written by the test like the owner's store Test; not checked against a local Agentfy; not deployed; no manager has used it.

Since the team keeps a catalogue of positions with descriptions, a position's description can say how many of its people a store needs: "every day 07:00–23:00 at least one cashier on the checkout", "Mon–Sat 07:00–18:00 at least one loader in the store". A store rule (minStaff, extraStaff) may name a position, and then only the people who hold it count toward it — a loader on the tills does not make up for a missing cashier. A rule may instead count only the store's managers. What the description says about each holder ("a loader's shift ends by 18:00") stays a person's constraint, given to every holder — since the same day as one rule that names the position (below).

  • The position by its name, in any case, among the positions the store's team holds. One nobody on the team holds is sent back with the reason and the team's positions named (ignored until 5 November 2026).
  • Worded with the position. "Store: at least 1 Кассир on the checkout 07:00–23:00"; what stays short: "no Грузчик in the store (…). Nobody can take it under the rules: …".
  • Weighed as the tills, like the other store rules, and kept apart from them: each position and area has its own count per hour, which only its holders' shifts in that area meet. Custom hours made for such a gap are offered only to its holders.
  • What it costs. On a store like Test (open 07:00–23:00, eleven people on 40 hours, no templates, the owner's week before as history) the week took 123 possible shifts, 488 constraints and about 0.1 s of solving; the seeded demo stores, which have no such rules, are unchanged (308–594 possible shifts, up to about 3 s).
  • What it gave on that store. A cashier on the checkout every hour, a loader Monday to Saturday 07:00–18:00 with no loader's shift ending later, a manager on duty every hour, every shift 8 hours, 32 of the reference week's 55 shifts the same. But not everyone 5 shifts and 2 days off: two loaders on 40 hours cannot cover 66 loader-hours with shifts that end by 18:00, and because the rule weighs above overtime the plan gave each loader a sixth shift; the floor rule gave one person seven; and, since a person may work two shifts a day (owner, 2026-10-14), two people worked two shifts on one day. The owner's own week leaves Monday and Saturday 15:00–18:00 without a loader instead (see Not decided).

Days a week ​

Added on 30 October 2026, on the owner's request that day. Tested on small weeks worked out by hand, through the tool on a store written by the test like the owner's store Test, and on the seeded demo stores; not checked against a local Agentfy; not deployed; no manager has used it.

On 30 October 2026 the owner asked for the next week of Test, whose positions all say "8-hour shift, five shifts a week and two days off, one shift a day". The agent applied the position rules and "at most 8 hours a day", but there was no kind for "five shifts a week", so it was dropped without a word, and the plan had four people on all seven days (16 hours of overtime each). Since that day a person's constraint counts the days with a shift: maxDaysPerWeek, and the agent passes "five shifts a week and two days off" as 5 for every holder of the position.

  • Days worked, not days off. The solver plans one week, so it counts what it chooses: days with a shift. "Two days off" is 7 − 2 = 5. Counting days off instead would leave open whether an approved time-off day, or a day outside a partly planned week, is one of them.
  • Hard, like a day off. No shift the solver adds makes a day past the count. A day counts once however many shifts it has. Days that already have a shift the plan may not change (a locked one, a manager's row for a manager) count first; once they fill the count, nothing new is offered on another day, and the plan says why ("Ann already works 5 days this week in shifts this plan may not change"). If they already break it, they stay, and the card warns about each day past the count. A week the plan may change that has too many days loses the extra ones.
  • Named in the reasons. An hour nobody can take says who already works the full count ("Ann already works 5 days that week: at most 5 (description)").
  • On Test. A store written like Test (open 07:00–23:00, eleven people on 40 h, the owner's week of 26 October as this week's reference, the rules from the agent's card, "at most 8 hours a day" and 5 days for everyone): 55 shifts, nobody over 5 days or 40 hours, every rule met, 29 of the reference's 55 shifts the same, 126 possible shifts, about 0.15 s. We could not reproduce the owner's 63-shift plan on that store: without the new rule it also gave 55 shifts and no overtime, with two or three templates added too (402–552 possible shifts) and with the search forced straight to diving. So that plan's overtime came from something our copy of Test does not have; the owner's card and the store's templates are needed to find it.
  • What it costs on a large week. The seeded stores have no such rule, so their plans are unchanged. With 5 days for every employee as an experiment, all eight weeks were still fully covered, but Central Station's next week (584 possible shifts) took 172 seconds instead of 0.9, and Riverside's draft 8.1 instead of 0.7: the narrowed search finds no whole plan, and diving's repeated solves are not bounded by the 8-second limit (Not decided).

One rule per position, and usual shifts ​

Added on 30 October 2026, on the owner's request that day. Tested on small weeks worked out by hand and through the tool on stores written by the test like the owner's store Test, with 11 and with 21 people; not checked against a local Agentfy; not deployed; no manager has used it.

The owner asked whether Test would still get a right week with ten more people. It would not have. The agent wrote "at most 8 hours a day" and "5 days a week" once for every person, so 21 people needed about 60 constraints, more than the 50 a call took, and the call was refused. And the new people, who have no shifts yet in a store with no templates, had no shapes of their own: only custom hours. Since that day a person's constraint may name a position instead of a person, and a position's usual shifts are a kind of their own, usualShifts.

  • One rule for every holder. noDays, onlyArea, timeWindow, maxHoursPerDay, maxDaysPerWeek, prefer and usualShifts take position instead of personId. The solver applies the rule to every holder of the position whose shifts the asker may plan, and to nobody else. The card shows it once: "every Кассир: at most 5 days a week". A position nobody on the team holds, or whose holders the asker may not plan (a manager's row for a manager), is sent back with the reason (ignored until 5 November 2026). Test's rules are now 21 constraints however many people it has, instead of 29 for 11 people and about 60 for 21. A person's own description still gives that person's own constraints.
  • Up to 100 constraints in a call (it was 50), so a store whose people have descriptions of their own, or an agent that still writes one rule per person, is not refused for its size.
  • Usual shifts. "Usual shifts 07:00–15:00, 10:00–18:00" in a position's description becomes usualShifts {position, shifts ["07:00-15:00", "10:00-18:00"]}, or the same with personId from one person's description. The solver offers each one to every holder on every day, as it offers a template, with a template's weight, not custom hours' extra cost, and also to someone with no shifts yet. It is not a limit: the shapes a person already works stay possible. A shift outside the opening hours, or one that is not a time, is sent back with the reason (ignored until 5 November 2026).
  • One area each. A usual shift goes to the area the rule names. Otherwise it goes to the area the person is limited to, then the one they work most, then the one the holders of their position and role work most, and only then to whichever area the day needs. So a new cashier's usual shifts go to the checkout and a new deputy manager's to manager on duty. One area per shift, not a choice of two, keeps a usual shift one possible shift, as a template with an area is. That is what keeps a store of 21 under the limit.
  • On Test. A store written like Test (open 07:00–23:00, no templates, no hourly load), the positions' descriptions as on Test with their usual shifts, everyone on 40 hours, the owner's week of 26 October as the week before for the eleven people it has. The ten new people have no shifts yet: three cashiers, a senior cashier who is a manager, two loaders, two on the floor, a deputy and a deputy who is a manager. The owner's solve_week with the rules the agent would pass (the store's five, and per position "at most 8 hours a day", "5 days a week", the loaders' 07:00–18:00 and the usual shifts):
11 people21 people21 people, no usual shifts (before the spread)
Constraints passed212116
Possible shifts237461406
Time, through the tool0.3 s0.5 s0.4 s
Moved by the spread (below)635—
Search stepnarrowednarrowednarrowed
Shifts55105105
Of them custom hours0050
Overtimenonenonenone
Under contractnobodynobodynobody
Days off2 for everyone2 for everyone2 for everyone
Every rule met, half hour by half houryesyesyes
Same as the reference week31 of 5533 of 5531 of 55
  • Before the spread. With 21 people the plan first put the ten new people on weekdays, mostly 07:00–15:00: on Monday ten people started at 07:00 and one cashier worked the evening. Every rule was met, but nothing asked for more. Since the same day the solver spreads people over the day and the week (below).
  • The seeded demo stores pass no such rules, so their model is unchanged: on 30 October 2026 the same plans, the same possible shifts (308–594) and search steps as before, and times 6–8 % slower on one run each on one laptop (0.37–2.5 s). Weeks with no shifts yet, the slow case below, were not measured again; their model is unchanged too.

Spread over the day and the week ​

Added on 30 October 2026, on the owner's decision that day. Tested on small weeks worked out by hand, through the tool on stores written by the test like the owner's store Test with 11 and with 21 people, and on the seeded demo stores; not checked against a local Agentfy; not deployed; no manager has used it.

With 21 people on Test every rule was met, but the ten new people piled up on weekday mornings (above). Since that day the solver spreads people over the day and the week. Every opening hour has a fair share of the week's people: the contract hours of the people it plans, plus the shifts it may not change, divided over the opening hours in proportion to how long the store is open in that hour — or, in a store with an hourly load, in proportion to its usual receipts. The plan keeps as few person-hours below those shares as it can.

  • Last of all. The spread never makes anything else worse. The tills, the store's rules, the manager on duty, every opening hour covered, overtime, contract hours, preferences, custom hours and changes to the week all stay as the plan had them; it only chooses between plans that are equal in all of those. It is the solver's own, for every store: not a constraint the agent passes, and nothing for the agent to do.
  • How. After the search, one new shift at a time moves — to another shape the same day, or to a day the person has free, with the same hours — where that lowers the person-hours below the fair shares and changes nothing above. A shift the week already has never moves. A usual shift on the day the person worked it the week before moves only to a thinner hour (since 5 November 2026; before, it never moved), and afterwards a new shift goes back to the person's usual shape on its usual day wherever nothing above changes and no hour gets thinner. The plan's explanation says how many shifts it moved, and the person-hours below the fair shares before and after. A cover search for a request does not spread: it adds as little as it can.
  • Why not a weight in the program. That was tried first: a row per opening hour, "people on shift + short ≥ fair share", at a weight below everything else. YALPS's relaxed program then stalled. On the 11-person Test week, 500 pivots took 9.6 seconds, and the program needed 8,000 pivots and 215 seconds to finish; without those rows it is solved in 0.2 seconds. The 21-person week did not finish in minutes, and rows per 4-hour band stalled the same way. As a pass after the search it cost about 0.1 seconds on the 21-person week and keeps the same order: below everything else.
  • On Test with 21 people. People on shift, on average, per band of the day, before → after:
07–1111–1515–1919–23
Mon10.5 → 6.512.5 → 8.54.5 → 8.52.5 → 6.5
Tue12.8 → 5.815.5 → 8.57.3 → 9.34.5 → 6.5
Wed13.8 → 6.816.5 → 8.57.3 → 8.34.5 → 6.5
Thu13.8 → 6.816.5 → 87.3 → 8.34.5 → 7
Fri10.8 → 6.813.5 → 8.55.3 → 8.32.5 → 6.5
Sat3.3 → 7.34 → 82.8 → 7.82 → 7
Sun3.3 → 7.34 → 82.8 → 7.82 → 7

The fair share there is 7.5 people every hour (21 × 40 hours over 112 opening hours). The spread moved 35 shifts. Everything else held: 105 shifts, no overtime, nobody under contract, two days off each, every rule met half hour by half hour, 461 possible shifts, the narrowed search, 0.4 → 0.5 seconds through the tool, and 33 of the reference week's 55 shifts the same (32 before). With the eleven people now it moved 6 shifts, and the reference week's shifts kept went from 30 to 31.

  • The seeded demo stores. On all eight of their next and draft weeks the spread found no move that kept everything else as it was, so their plans are unchanged; times as before. In two other weeks that the solver's tests plan it moved 2 and 5 shifts. One week with no shifts yet, the slow case below, took 214 seconds without the spread and 217 with it (one run each, one laptop).

Breaks covered, and numbers the agent quotes ​

Added on 5 November 2026, after the owner's store MyTest got no week, and on the owner's decision that day that shifts may be moved or overlapped so a manager's or cashier's break is covered. Tested on small weeks worked out by hand, on a store written by the test like MyTest, and through the tool on a copy of the owner's local data; not checked against a local Agentfy; not deployed; no manager has used it.

The owner asked for next week at MyTest: open 07:00–23:00, four managers on 40 hours, four cashiers on 40 and one on 20, three on the sales floor, three loaders; at least one manager in the store and one cashier on the checkout every minute; five shifts a week. The agent answered that the rules were physically impossible and did its own arithmetic ("160 h is not enough for the managers, 112 needed"). The rules could be met; the solver had made no plan at all, and the agent read that as impossible.

  • Why there was no plan. On a store whose people are alike — the same contracts, the same templates, every shift costing the same — YALPS's simplex went round in a circle until its limit of 8,192 pivots ("cycled", 6 seconds) and came back with nothing. YALPS has no rule against that. Since that day every cost gets a different nudge of at most a millionth, far below the smallest weight (0.01 an hour), the same every time: the same relaxed problem then takes about 60 milliseconds. Between plans that cost exactly the same the nudge decides which comes back; on the seeded demo stores' tests one such tie came back the other way.
  • Why "8 hours a shift" did not add up. The store's 8-hour templates fix their breaks: 11:00 in the morning, 18:00 in the evening. A break with a known start counts as time away, so with two shifts a day a manager on a break leaves the store without one, twice a day. The owner's "14 shifts of 8 hours" leaves the breaks out. With those templates, at most 8 hours a day and five days a week, the solver proved that 5.5 person-hours a week stay short: the relaxed problem's lower bound equals the plan found.
  • Shifts that overlap by the breaks. Where a store rule counts the holders of a position or the managers every minute (minStaff with position or managers), a usual shift that hands over to the next — 07:00–15:00 and 15:00–23:00 — is offered as custom hours that overlap by the two breaks, each break inside the overlap: 07:00–15:30 with its break at 15:00, and 14:30–23:00 with its break at 14:30. Each is the usual shift's hours plus its break, 8 hours worked. They replace the plain usual shift for those people, whose break with no start the count would take as worked. A shift that hands over to none, has no break, or would leave the opening hours stays as it is.
  • Usual shifts instead of the store's templates. Someone whose own or position's description names usual shifts gets those, not the store's templates — so a 16-hour "full day" template is no longer offered to a cashier whose usual shifts are 8 hours. A manager's usual shift whose area nothing else says is manager on duty. On MyTest with the owner's rewording this took the week from 714 possible shifts (refused: the limit is 600) to 189.
  • Overtime taken off after the search. The narrowed search and diving round shifts up; on MyTest that came out as overtime nobody needed (24 of 20 hours while every hour was covered). For anyone over contract the solver then tries, in a fixed order, dropping one of their shifts, giving it a different shape that day, or giving it to someone else on the same day and hours, who gives up what they had that day — keeping a change only if the whole plan gets cheaper with every rule still kept. The plan says how many it changed.
  • Numbers to quote. The plan and the tool's answer have coverage: for each rule that counts a position or the managers, and for the manager on duty, the person-hours it asks over the week, how many the plan covers and how many stay short, how many people may fill it, their contract hours together and their hours in the plan. Where a rule stays short the explanation says it in one line: "Менеджер: the rules ask 112 person-hours this week; the plan covers 108, 4 short. The 4 people who may fill it have 160 contract hours together … — the hours are there, so what stays short comes from when they may work". A result with no plan says on every stretch "Not planned: no plan was made (…)" and "never call them impossible", instead of "no manager can take it under the rules". The agents' profiles tell them to quote these numbers and never compute their own (store revision 16, owner revision 12).
  • The same day, after a full test run. The tie-break settled ties among equal plans arbitrarily, so Test's week kept 10 of its reference shifts where 30 had been kept. Now the choices that keep the week as people work it — a shift it has, or a person's own shape on the weekday they worked it — get the smaller nudges; a week with nothing to keep gets the nudge as it was. On Test 38 of 55 reference shifts are kept with 11 people and 36 with 21, every hour still at least 60 % of its fair share. The solver's tests had also run YALPS hundreds of times slower than the API does (jest runs code in a sandbox where every global lookup is slow, and YALPS looks one up per matrix cell: MyTest's relaxed problem took 25–63 s there against 0.09 s), so tests hit the time limit and came out different; they now run YALPS as the API does. Tested on small weeks worked out by hand and through the tool on stores written by the test; not deployed.
MyTest, week of 9 November 2026BeforeAfter
First wording: managers and cashiers 07:00–23:00, five days, loaders 07:00–15:00No plan ("cycled"), 525 possible shifts, 6.3 sEvery minute covered; one manager 5 h over (the 16-hour templates and the fixed breaks leave little else); 19 sixteen-hour shifts; 0.5–0.6 s
The same with "at most 8 hours a day"No plan ("cycled")5.5 person-hours short, proven; one cashier 17.5 h over — the store's rules weigh above overtime
No constraints at allRefused: 609 possible shifts against 600The same; the duty gaps now say "Not planned"
The owner's rewording: usual shifts 07:00–15:00 and 15:00–23:00, which may overlapRefused: 714 possible shifts72 shifts, a manager and a cashier on the checkout every minute (checked minute by minute), nobody over contract, no 16-hour day, at most five days each; 189 possible shifts, 0.12–0.14 s through the tool

One run each on one laptop, against a copy of the owner's local data. The wording "Требуются на весь рабочий день магазина (7-23)" can be read as a 16-hour shift for each person rather than one person at every moment; the owner's rewording ("usual shifts 07:00–15:00 and 15:00–23:00 … at least one manager every day 07:00–23:00 … may be moved or overlapped") says what was meant.

No constraint dropped silently ​

Added on 5 November 2026, after the owner's store MyCity got a week that broke eight people's descriptions, on the owner's decision that day. Tested on small cases worked out by hand, through the tool on a store written by the test like Test, and through the tool on a copy of the owner's local data; not checked against a local Agentfy; not deployed; no manager has used it.

The owner asked the company assistant for next week at MyCity: 20 people, each with a description, open 07:00–23:00. The assistant sent every person's rule with both the person and their position. The solver took "a person or a position, not both" as a reason to leave each one out, listed them on the card as ignored, and planned the week without them: Андрей on a Sunday he never works, Роман on a Wednesday, Игорь on a Monday, Алина at 07:00 when she starts at 14:00 at the earliest, Максим 8 hours when at most 6, Полина on a Sunday, Евгений on a Thursday, Степан on a Saturday. "Delivery on Tuesdays and Fridays 07:00–11:00 needs one extra loader" came without the position, so anyone counted as the extra person. The store's rules were kept and every personal one was dropped, and the card was the only place that said so.

  • A person's rule with a position too is the person's own. It came from that person's description, so the solver keeps the person, drops the position and says so in a note on the card and in the answer ("named both a person and the position “Менеджер”: read as Андрей's own rule, without the position").
  • A constraint the solver cannot use stops the plan. Someone not on the store's team, a position nobody holds, a row the asker may not plan, a time that is not a time, a store rule naming a person, a window outside the opening hours: the solver plans nothing and makes no card. It answers like the answer that hands the descriptions over (needsConstraints, reason rejected): each such constraint with its place in the list and why, the descriptions and their fingerprint, and an instruction to fix them and call again in the same turn. The same holds for weighing and deciding a request.
  • "Ignored" is only what the agent leaves out itself. Something a description says that no kind expresses — a skill, a habit, a delivery before opening — goes into ignored with the agent's reason, and the card shows it as ignored. Nothing the agent passed as a constraint is ignored any more.
  • A store rule that names a position carries it. "One extra loader" is extraStaff with position Грузчик; "two cashiers" is minStaff with position Кассир. When a store rule counts anyone and its words name a position the team has, the card and the answer get a note asking whether only its holders should count. The check is a word match on the rule's own words, not a reading: "в зале" ("on the floor") is a place, not the position Зал, and is left alone.
  • The agents are told the same, with examples (store profile revision 17, owner revision 13): a person's rule takes only the person ("works weekdays only" → noDays sat, sun; "not before 14:00" → timeWindow from 14:00), a rule for every holder of a position only the position, and a rejected answer is fixed and sent again at once.

On a copy of the owner's local data, the owner's solve for the week of 9 November 2026, each set of constraints sent as the agent would. The store's rules are the six the assistant read (a manager every hour, one cashier on the checkout every hour and two 17:00–21:00 on weekdays, one person on the floor 09:00–21:00, 4 tills, one extra loader on Tuesdays and Fridays 07:00–11:00), with each position's usual shifts.

Week of 9 November 2026What the solver didPersonal rulesStore's rulesHours against contractsTime
The assistant's own set (each person's rule with both ids) on the draft it had writtenRead each as the person's own, with a note each (13), and a note that the extra loader counted anyoneAll 11 hard ones kept; the 11 shifts that broke them removed, 5 addedAll kept; nothing shortNine people under contract, from 4 to 12 hours: the plan changes the week as little as it can, and a change outweighs hours under contract0.2 s
The same with one broken constraint added (noDays without days)Planned nothing; sent that one back with its place and why———under 0.1 s
The corrected set, from an empty weekSolved, 92 shiftsAll keptAll kept; nothing shortАлина 24 of 30 (at most four shifts, none before 14:00, of 8 hours), Дарья and Степан 16 of 20 (two days of 8-hour shifts each)0.4 s
The same plus "5 shifts a week" for every positionSolved, 92 shiftsAll keptAll kept; nothing shortThe same0.5 s

One run each on one laptop, after #87 (YALPS in the tests as in the API). The soft wishes were kept where nothing else depended on them (Светлана's mornings, Юлия's evenings). The owner's draft and data were not changed: the copy is this working copy's own database.

Within the tool's time. Agentfy gives a tool call 15 seconds; the solver's budget is 10 seconds (owner's decision that day, #87). YALPS stops only its exact search on time, not a relaxed program, so a dive that fixes one part-chosen shift a round could run on past the budget. Since that day the narrowed search may take at most 40 % of the budget, and the dive fixes shifts in batches once one at a time would not fit what is left — at most one a person per batch (a person a day once past the budget), halved while a batch leaves no answer — and gives up 3 seconds past the budget. On MyCity these weeks take well under a second, so the batches are a safety net here; they were measured before #87, when the tests ran YALPS many times slower than the API does.

Every description accounted for, one shift a day ​

Added on 5 November 2026, on the owner's decision that day, after the second MyCity week. Tested on small weeks worked out by hand, through the tool on a store written by the test like Test, and through the tool on a copy of the owner's local data; not checked against a local Agentfy; not deployed; no manager has used it.

After the fix above the company assistant built MyCity's week again. It kept all the personal rules and the store's, but passed no rule from any of the four positions' descriptions — Менеджер, Кассир, Зал, Грузчик: no usual shifts 07:00–15:00 and 15:00–23:00, no "loaders work mornings only, 07:00–15:00", no "5 shifts a week, 2 days off". The solver then allowed up to three shifts a day, so many people got 07:00–15:00 and 15:00–23:00 on one day — 16 hours — and met their contract in three long days; loaders worked evenings; managers stood on the sales floor while the manager-on-duty hours stayed open; and the week was so loose that custom hours had to be left out to stay under the solver's size limit.

  • Every description handed out counts. The answer that hands the descriptions over lists the store's, each position's (of someone the plan covers) and each person's. A call now has to account for every one: the store's by a rule with no person, a position's by a rule with that position, a person's by a rule or an ignored entry with that person — or name it in a new list, noConstraints ({about: "store"}, {about: "position", position}, {about: "person", personId}, with a short reason), for a description that states nothing any kind expresses. Otherwise nothing is solved and no card is made: the answer is needsConstraints with reason incomplete and the list of what is not accounted for (incomplete: store, positions, people, each with its words), to read and call again in the same turn. The same holds for weighing and deciding a request.
  • A store rule that counts a position is the store's when the store's words say it. "One extra loader" counts loaders, but it comes from the store's description, so it does not account for the position Грузчик's own description. The solver tells them apart by the rule's source: the description it shares more words with, the store's when both read alike. A word match, not a reading.
  • For people, pragmatic. A description with only skills ("experienced, drives the forklift") is accounted for by noConstraints or ignored; it needs no rule.
  • One shift a day by default. A person gets at most one shift a day from the solver. Only where the store's, a position's or a person's description says split shifts are allowed does the agent pass a new kind, splitShifts (count 2 or 3; no person for the store's, position, or the person) — then up to that many a day. Two shifts that touch or overlap, 07:00–15:00 and 15:00–23:00, are one 16-hour day and never allowed, split shifts or not. A shift a plan may not change still counts toward the day. A day the week already has with more shifts keeps one, and the card says why ("one shift a day — no description allows split shifts"): in the demo's Riverside draft, Sara Lind's split Monday (12:00–14:00 and 18:00–20:00) loses 12:00–14:00 when the solver plans that week — checked once, as Daniel, on the demo data in this working copy's database. The schedule editor by hand is unchanged: up to three that do not overlap (Screens).
  • A manager covering the opening hours stands as manager on duty. A manager's usual shifts are offered as manager on duty — and, where the manager's week so far had them on another area, on that area too, for when another manager is on duty already. Custom hours cut for "a manager in the store" (a rule that counts managers anywhere) are manager-on-duty shifts, not open ones that became checkout or floor. Why MyCity's managers were on the floor: with no usual shifts and no templates, the only shapes for that rule were custom hours with no area, which became floor or checkout shifts, while the manager-on-duty custom hours came later against the size limit and were left out.
  • "One extra" of a position is one more than one. An extraStaff that counts a position, in hours where no minStaff of that position applies, now counts on top of one holder, as the store's default rule is one person: "one extra loader on Tuesdays and Fridays 07:00–11:00" is two loaders then. Before, it was one. Our reading, for the owner to review.
  • The agents are told the same (store profile revision 18, owner revision 14): every position's description gives its rules, every time, with the MyCity positions as the example; what incomplete means; noConstraints; one shift a day.

On a copy of the owner's local data, the owner's solve_week for the week of 9 November 2026, as the agent calls it. Each result was checked per person by a script from the shifts themselves, not from the solver's summary.

Week of 9 November 2026What the solver didChecked per personHours against contractsTime
The assistant's own set (the store's and the personal rules only)Planned nothing; incomplete named the four positions and the eight people whose descriptions no rule or entry cited———
The full set, on the draft the assistant had written (98 shifts, with the 16-hour days)Solved: 17 added, 30 removed, 68 kept; 374 possible shifts, none left outOne shift a day each; loaders only 07:00–15:00; two or more loaders 07:00–11:00 on Tuesday and Friday; a manager on duty every minute 07:00–23:00; a cashier on the checkout every minute, two 17:00–21:00 on weekdays; someone on the floor 09:00–21:00; all personal rules; no rest under 11 h; nobody over contractNine people under contract, 6 to 16 hours: the plan changes the draft as little as it can, and a change outweighs hours under contract0.13 s
The full set, from an empty weekSolved: 89 shifts; 374 possible shifts, none left out; 4 with custom hours (Максим's 6-hour days)The same checks, all heldВера 24 of 40, Алина 24 of 30, Дарья and Степан 16 of 20, Максим 24 of 30; everyone else on contract0.2 s

One run each on one laptop. The search is the narrowed one (the week is too big to prove the very best), so Вера's 24 hours are what it found, not a proven limit. Breaks: these positions' shifts have none, so the minute checks count every shift as worked. The owner's draft and data were not changed: the copy is this working copy's own database, and the empty-week run emptied that copy's draft and restored it afterwards.

Rules stored next to the descriptions ​

Added on 10 November 2026, on the owner's request that day. Tested on small cases worked out by hand, through the tools on a store written by the test, and through the tools on a copy of the owner's local data (MyMega and MyCity) with a script standing in for the assistant; not checked with a live agent; not deployed; no manager has used it.

The company assistant could not build a week for MyMega, a store of 110 people (8 managers, 40 cashiers, 42 on the sales floor, 20 loaders), 47 of them with personal rules in their descriptions. Every plan asked it to read every description again and send every rule — and a "nothing here" entry for every other description — in one call. At 20 people it barely managed, and once "only Tuesday and Friday" lost a Saturday; at 110 it gave up. The solver itself plans such a store in about a second (HiGHS), and descriptions change rarely. So the reading is now kept.

  • Each description keeps the rules read from it: the store's, each position's and each person's. They are the constraints of the kinds above, each with the words it came from — or "no rules" with a reason ("only skills") — together with a fingerprint of the text they were read from. Rules whose fingerprint is not the description's current one are stale.
  • The assistant saves them with save_rules, up to 20 descriptions a call. Each entry is checked as a constraint passed to solve_week is — the person and the position known, each kind's fields, the opening hours — and stored, replacing what was there, or sent back with why. A store rule whose words name a position it does not count is saved with a note. The answer lists what was stored and the next descriptions still without fresh rules, with their text.
  • The rights are those of editing the description: the store's, its managers and the owner; a person's, the owner and, for the store's employees, its managers; a position's, the owner only, as the positions are the owner's.
  • No card. The rules come from descriptions the person already confirmed, so saving them asks for no confirmation; they are visible instead — in the app under each description, read only, and on the week's card as before, each with its source. Our choice, for the owner to review.
  • solve_week uses them. Called without constraints, when every description it would consider has fresh rules, it plans with them at once. Otherwise it answers needsRules with only the descriptions without fresh rules — never read, stale, or no longer usable as saved (an opening hour moved, say) — at most 20 at a time, and plans nothing until they are saved. evaluate_request and decide_request do the same.
  • A changed description is read again, never re-derived. The API has no language model: when a description changes, in the app or through a card, its rules only become stale, and the next plan asks the assistant for that one description.
  • A manager cannot save a position's rules. Their needsRules marks a position's as the owner's; only when the manager says outright to build without them does the assistant call again with withoutRules, and the card names what was left out.
  • The hand-passed list stays, as an override for one call, with the checks it had: the fingerprint of the descriptions handed over, every constraint usable, every description accounted for.
  • "Only Tuesday and Friday" has its own kind, onlyDays: the days worked, which the solver reads as no shift on the other five. The assistants are told to use it rather than list five days off, and to check that the days make 7 when they do list days.
  • The agents are told the same (store profile revision 19, owner revision 15): call without constraints, save what needsRules lists in batches, never retype stored rules.

On a copy of the owner's local data, as the owner (team MyDemo), for the week of 16 November 2026. A script played the assistant: it read each description by fixed phrases ("не работает по средам" → noDays Wednesday, "только вторник и пятница" → onlyDays Tuesday and Friday, a skill only → no rules) and called the real tools. Each plan was then checked per person by a script from the shifts themselves, not from the solver's summary.

StoreCallsLargest callLargest answer before the planThe planChecked per person
MyMega, 110 peoplesolve_week → needsRules (20 of 115 listed); 6 × save_rules (115 descriptions: 52 with rules, 63 with "no rules"); solve_week6.2 KB10.1 KB508 shifts in 0.7 s; no hour short on the tills, the store's rules, a manager on duty or someone on shift296 personal and position rules, none broken; one shift a day each; every minStaff rule held in every quarter-hour
MyCity, 20 peoplesolve_week → needsRules (20 of 21; the 4 positions already saved for MyMega, the same company); 2 × save_rules; solve_week4.9 KB7.2 KB92 shifts in 0.18 s; nothing short57 rules, none broken — Степан only on Tuesday and Friday, Дарья only at weekends; one shift a day each

Then one MyMega person's description changed in the app's way (a sentence added): solve_week answered needsRules with that person only, as stale, and 114 fresh; saving it took one call of 0.4 KB. The plan's own answer is large — 188 KB for MyMega, nearly all of it the 508 shifts it adds; that was so before and is not changed here. One run each on one laptop; the owner's data was not touched: the copy is this working copy's own database.

Every opening hour covered ​

Added on 28 October 2026, on the owner's decision that day. Tested on small weeks worked out by hand, on the seeded demo stores and through solve_week on a store written by the test; not checked against a local Agentfy; not deployed; no manager has used it.

Until then the solver covered only what it was told to: the tills from the hourly load, the manager on duty, and the rules in the store's description. A new store with no load and no description asked for nothing, so nothing made an opening hour need anyone. On 28 October 2026 the owner asked for TestStore's next week (open 09:00–18:00, ten people on 40 hours, no load): every shift was the 09:00–17:00 template, and 17:00–18:00 had nobody, every day. Now every store has a default rule: at least one person on shift in every opening hour of every day, with or without a load or a description.

  • Weighed as the tills. An hour with nobody costs as much as a till-hour short (10,000), so it comes before overtime, a changed shift and contract hours. Where someone can take it, they get it, even with overtime, and the overtime is a warning on the card.
  • Counted with the other rules, not added to them. Anyone on a shift counts, the manager on duty too. Where the tills already ask for someone, that is the person. A description's own rule takes the larger count, not the sum: "never fewer than two in the store" asks for two, not three. The default rule is not a constraint read from a description, so the card does not list it among them.
  • Custom hours close the gap. Where no template reaches closing time, the solver sets the hours of a shift itself (custom hours): for an hour short before closing, the shortest custom shift up to closing (14:00–18:00), and since this day also one of the person's usual length (09:30–18:00 for a 40-hour contract with the store's 30-minute break). A template that does the same job is kept.
  • Half an hour is half. In an hour the store is open only partly — closing at 17:30 — the rule asks for someone for that half, since nobody can be there for the rest.
  • What stays empty is said, with why. The plan lists every stretch of opening time with nobody on shift (nobodyOnShift), counted to the minute and with a break whose start is known taken off. The card's warnings say "Fri 17:00–18:00: nobody on shift — Nobody can take it under the rules: Ann has a day off; …", the summary says "Every opening hour has someone on shift." or how many hours stay empty, now and with the plan, and the plan's explanation does the same. The hours also count in the person-hours short of the store's rules (staffShort).
  • Requests too. A change that leaves an opening hour with nobody is weighed like one that leaves the tills short: the solver looks for cover on that day and says "It leaves Fri 09:00–18:00 with nobody on shift" when nobody can take it.
  • Checked on 28 October 2026 on a week made like TestStore's (open 09:00–18:00, ten people on 40 hours, the one 09:00–17:00 template with a 30-minute break, no load, no description), with the solver's own code. Without the rule: every shift 09:00–17:00 and nobody 17:00–18:00 on any day, as the owner saw. With it: someone on shift every hour 09:00–18:00 on all seven days, 7 of the 62 shifts with custom hours ending at 18:00 ("covers 17:00–18:00 short in the store, which no template does"); 210 possible shifts, under 0.1 seconds in the solver's own test; through the tool, on the store built by the test, the same 210 took 2.4 seconds — the search path depends on the order of the people, and the 8-second limit was not reached. But eight of the ten people end 5 hours over their contract and one 2 hours over — 42 hours of overtime where a plan with none exists: the exact search finds no whole plan for this week, and the last resort, diving, only ever rounds a partly given shift up, so 5⅓ shifts each (40 ÷ 7.5) become 6. That was so before the rule too (50 hours on the same week without it) and is not decided. The same week runs as a test, also built through the API's own routes and written into a draft by the card, read back through the schedule's coverage rows.

Custom hours ​

Added on 26 October 2026, on the owner's decision that day. Tested on small weeks worked out by hand, on the seeded demo stores and through solve_week with an in-memory Agentfy; not checked against a local Agentfy; not deployed; no manager has used it.

Until then the solver only chose among ready shapes: the store's templates and the shifts people already work. A stretch no template covered stayed short ("No checkout or open-area template covers 12:00–16:00"), a person whose description allowed only 12:00–18:00 got no new shift when no template fitted that window — a description could only take shapes away, never make one — and a store without templates or history had nothing to build from. Now, where the templates and usual shifts cannot make a fitting shift, the solver sets the hours itself, from the person's description, their usual shifts, their contract, the opening hours and the hours left short.

  • Where it makes them. Per person and per day on which the person may get a new shift, in two cases:
    • Nothing else fits the person that day — every template and usual shift breaks one of their constraints or a rule, or the store has none. Then their window from their description, or the opening hours, cut to their usual length: "fits Maya's 12:00–18:00".
    • A stretch needs more people than the templates and usual shifts could put there at all — on the tills, for the store's own rules, or as manager on duty. Then that stretch, cut to the person's window and lengths: "covers 12:00–16:00 short on checkout, which no template does".
  • Lengths. From 4 to 10 hours, start to end, on whole or half hours; shorter only when the person's own window or hours a day leave less. The usual length is the median of the person's shifts this week and the week before; without any, their contract's weekly hours over 5 days; without a contract, 8 hours. Our choice, for the owner to review.
  • Breaks. Shiffty has no legal break rules, so a shift with custom hours gets the break the store's own templates and shifts give a shift of that length — the break of the longest one that is not longer — with no break start. A 6-hour custom shift in a store whose templates are 8 hours with a 30-minute break gets none.
  • A shift removed for a description is replaced, not just lost. In a week that already has shifts, a new shift costs a change (40) — more than the contract hours one shift brings — so before this a person whose shifts the plan removed for their description simply lost those hours. Since 26 October 2026 a new shift on a day whose shift the plan removes for that person's description takes its place and is not counted as one more change; templates count the same way.
  • Every hard rule holds. Custom hours go through the same rules as any other shape: opening hours, one shift a day (up to 3 with a gap between where split shifts are allowed), days off, 11 h rest, the person's constraints, the manager-on-duty area only for managers.
  • Templates first. A shift with custom hours costs 3. That is above a wish (1.5), so a template is never left for a wish; below the contract hours of the shortest custom shift (4 hours at 1 each), so where nothing else fits they can still fill a contract in a new week; and far below a changed shift (40), an hour of overtime (100) and an hour short (10,000). So a template or usual shift that does the same job is kept, and a stretch only custom hours can close gets closed. We put it below a changed shift on purpose: at 40 or more, custom hours could never fill a contract, and a person like Maya above would still get nothing. Our choice, for the owner to review.
  • The size limit. Custom hours come last against the limit of possible shifts (600 with YALPS; 10,000 with HiGHS since 10 November 2026, above): at most 4 per person and day, none the same as a template or usual shift, and only in the room the templates and usual shifts leave. What does not fit is left out first, and the plan says how many; every template and usual shift is kept.
  • Visible. The plan lists every shift with custom hours and why (customHours); on the card that shift's line ends "Custom hours: fits Maya's 12:00–18:00", and the summary counts them. A request's cover says so too. A stretch still short says why custom hours could not close it either — rest, a day off, a description, hours a day, or the size limit — instead of only "no template covers".
  • Checked on 26 October 2026, through solve_week over the real /mcp with an in-memory Agentfy (the test playing the model), on Riverside Market's seeded draft week as Daniel, with Maya limited to 10:00–15:00 (none of the store's templates — 08:00–16:00, 12:00–20:00, 16:00–22:00 — fits it). Before: the plan removed her four shifts that broke the window and gave her nothing (0 of 32 h), and 43 of 63 till-hours stayed short, each "No checkout or open-area template covers …". After: four shifts 10:00–15:00 in place of the removed ones, each on the card as "Custom hours: fits Maya's 10:00–15:00" (20 of 32 h), and no till-hour short; 455 possible shifts, under a second. The same check is kept as a test.

How it weighs a request ​

  1. A request that does not wait for this manager's decision gets no recommendation.
  2. A change that breaks a hard rule — the API's own request checks, or under 11 h rest — is decline.
  3. A change that leaves the tills or the manager on duty shorter than now, or (since 28 October 2026) an opening hour with nobody on shift: the solver looks for cover on the request's days, adding as few shifts as it can and never moving anyone else's. If the cover closes the gap, approve, naming the cover; if not, decline, naming the hours and why nobody can take them.
  4. Otherwise approve; overtime for the people concerned is noted.

The recommendation is the solver's; the decision is the manager's.

Limits ​

This section is about YALPS, the engine until 10 November 2026 and the fallback since; HiGHS's limits are above.

YALPS is small on purpose. Its authors aim it at small problems: by their own guideline a few thousand variables and constraints at most, and no more than a few hundred whole-number (yes/no) decisions; it keeps the whole problem in memory (YALPS, read 19 October 2026).

What we measured, 19 October 2026, on the seeded demo company on one developer laptop, through the API's tests:

  • A week of a seeded store is 224 to 398 possible shifts — inside the few hundred.
  • YALPS's exact search alone does not finish such a week: on a made-up week of that size (8 people, 4 templates, 224 possible shifts) it found no plan at all in 20 seconds.
  • So the solver searches in steps: first the exact search with a small budget (small weeks finish here, and the plan is then proven the best under the weights; since 26 October 2026 only for weeks of up to 200 possible shifts, below); otherwise the relaxed problem, in which a shift may be "partly given" and which takes milliseconds, and then the exact search over only the few dozen shifts it used; diving — fix the most-given shift, solve again — as a last resort. A plan from the later steps is the best found, not proven the best, and the plan says so.
  • With that, each of the four seeded stores' next and draft weeks took from about 0.2 to 1.1 seconds.

Measured again on 26 October 2026, with custom hours, the same way: a seeded week is now 266 to 594 possible shifts, all under the limit, and no custom hours had to be left out. The first step — the exact search with 24 steps — no longer finished any of them and cost 0.5 to 8.2 seconds, because each of its steps solves the whole relaxed problem, now larger; on Central Station's draft week it alone used up the 8-second limit. So since that day it runs only for weeks of up to 200 possible shifts, and the exact search over the shifts the relaxed problem used has a budget of 512 steps instead of 4,096 (which took up to 6.5 seconds). On all eight weeks this gave the same plans as the full budgets, in 0.2 to 1.9 seconds per call. The same weeks before custom hours: 224 to 398 possible shifts, 0.2 to 1.1 seconds.

Does a real store's week fit? With YALPS a small store's does: the demo stores are 7 to 14 people with 3 or 4 templates. A store with many more people or templates passes the guard of 600 possible shifts, and YALPS then refuses with that reason instead of trying; a MyCity-like store of 50 people is already 1,180. With HiGHS, stores of 50 to 120 people like MyCity are planned in about a second, and 360 in about three (above). Test with 21 people, no templates and its positions' usual shifts is 461 (above); each template adds one possible shift per person and day (two where it has no area and the floor needs people), so a few templates on top of that would pass 600 — not measured. Those are our demo data, not a customer's; what a real store needs is not known.

What the demo data shows. Until 26 October 2026 the seeded weeks kept many till-hours short with or without the plan — 14 to 73 per week with it — because the demo stores have no checkout template or checkout habit for many hours (Riverside Market's mornings, for example). With custom hours the plan covers every till-hour of all eight weeks. It changes them a lot to do so — up to 24 shifts added and 20 removed at Central Station — mostly moving people from sales-floor shifts, which no rule in the demo asks for, to checkout shifts with custom hours. That follows the order the owner set (the tills first); whether a plan should move that much is for the owner (below).

Is the solver an MCP server? ​

No. The solver is a part of the API. MCP — an open standard for connecting AI applications to external systems — is how Shiffty hands its tools to an agent, and whether that is needed depends on where the agent runs:

Where the agent runsMCP?
Inside our own APINot needed. The tools are described directly in the call to the model: fewer moving parts, simpler permissions, simpler separation between stores.
On an external agent platform — Agentfy. Chosen on 5 October 2026.Yes. It is the standard way to connect our tools to such a platform.
In a manager’s own AI assistantYes. The assistant connects to our tools the same way.

Settled on 5 October 2026: the agent runs on Agentfy, so the tools reach it through one MCP server that Shiffty runs; the solver sits behind that server. The platform limits how long a tool call may take. On 2 October we expected that building a schedule would therefore be started from the application; the measured solve times above make a tool call possible for the demo stores, and the solver's own limit is 10 seconds since 5 November 2026, within the 15 seconds Agentfy gives a tool call (above).

Which solver ​

Since 10 November 2026: HiGHS, on the owner's request that day, built the same day (above); YALPS stays as the fallback (SOLVER_ENGINE=yalps). Whether HiGHS replaces YALPS for good is for the owner to accept (Not decided).

For the first version: YALPS — decided on 2 October 2026, built on 19 October 2026. YALPS is a linear and mixed-integer solver written in TypeScript; it runs inside the NestJS API, so the first version needs no second language and no separate service. Checked on 19 October 2026: the npm package yalps, version 0.6.4 (24 December 2025), MIT licence; the repository has moved from Ivordir/YALPS to IanManske/YALPS; its authors maintain it for fixes and security updates only, with no new features planned.

Behind an interface. The schedule is written as a neutral mixed-integer program; YALPS and, since 10 November 2026, HiGHS (as highs-js, in Node) are two adapters for it. Google OR-Tools (as a separate service) could take their place without changing the model, the tools or the app.

Later, most likely: Google OR-Tools, run as a small separate service in its own pod next to the API in Kubernetes. This is an intention, not a decision; it depends on what the first version shows. OR-Tools officially supports C++, Python, Java and C#, not Node, which is why it would be a separate service.

Also looked at on 2 October 2026, and not chosen for the first version:

LibraryWhat it isWhy not now
highs-jsThe HiGHS solver compiled to WebAssembly; runs inside NodeThe closest alternative if YALPS proves too small before OR-Tools is in place. Taken on 10 November 2026 (above)
MiniZinc for JavaScriptA constraint language with several solversIn Node it needs MiniZinc installed on the server
OptalCPA scheduling solver with a TypeScript APIProprietary; the free preview does not return the schedule itself

We have used YALPS and HiGHS only as described here, and none of the others. Everything about them comes from their own pages.

What it would cost us ​

  • A second language, later. YALPS and highs-js keep the solver inside the TypeScript API (HiGHS as WebAssembly, 3.5 MB). OR-Tools would bring a second language and one more service to run and deploy.
  • An explanation does not come for free. A solver reports that a week is short, not why. The first version explains by looking at the shifts it could not give and the rule that took each out; it does not prove that nothing else would help.
  • Traffic has to become a headcount first. The first version uses the day view's tills needed, from receipts per hour; how traffic should turn into headcount in general is still open.

Not decided ​

  • A position's rule against contracts. The store's rules weigh above overtime, so where a position's people cannot cover its rule within their contract hours the plan adds overtime; the owner's own week on Test leaves those hours short instead. Whether such a rule should give way to contract hours is for the owner.
  • The 11 h rest rule as a hard rule in the solver, while by hand it stays a warning. Proposed on 19 October 2026; the owner to confirm.
  • The weights above, and the order of their tiers in practice.
  • A shortest shift. Shiffty has no minimum shift length, so the solver may cover a gap with a short shift a person already works: in the live check it gave one person several 2-hour checkout shifts at midday. A minimum length, or a cost per shift, would prevent it. Custom hours (26 October 2026) are at least 4 hours unless the person's own hours leave less; templates and usual shifts still have no minimum.
  • How much a plan may move a week. With custom hours the seeded weeks' plans move many sales-floor shifts to the tills (up to 20 removed and 24 added in one week), because the tills come first and nothing in the demo asks for people on the floor. A store that needs the floor says so in its description (minStaff); whether the solver should also hold back without one is for the owner.
  • Diving rounds only up, so where a contract is not a whole number of shifts everyone can get one shift too many: 42 hours of overtime on the TestStore-like week above (50 without the default rule). A first fix — rounding to the nearer side — was tried on 28 October 2026 and taken back: on empty seeded weeks it needed many more rounds, ran past the time limit and left them without a plan. For a stronger search, or a separate change.
  • Days a week on a large store (added 30 October 2026): with maxDaysPerWeek for everyone, Central Station's next week took 172 seconds against 0.9 without it; the same unbounded diving as below. Measured again on 10 November 2026 with today's code: 0.05–0.12 s with YALPS, 0.08–1.9 s with HiGHS (above).
  • The spread (added 30 October 2026): fair shares from contract hours over the opening hours (or the receipts), and the spread as the last tier, so it never trades anything above it — where the week already has shifts, or a change would cost more, people stay where they are. Whether it should weigh more, against a changed shift for example, is for the owner.
  • Usual shifts (added 30 October 2026): the kind's name and shape, and the area a usual shift takes when the rule names none (from the person's and their position's past shifts). For the owner.
  • "One shift a day", which Test's positions also state, has no kind of its own: "at most 8 hours a day" stands in for it where every shift is 8 hours. For the owner.
  • Weeks not made yet are slow. Measured on 29 October 2026 on the seeded stores, a week with no shifts yet: 1.2 to 7.9 seconds before the default rule, 1.7 to 13.1 with it — and Central Station's, 560 to 596 possible shifts, 203 and 361 seconds before, 326 and 572 with it, during which the API answers nobody else (YALPS runs in the API's own process). The 8-second limit did not hold there: it bounds YALPS's exact search, not the relaxed solves the later steps repeat. Known before the rule; worse with it. Since 5 November 2026 the dive keeps to the budget (above). Measured again on 10 November 2026 with today's code: 0.06–0.3 s with YALPS; with HiGHS 0.09–5.1 s, and Central Station's 7.5 s, stopped at the limit with its plan already as good as the best one (above).
  • Every opening hour covered (added 28 October 2026): that the default is one person anywhere (not, say, one on checkout), that it weighs as much as the tills — so it takes overtime before it leaves an hour empty — and whether a store should be able to be closed on some days. For the owner.
  • Custom hours (added 26 October 2026): their lengths (4 to 10 hours), a usual length from a contract over 5 days, the break taken from the store's own templates and shifts, their weight (3 per shift, below a changed shift), and at most 4 per person and day. For the owner.
  • The constraints from descriptions (added 20 October 2026): the weight of a wish (1.5 per shift), where morning ends (12:00), and whether a current shift against a hard constraint should be removed by the plan, as now, or kept with a warning. Whether availability should later become fields of a person's profile instead of a reading of their description. For the owner.
  • A decision through the assistant's card is recorded as the manager's own ("decided by a person"), not as "by the assistant": the manager confirmed it. For the owner to confirm.
  • HiGHS for good (10 November 2026): whether it replaces YALPS, which stays as the fallback, and whether OR-Tools is still needed. Its time limit, 75 % of the 10 seconds, and its gap, 0.04, are our choice. For the owner.
  • Overtime on a store whose rules ask more than its contracts give: on the made-up 120-person store with 1.6× the rules, 22 people work 232 hours of overtime, because the store's rules weigh above overtime. The same question as a position's rule against contracts, above. For the owner.
  • How traffic turns into required headcount beyond receipts per cashier.
  • Whether the agent-and-solver split works for managers. It is a hypothesis until managers use it on a real store's data.

Where to go next ​