The product moves. So does its paperwork.
On a batch machine, a thing travels through stages — weigh, mix, hold, discharge — and something travels with it. A batch number. A recipe. A measured weight that the next stage needs in order to do its job properly.
Getting the product from stage to stage is mechanical. Getting the data from stage to stage is where control projects quietly turn into a pile of shift registers, index variables and arrays that everyone is afraid to touch.
What usually happens instead
The data conveyor gets built by hand, one index variable at a time.
Without a queue, batch data moves through arrays. Stage 1 writes BatchData[wIdx] and bumps the index. Stage 2 reads BatchData[rIdx] and bumps its own. Somewhere there is a comparison of the two indices standing in for “is there anything waiting”, and somewhere else a wrap-around that was tested once.
It works. It also means every stage now knows how every other stage indexes, the array size is baked into four places, and the day someone inserts a new stage in the middle is a bad day.
None of this is a failure of skill — it is what you build when the platform gives you arrays and nothing else. The cost is not the code, it is the coupling: the stages are no longer separable, so they cannot be tested, timed or commissioned separately either.
A queue you declare rather than build
Element type, capacity, owner, subscriber, and what to do when it is full.
In StateTick a queue is an object in the project, not a pattern you assemble. You define a template — what it carries and how deep it is — and place an instance in the machine that owns it.
What it carries can be a single value, but for batch work it is usually a record: a set of named, typed fields travelling together. That is the batch, moving as one thing, instead of six parallel arrays that must be kept in step.
Direction is part of the declaration, and it is enforced. The machine that owns a queue is the only one that can push to it; the machine bound as subscriber is the only one that can pop. A stage cannot accidentally consume its own output, and the flow through the plant is readable from the bindings alone.
Why the single-reader rule is a feature. One writer and one reader is exactly what lets the queue be lock-free underneath — which matters here, because each stage is a composite running on its own thread at its own period. The queue is the sanctioned way for two independently-clocked machines to hand something over safely.
A stage per composite
Now the pipeline is drawn the way you would sketch it on a whiteboard.
Give each stage its own composite and connect them with queues. Weigh pushes a completed batch record. Mix pops it, does its work, pushes an enriched one. Hold pops that, and so on down the line.
Each stage runs at whatever rate suits it. Each one has a plan you can read on its own. And crucially, each one only knows two things about the outside world: what arrives, and what it hands on.
Popping in a state is a single action — take the front item into a variable the stage can use. Pushing is the mirror of it. The state plan reads as the sequence you would describe out loud, because the plumbing is not in it.
What each stage knows
- The shape of the record it receives.
- The shape of the record it sends.
- Its own equipment and its own timing.
What it no longer knows
- How many slots the buffer has.
- Where the other stage's read index is.
- How fast the neighbouring stage runs.
What happens when the line backs up
Because it always does, eventually.
A queue’s state is available to the logic as ordinary conditions. A stage can guard on the queue being empty, being full, holding a particular number of items, or having had its overflow policy triggered. So “don’t start another batch while the mixer is backed up” is a guard on a transition, not a counter someone maintains.
The counts are kept too — how many were dropped, how many rejected — which turns “we think we lost a batch last Tuesday” into a number you can look at.
And because a queue is a real object, it shows up in monitoring as one: depth, contents and flags, live, alongside the machines either side of it. When a line stalls, the queue between two stages usually tells you which of them is the problem before either machine does.
Why this shape holds up
Batch lines change. This one changes in one place at a time.
Stages joined by what they hand over, not by shared memory.
Insert a new stage and you add a composite and re-point one binding. Change how long mixing takes and nothing else notices. Test the hold stage on its own by pushing records into its inbound queue — no rig, no upstream, no simulated array.
That is the real return on treating the queue as a first-class object. Not that it saves you writing a ring buffer, though it does. It is that the boundary between two stages becomes a stated, typed contract, so each stage stays something one person can hold in their head and change on a Tuesday afternoon.
Sub-Machines and Machine Modes — the other reason to have several composites: parallel sections, and automatic and manual as separate plans.
Machine Paths, Not Scattered Tags — where the record fields and equipment names come from in the first place.