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.
| key | type | required | what it means |
|---|---|---|---|
ranks | array of rank | no | Ranks 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. |
| key | type | required | what it means |
|---|---|---|---|
weights | array of [feature, number] pairs | no | Feature-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
| value | means |
|---|---|
"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 moneyIt 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.
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 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
| key | type | required | what it means |
|---|---|---|---|
setup | string | yes | The setup this minimum applies to, matching a setup named on a step. |
lots | integer | yes | Lots. ⚠️ Zero and one both mean "no constraint"; a campaign of one lot is what happens with no minimum at all. |
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
| key | type | required | what it means |
|---|---|---|---|
from | string | yes | The setup being left. An empty string means "from any setup", which is how most datasets express a fixed cost to reach a state. |
to | string | yes | The setup being entered. |
seconds | number | yes | Seconds. ⚠️ 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. |
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.
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.