---
title: "Tutorial Extension | Repeated Additions to a Silo"
id: "tutorial-extension-repeated-additions-to-a-silo"
project: "Tutorials"
url: "https://docs.sepasoft.com/articles/mes-module-tutorials/tutorial-extension-repeated-additions-to-a-silo"
updated: 2026-08-04T16:03:49Z
---

# Tutorial Extension \| Repeated Additions to a Silo

* * *
 
## T&T Tutorial Extension — Repeated Additions to a Silo (Blending)

Extends the [Track and Trace Practical Tutorial](/articles/mes-module-tutorials/tt-practical-tutorial) (the coffee roasting scenario). That tutorial receives a bag of beans into Silo 1 once and moves on. Real silos get topped up repeatedly, and each top-up mixes new beans with whatever is already in there. This extension covers how to record that correctly with [system.mes.trace.recordOperation()](/smart/project-imported-manual/system-mes-trace-recordoperation-userid-name-begindatetime-enddatetime-equipmentpath-materialin-mat).

| ![Success](/resources/Storage/mes-module-tutorials/Success.png) | **Prerequisite:** complete the base tutorial's Configure Equipment and Configure Material steps. This extension reuses Receiving Dock, Storage Zone\Silo 1, Roaster 1, and the Raw Colombian Beans material definition. Everything here is version 3.81.8 RC1 or greater, same as the base tutorial. |
| --- | --- |

* * *

### The scenario and why it needs its own operation

**Physically:** a truck delivers raw beans to the dock. A conveyor moves that delivery into Silo 1, which already contains beans from earlier deliveries. The moment the new beans hit the silo, the two are one homogeneous mass — nobody can separate them again.

**Materially:** that is a transformation, not a movement. Two distinct material lots go in, one new material lot comes out, and the two input lots cease to exist as separately identifiable material.

So it is one operation:

| **Operation Name** | **Add Beans To Silo** |
| --- | --- |
| Description | Combine an incoming delivery lot with the silo's current contents into a single new blended lot. |
| Equipment | Storage Zone\Silo 1 |
| Personnel | N/A |
| Tooling | N/A |

| **Material** | **In/Out** | **Material Type** | **Location** | **Lot Name** | **Qty** | **Status** |
| --- | --- | --- | --- | --- | --- | --- |
| Silo current contents | In | Raw Colombian Beans | Silo 1 | Existing silo lot | Entire available quantity | N/A |
| Incoming delivery | In | Raw Colombian Beans | Receiving Dock | Delivery lot from label scan | Entire available quantity | N/A |
| Blended silo contents | Out | Raw Colombian Beans | Silo 1 | Silo lot, next sequence | Sum of the two inputs | N/A |

The two bolded cells are the whole design. Both inputs are consumed in full, so the output quantity equals the sum of the inputs, and nothing is left behind claiming to still be in the silo.
 
### Add it to the Material Process Flow Diagram

The base tutorial insists on mapping the process flow before scripting, and this is exactly why. Adding this row changes the flow from:

 
```markdown
Receive Coffee ──> [Silo 1] ──> Roast Coffee ──> Package Coffee
```

to:
 
```markdown
Receive Coffee ──> [Receiving Dock] ──┐
                                      ├──> Add Beans To Silo ──> [Silo 1] ──> Roast Coffee ──> ...
      [Silo 1 current contents] ──────┘                              │
                                      ▲                              │
                                      └──────────────────────────────┘
                                        (silo feeds its own next blend)

```


| ![Note](/resources/Storage/mes-module-tutorials/Note.png) | **Note the loop:** the silo's output feeds the silo's next input. That loop is the source of every mistake in this pattern, and section 5 is about keeping it safe. |
| --- | --- |

* * *

##  Lot numbers, lot instances, and lot sequence numbers 

Before writing any script, get this model straight, because the scripts only make sense in terms of it.

**A lot number is an identifier that travels through the plant. A Material Lot object is one instance — one snapshot — of that identifier at one point in the process.** Every time an operation produces a lot number that already exists, Track & Trace creates a new Material Lot instance and increments its **LotSequence** (see the [Material Lot](/smart/project-imported-manual/material-lot) object reference: "The sequence property is incremented every time a new Material Lot is created for a given lot number. This makes it unique as an unchanging lot number flows through a manufacturing facility.").

For our silo, lot number **SILO1-RCB** is "whatever is in Silo 1." Its instances are the successive snapshots:

