StateTick Docs Home →
StateTick
Passing work between machines
Batch flow

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.

01

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.

Coming from a PLC

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.

02

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.

Queue template — BatchQ CARRIES Record { Id : Int Recipe : String NetKg : Double } CAPACITY 8 slots WHEN FULL RejectPush Instance in the plant Weigh owner — pushes Mix subscriber — pops THE SLOTS A B C pops from the front pushes at the back
One definition covers the type, the depth, the direction and the failure behaviour. There is no index variable anywhere, because there is nothing left for one to do.

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.

03

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.

A BATCH LINE, FOUR STAGES Weigh 20 ms Dose → Settle → Record Mix 50 ms Start → Blend → Sample Hold 100 ms Heat → Soak → Verify Discharge 20 ms Open → Empty → Report WeighQ MixQ HoldQ WHAT TRAVELS — ONE RECORD, GAINING FIELDS Id: 4417 Recipe: 'B2' NetKg: 24.06 Id: 4417 NetKg: 24.06 MixSec: 180 Id: 4417 MixSec: 180 PeakC: 72.4 Id: 4417 PeakC: 72.4 Done: true
The batch record is the unit of work. Four machines at four different rates, and the only thing crossing each boundary is one typed record.

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.
04

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.

Queue is full 8 / 8 — push arrives DropOldest The front item is discarded to make room. Use where the newest reading is the only one that matters. RejectPush Nothing is lost and nothing is added. The sending stage sees the queue is full and waits. The batch default. Error A flag is raised and the queue shows red. Use where a full queue means something upstream is already wrong.
The policy is chosen when the queue is defined, so the behaviour under load is a decision on the record rather than whatever the code happened to do.

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.

05

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.

Also worth reading

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.