Skip to main content

Dispatch rules

When a server frees and several jobs are waiting, something decides which goes next. That decision is a dispatch rule, and it is one of the few places where a small change moves the answer a lot.

id: mill
servers: 2
entity_rule:
ranks:
- weights:
- - priority
- 1.0
- weights:
- - setup_cost
- -1.0
- weights:
- - age
- 1.0

Read that as: highest priority first; among equals, avoid a changeover; among those, oldest first.

Rules and ranks​

A rule is a list of ranks. Each rank scores every candidate and orders by that score; a later rank only breaks ties left by the earlier ones.

keytyperequiredwhat it means
ranksarray of ranknoRanks applied in order, the first deciding unless it ties. At most four; a fifth is refused by validate. A list that can only score a constant is refused too. ⚠️ An empty list is not — it is accepted and silently becomes FIFO.
keytyperequiredwhat it means
weightsarray of [feature, number] pairsnoFeature-and-weight pairs summed into one score for this rank. A rank orders by that score; later ranks break ties in earlier ones.

The features you can score on​

valuemeans
"priority"The entity's own priority, from its release. Higher is more urgent.
"age"How long this candidate has waited, in seconds. A positive weight gives FIFO (first in, first out — longest wait goes first); a negative one gives LIFO (last in, first out).
"setup_cost"Seconds of changeover the server under consideration would spend to take this candidate, and zero if it already carries the right recipe. A negative weight avoids setups. ⚠️ A duration, not money — see below.
"slack"Seconds until the due date. Lower is more urgent, and negative means already late.
"remaining_work"Seconds of processing still ahead of this entity on its route.
"critical_ratio"Slack divided by work remaining. ⚠️ Derived, because a ratio is not a weighted sum — see Feature::CriticalRatio.
"quantity"Units the candidate is asking for.
setup_cost is a duration, not money

It is seconds of changeover — the time the server would spend getting ready for this candidate. It has nothing to do with charges, which is the separate mechanism for pricing a finished run.

A negative weight says prefer the candidate needing the shortest changeover, not prefer the cheapest. No feature in this table can see what anything costs: charges are read off a finished run, so they can price an answer but never change one.

It is also the only feature here that depends on which server. Every other one is a property of the candidate alone and scores the same wherever the candidate is considered; this one is scored per candidate and per server, which is what lets a rank keep a tool on the recipe it already carries.

Every feature is in one unit

age, slack, setup_cost and remaining_work are all in seconds, so a weight means the same thing wherever you put it and a rule that looks balanced is balanced.

This was not always true. Until 2026-09-18 age and slack were in ticks while the other two were in seconds, so at the default scale of 1000 ticks a second a weight on age was a thousand times heavier than the same weight on remaining_work — and a rule written by eye was usually pure FIFO without saying so. If you are carrying a rule from before that change, its age and slack weights are now a thousand times too small.

A rank that can only score a constant is refused

A setup-avoidance rank on a resource with no changeovers compares zero with zero at every dispatch. If every rank in a rule is like that, the rule orders nothing and validate refuses it — the whole rule, not the individual rank, because a redundant rank beside a live one is harmless.

Campaigns: a minimum run before changing setup​

keytyperequiredwhat it means
setupstringyesThe setup this minimum applies to, matching a setup named on a step.
lotsintegeryesLots. ⚠️ Zero and one both mean "no constraint"; a campaign of one lot is what happens with no minimum at all.
A campaign minimum can deadlock a plant

If the only source of work in setup B is a lot that has already had setup A, and every server owes a B campaign, nobody runs A, no B work is created, and the work everybody is waiting for cannot arrive. Watch campaign_blocked_pct in the results: it reports the time a server spent refusing work it could have taken.

Changeover costs​

keytyperequiredwhat it means
fromstringyesThe setup being left. An empty string means "from any setup", which is how most datasets express a fixed cost to reach a state.
tostringyesThe setup being entered.
secondsnumberyesSeconds. ⚠️ Asymmetry is normal — A→B and B→A routinely differ, and a model that assumes symmetry gets the setup percentage wrong without ever looking wrong.
An empty from means "from any setup"

Most datasets express a fixed cost to reach a state rather than a matrix of every pair. Reading an empty from as the literal empty string drops those rows silently, because a changeover table is allowed to be sparse and nothing complains.

Types used above​

rank​

One sorting pass over the waiting candidates, expressed as a weighted sum of features.

weights:
- - priority
- 1.0
- - age
- 0.001

Each entry is a [feature, weight] pair. A candidate's score is the sum of feature x weight, and the highest score goes first. A negative weight reverses the ordering: a positive weight on age is FIFO, a negative one is LIFO.

Ranks are applied in order, and each later rank only breaks ties the earlier ones left. That is why they are a list rather than one big weighted sum — real plant rules are lexicographic ("hot lots first, then whatever avoids a changeover, then oldest"), and a single sum cannot express that without weights that are impossible to choose.

Check the magnitudes anyway

Every feature is in seconds, so the units no longer fight you — but the scales still differ. [["priority", 0.7], ["age", 0.013], ["remaining_work", -0.4]] on a plant whose steps take hours puts an age term of 47 against a priority term near 1, and that is pure FIFO with extra steps. Multiply each weight by the magnitude the feature actually reaches on your model before trusting a rule you wrote by eye.