| **Instance** | **Produced by** | **Composition** | **Available after** |
| --- | --- | --- | --- |
| **SILO1-RCB** seq 1 | Add Beans To Silo #1 | delivery **LINDO-2601** (2000 lb) | 2000 lb |
| **SILO1-RCB** seq 2 | Add Beans To Silo #2 | seq 1 (2000 lb) + **LINDO-2602** (1500 lb) | 3500 lb |
| **SILO1-RCB** seq 3 | Add Beans To Silo #3 | seq 2 (3500 lb) + **LINDO-2603** (1000 lb) | 4500 lb |

Each row is a genuinely different material — seq 2 is not seq 1 with more beans, it is a new mixture with its own composition and its own genealogy. Anyone asking "what was in the silo on the 14th?" is asking about a specific sequence number, and the trace graph can answer it exactly.
 
### The invariant: one live sequence at a time

**At any given moment, exactly one sequence number of a given lot number may have available quantity greater than zero.**

Two sequences of **SILO1-RCB** sitting in inventory at the same time is a corrupt state. Avoid it at all costs. When it happens:

* **Inventory is wrong**. **getInventory()** reports seq 2 (3500 lb) and seq 3 (4500 lb) = 8000 lb in a silo that physically holds 4500 lb.
* **Consumption becomes a coin flip.** Downstream scripts that do result['lots'][0] — including the base tutorial's Roast Coffee script — pick whichever row comes back first. Half your roasts trace to a stale snapshot.
* **Genealogy forks and never rejoins.** The trace graph shows two live branches of the same lot number, and a recall query on SILO1-RCB cannot tell you which finished goods came from which physical beans.
* **It is very hard to unwind after the fact.** By the time it is noticed, downstream lots have already consumed from the wrong instance, and those operations have to be found and corrected one at a time.

The one and only defense is the rule from section 1: **consume the previous instance completely in the same operation that produces the next one.** The old snapshot goes to zero available in the same transaction that creates the new one, so there is never a window where both are live.
 
### What does and does not create a new sequence

Worth being precise, because this trips people up:

* Producing lot number X when X already exists → **** **new instance, sequence increment**s. This is the blend operation.
* Consuming part of X → **no new instance**. Roasting 500 lb out of SILO1-RCB seq 3 leaves seq 3 with 4000 lb available. Still one live sequence, still correct. Partial draws are fine; partial blends are not.
* Producing a brand-new lot number from X → **no new instance of X**. Roasting SILO1-RCB into DRC-2601 creates sequence 1 of DRC-2601 and leaves SILO1-RCB's sequence alone.

* * *

## Step 1 — Receive a delivery to the dock

The base tutorial's Receive Raw Coffee script writes beans straight into Silo 1. Retarget it at the dock so that arriving and blending are two separate, individually traceable events.

Create a view (or reuse the receiving view from the base tutorial) and put this in a button's **onActionPerformed** event. As in the base tutorial, values are hard-coded for clarity; in production these come from a label scan, scale, or tag.

**Receive Raw Beans To Dock**
 
```python
##Delivery identity: in production this comes off the shipping label / scale.
now = system.date.now()
deliveryLot = 'LINDO-2601'
deliveryQty = 2000.0
material = 'Raw Colombian Beans'
dockPath = 'New Enterprise\Site\Area\Receiving Dock'

opId = 'ReceiveBeans %s %s' % (deliveryLot, str(now))

##The delivery arrives as a brand-new lot at the dock -> autoCreateLot = True.
matOut = system.mes.trace.configureMaterial(
    userid = 'Mat Out 1',
    name = 'Mat Out 1',
    lotNo = deliveryLot,
    materialName = material,
    quantity = deliveryQty,
    UOM = 'lb',
    equipmentPath = dockPath,
    autoCreateLot = True)

system.mes.trace.recordOperation(
    userid = opId,
    name = 'Receive Raw Beans',
    beginDateTime = now,
    endDateTime = now,
    equipmentPath = dockPath,
    materialOut = [matOut])

system.perspective.print('Received %s (%s lb) to dock' % (deliveryLot, deliveryQty))
```


Give the button an input field for **deliveryLot** and **deliveryQty** if you want to fire it repeatedly without editing script — you will run it at least three times below.

* * *

## Step 2 — Add the delivery to the silo

This is the core script. It is longer than the base tutorial's scripts because it validates before it writes. That validation is the difference between a traceable silo and a corrupt one, so do not trim it out.

Add a second button to the view with this in onActionPerformed.

**Add Beans To Silo**
 
```python
##---------------------------------------------------------------------------
##Configuration.  In production, deliveryLot comes from the operator's scan.
##---------------------------------------------------------------------------
now         = system.date.now()
material    = 'Raw Colombian Beans'
dockPath    = 'New Enterprise\Site\Area\Receiving Dock'
siloPath    = 'New Enterprise\Site\Area\Storage Zone\Silo 1'
siloLotNo   = 'SILO1-RCB'          ##The silo's persistent lot number.
deliveryLot = 'LINDO-2601'

opId = 'AddToSilo1 %s %s' % (deliveryLot, str(now))

##---------------------------------------------------------------------------
##Input A - the delivery waiting on the dock.  Consume ALL of it.
##---------------------------------------------------------------------------
dockInv = system.mes.trace.getInventory(equipmentPath = dockPath,
                                        lotNumber     = deliveryLot)
dockLots = [l for l in dockInv['lots'] if l['AvailableQuantity'] > 0]

if len(dockLots) != 1:
    raise ValueError('Expected exactly 1 available lot %s at the dock, found %d.'
                     % (deliveryLot, len(dockLots)))

delivery    = dockLots[0]
deliveryQty = delivery['AvailableQuantity']
deliverySeq = delivery['LotSequence']

##---------------------------------------------------------------------------
##Input B - whatever is currently in the silo.  Consume ALL of it.
##The silo is legitimately empty on the very first delivery, so this is
##optional - but MORE THAN ONE available instance is always an error.
##---------------------------------------------------------------------------
siloInv = system.mes.trace.getInventory(equipmentPath = siloPath,
                                        lotNumber     = siloLotNo)
siloLots = [l for l in siloInv['lots'] if l['AvailableQuantity'] > 0]

if len(siloLots) > 1:
    seqs = [(l['LotSequence'], l['AvailableQuantity']) for l in siloLots]
    raise ValueError(
        'Silo 1 holds %d live instances of %s %s. A lot number must never '
        'have two available sequence numbers at once - fix the source data '
        'before blending.' % (len(siloLots), siloLotNo, seqs))

##---------------------------------------------------------------------------
##Build the material-in list.  lotSequenceNumber PINS the exact snapshot we
##intend to consume - never let the platform guess which instance we meant.
##autoCreateLot = False: these lots must already exist.  If a typo means they
##do not, we want a failure, not a phantom lot conjured out of nothing.
##---------------------------------------------------------------------------
matIn = []

matIn.append(system.mes.trace.configureMaterial(
    userid             = 'Mat In Delivery',
    name               = 'Mat In Delivery',
    lotNo              = deliveryLot,
    lotSequenceNumber  = deliverySeq,
    materialName       = material,
    quantity           = deliveryQty,
    UOM                = 'lb',
    equipmentPath      = dockPath,
    autoCreateLot      = False))

priorQty = 0.0
if len(siloLots) == 1:
    prior    = siloLots[0]
    priorQty = prior['AvailableQuantity']

    matIn.append(system.mes.trace.configureMaterial(
        userid             = 'Mat In Silo',
        name               = 'Mat In Silo',
        lotNo              = siloLotNo,
        lotSequenceNumber  = prior['LotSequence'],
        materialName       = prior['MaterialName'],
        quantity           = priorQty,        ##ALL of it - this is the rule.
        UOM                = 'lb',
        equipmentPath      = siloPath,
        autoCreateLot      = False))

##---------------------------------------------------------------------------
##Output - the new blended snapshot.  Same lot number, next sequence number.
##Quantity is the SUM of the inputs, so the mass balance closes exactly.
##---------------------------------------------------------------------------
blendedQty = priorQty + deliveryQty

matOut = system.mes.trace.configureMaterial(
    userid        = 'Mat Out Silo',
    name          = 'Mat Out Silo',
    lotNo         = siloLotNo,
    materialName  = material,
    quantity      = blendedQty,
    UOM           = 'lb',
    equipmentPath = siloPath,
    autoCreateLot = True)

##---------------------------------------------------------------------------
##One physical event -> exactly one recordOperation() call.
##---------------------------------------------------------------------------
system.mes.trace.recordOperation(
    userid        = opId,
    name          = 'Add Beans To Silo',
    beginDateTime = now,
    endDateTime   = now,
    equipmentPath = siloPath,
    materialIn    = matIn,
    materialOut   = [matOut])

system.perspective.print('Blended %s lb + %s lb -> %s lb in %s'
                         % (priorQty, deliveryQty, blendedQty, siloLotNo))
```


### Notes on the script

**lotSequenceNumber on the material-in dicts.** configureMaterial() accepts lotSequenceNumber specifically to "*look up an existing lot when multiple instances exist with the same lot number*" — and it is valid for material-in entries only. Once SILO1-RCB has more than one instance in history, an unpinned lotNo='SILO1-RCB' is ambiguous. Pin it from the inventory row you validated. Never trust an implicit lookup on a lot number you know is recycled.

**autoCreateLot=False****on inputs**, True on the output. Inputs must already exist; if a scan typo means they do not, you want the operation to fail loudly rather than silently invent an empty lot and record a blend that never happened. The output genuinely is new material, so it is created.

**No result['lots'][0].** The base tutorial uses that shorthand and it is fine there because each silo holds exactly one lot. Here it would be the bug that hides the corrupt state you are trying to prevent — hence the explicit count checks.

**Both inputs are consumed at their full AvailableQuantity.** This is not an accounting nicety. It is the mechanism that guarantees the old silo snapshot and the delivery lot both hit zero in the same transaction that creates the new snapshot, so the "one live sequence" invariant holds continuously.

**The operation's equipmentPath.** **recordOperation()** documents that the operation's equipment path "must be the same equipment path as the target equipment (where that lot is coming from)". Here the blend physically happens in the silo, so siloPath is correct for the operation, while each material dict carries its own equipmentPath to locate its lot. If a consumption does not resolve in your version, that warning is the first thing to check.
 
###  Run it three times 
 
Fire Receive then Add for each of these, and watch inventory after each pair:

| **#** | **Delivery lot** | **Delivery qty** | **Silo before** | **Silo after** |
| --- | --- | --- | --- | --- |
| **1** | LINDO-2601 | 2000 lb | empty | SILO1-RCB seq 1 — 2000 lb |
| **2** | LINDO-2602 | 1500 lb | seq 1, 2000 lb | SILO1-RCB seq 2 — 3500 lb |
| **3** | LINDO-2603 | 1000 lb | seq 2, 3500 lb | SILO1-RCB seq 3 — 4500 lb |

* * *

## Step 3 — Verify it: the check that matters

Use the base tutorial's Inventory screen, or run this in the script console. Run it after every Add operation, and keep it as a permanent health check in production.

**Silo Lot Integrity Check**
 
```python
siloPath  = 'New Enterprise\Site\Area\Storage Zone\Silo 1'
siloLotNo = 'SILO1-RCB'

inv  = system.mes.trace.getInventory(equipmentPath = siloPath,
                                     lotNumber     = siloLotNo)
live = [l for l in inv['lots'] if l['AvailableQuantity'] > 0]

for l in inv['lots']:
    print 'seq %-3s in %-9s out %-9s available %s' % (
        l['LotSequence'], l['InQuantity'], l['OutQuantity'], l['AvailableQuantity'])

if len(live) == 1:
    print 'OK - one live instance: seq %s, %s lb' % (
        live[0]['LotSequence'], live[0]['AvailableQuantity'])
elif len(live) == 0:
    print 'OK - silo empty'
else:
    print 'CORRUPT - %d live instances of %s' % (len(live), siloLotNo)
```


After three deliveries the expected output is:
 
```markdown
seq 1   in 2000.0   out 2000.0   available 0.0
seq 2   in 3500.0   out 3500.0   available 0.0
seq 3   in 4500.0   out 0.0      available 4500.0
OK - one live instance: seq 3, 4500.0 lb
```

Every superseded instance shows **available 0.0** — fully consumed by the blend that replaced it. Only the newest snapshot holds material. That output is what "done correctly" looks like; anything else is a defect.
 
###  On the Trace Graph 
 
Add an MES Lot Selector and MES Trace Graph to the view (bind the graph's lotUUID to the selector's selectedLotUUID, same as the base tutorial) and select SILO1-RCB. You will see a chain, not a fork:
 
```markdown
LINDO-2601 ─┐
            ├─[Add Beans To Silo]─> SILO1-RCB seq 1 ─┐
            ┘                                        ├─[Add Beans To Silo]─> SILO1-RCB seq 2 ─┐
                             LINDO-2602 ─────────────┘                                        ├─[...]─> seq 3
                                                              LINDO-2603 ──────────────────────┘
```


Select the finished 12 oz bag lot from the base tutorial's packaging step and trace upstream: it now walks back through the roast, through the blend chain, and out to every individual delivery that contributed beans. That is the recall answer the whole exercise exists to produce.
 
###  Then roast from it 
 
The base tutorial's Roast Coffee script works unchanged against the blended silo — with one improvement worth making. Replace its **dict = result['lots'][0]** with the same filter-and-count logic used above, so roasting can never draw from a stale snapshot. Remember from section 2 that a partial draw does not create a new sequence: roasting 500 lb out of seq 3 leaves seq 3 at 4000 lb available, still the single live instance, and the next delivery blends against that.

* * *

## recordOperation() vs modifyOperation() 
 
The base tutorial introduces [modifyOperation()](/smart/project-imported-manual/system-mes-trace-modifyoperation-userid-name-begindatetime-enddatetime-equipment-workorder-material) for the roasting "wrap up" — record the operation when roasting starts, then modify it with final time and quantity when it finishes. That is exactly the right use. It is also the use that gets over-extended into a serious data modelling error, so be precise about the boundary.
 
###  The rule 
 
**recordOperation() records that something happened.modifyOperation() corrects the record of one thing that already happened.**

**One physical event = one recordOperation() call. N physical events = NrecordOperation() calls. Never one call and N-1 modifications.**

**modifyOperation()** is keyed by the operation's **userid**. It reaches into one existing operation and edits it in place. Legitimate reasons to do that:

* **Wrap-up**. Record at start with estimates, modify at end with the actual end time and quantity — the base tutorial's roasting pattern.
* **Correction**. The operator keyed 1500 lb and the scale ticket says 1450. Same event, wrong number, fix the number.
* **Retraction**. A lot was attached to the wrong operation. Remove it with delete on the material entry (and see removeLot for whether the Material Lot object itself is deleted or just unlinked).

Every one of those is the same physical event, described more accurately.
 
### The error to avoid

The tempting shortcut for this silo scenario is: record one long-running "Silo 1 Filling" operation, then each time a truck arrives, call **modifyOperation()** to bump the input quantity and the output quantity.

**This is wrong.** Each delivery is a separate physical event, and modelling N events as one edited-N-times operation breaks things concretely:

1. **The events disappear.** The record ends up with one segment, one begin/end time, one aggregated quantity. There is no way to answer "when did the third delivery go in," "who authorized it," or "which trucks contributed" — because only the final totals survive.

1. **Modify replaces, it does not accumulate.** modifyOperation() sets the material entry to the quantity you pass. Any running total has to be maintained in your own script, outside the MES, with no transactional protection. A failure between reading the old total and writing the new one silently loses a delivery.

1. **The snapshots stop being snapshots.** This is the deepest problem. A Material Lot instance is supposed to be immutable — "here is exactly what this material was at this moment." Repeatedly modifying the operation that produced it means its composition changes after downstream operations have already consumed it. Yesterday's roast traces to a silo lot whose recorded contents now include beans that had not been delivered yet. The genealogy is not merely incomplete, it is retroactively false — which is worse, because it still looks authoritative.

1. **It can spawn the exact corruption section 2 warns about.** Per the modifyOperation() release notes: as of MES 3.81.11 SP7, modifyOperation increments the lot sequence number when the in lot and out lot are the same, or when the out lot already exists. Our blend operation has SILO1-RCB as both input and output. Repeatedly modifying it therefore mints additional sequence numbers of SILO1-RCB as a side effect — multiple live instances of one lot number, arrived at by exactly the route you were trying to avoid.

1. **Recall becomes impossible.** The point of all this is answering "a customer complaint traces to bag lot X — which raw deliveries are implicated, and what else did they touch?" A single aggregated operation collapses every delivery into one undifferentiated blob. You end up recalling everything, or nothing.

###  Quick reference 

| Situation | Function |
| --- | --- |
| A truck delivered beans | recordOperation() |
| Beans were added to the silo | recordOperation() — one call per addition |
| Beans added again an hour later | recordOperation() — a new operation, new userid |
| Roasting started, finish data not known yet | recordOperation() now, modifyOperation() at the end |
| Recorded quantity was keyed wrong | modifyOperation() on that operation |
| Wrong lot attached to an operation | modifyOperation() with delete on that material entry |
| An operation was recorded that never happened | deleteOperation() |

The test that settles every case: **did something new physically happen**, **or did I describe something that already happened incorrectly?** New physical event → **** **recordOperation()**. Bad description → **modifyOperation()**.

* * *

##  Summary 

* A repeated addition to a silo is a transformation: two lots in, one new lot out — not a quantity adjustment.
* Consume both inputs in full in the same operation that creates the blended output. The output quantity equals the sum of the inputs, so the mass balance closes and no stale instance is left holding material.
* A lot number persists through the process; each instance (lot sequence number) is an immutable snapshot of the material at one point in time.
* Never allow two sequence numbers of one lot number to be available at the same time. Validate it after every operation with the check in the Verify It section.
* Pin lotSequenceNumber on material-in entries whenever a lot number is recycled, and never resolve a consumption with result['lots'][0].
* recordOperation() per physical event; modifyOperation() only to correct or complete the record of a single event. Using modifyOperation() in a loop to accumulate repeated consumptions into one static input is an error.