The launch journal

Lot data lives at birth.

31 July 2026 · the launch journal

Ask a typical traceability integration where the lot's expiry date lives and you get an architecture diagram: the events reference a lot number, the lot number keys into the batch table, the batch table lives in the ERP, and the ERP answers queries for anyone with a service account. Which is to say: the expiry date lives at home, and the events that travel out into the world carry a foreign key into a database the rest of the world cannot see.

That design fails on the exact day traceability exists for. A trace-back crosses a company boundary — your co-packer's events, your distributor's events, yours — and every party's lot attributes are locked in a side-table the other parties don't share. The chain of events reconstructs beautifully; the question "which of these lots expires next month" dies at the first boundary, and the answer gets rebuilt over email, which is how a trace that should take minutes takes days.

EPCIS 2.0 has a specific mechanism for exactly this, and it is underused because its name sounds like bureaucracy: ILMD — instance/lot master data. The contract is simple and strict: attributes of a specific lot or instance (as opposed to attributes of the product class) are stamped once, on the event that creates the objects — the commissioning event — and are immutable after. The data travels inside the record, so any holder of the events holds the answers.

The fixture

A lot of 480 units is born on a filling line; the commissioning event carries its identity and its birth certificate in one artifact — validated against the pinned official EPCIS 2.0.1 schema, exit 0:

{ "type": "ObjectEvent",
  "eventTime": "2026-07-31T05:12:00.000Z", "eventTimeZoneOffset": "-06:00",
  "quantityList": [
    { "epcClass": "urn:epc:class:lgtin:0614141.011111.LOT2607A",
      "quantity": 480, "uom": "EA" } ],
  "action": "ADD",
  "bizStep": "commissioning", "disposition": "active",
  "readPoint": { "id": "urn:epc:id:sgln:0614141.00888.line2" },
  "bizLocation": { "id": "urn:epc:id:sgln:0614141.00888.0" },
  "ilmd": {
    "cbvmda:lotNumber": "LOT2607A",
    "cbvmda:productionDate": "2026-07-31",
    "cbvmda:itemExpirationDate": "2026-11-28" } }

Three things to read closely:

  • The identity is lot-grain. urn:epc:class:lgtin:…LOT2607A — an LGTIN, GTIN plus lot — in a quantityList, because these 480 units are interchangeable within the lot and nobody serialized them. Lot grain is the honest grain for most food and many pharma flows; serial grain is a per-unit decision, not a default.
  • The attribute names are vocabulary, not invention. cbvmda: is the CBV master-data namespace (urn:epcglobal:cbv:mda:, mapped in the pinned JSON-LD context), and CBV 2.0 §9.2 defines the trade-item attribute set — lotNumber, productionDate, itemExpirationDate and their siblings, GDSN-aligned. Inventing "expiry_dt" here would be the same dialect mistake as inventing a bizStep: readable by you, opaque to every verifier you'll never meet.
  • action: ADD at commissioning is what licenses ILMD. Writable at object creation, never after — a downstream shipping event that tried to "update" the expiry is mis-modeled by definition. Downstream events reference the lot; they do not re-describe it.

One lot, one story

Downstream, the contract inverts: every later event carries the same LGTIN in its quantity list — the ship event, the receive event, the aggregation onto the pallet via childQuantityList — and none of them repeats the attributes. The join is by identity, and the attributes are wherever the commissioning event is, which is in the same record. A receiver who has never met your ERP runs one query — events for lgtin:…LOT2607A — and holds the full story: born 07-31 on line 2 at Plant 2, expires 11-28, packed, shipped, received. Self-contained is the whole point: the trace answer has no external dependencies, no service account on someone else's ERP, no side-table that didn't travel.

This is also, not incidentally, the shape food-safety regulation keeps converging on. A traceability lot code with its source and dates, carried with the records that reference it — a conformant commissioning event with ILMD already contains the key data elements a sortable-spreadsheet request asks for, at exactly one place in the record. The mark on the case ties in the same way: AI (10) on the label parses to the same lot the events carry — the label-to-record contract barcoding.dev handles on the mark side.

Two boundaries keep the mechanism honest. ILMD is for instance/lot attributes — this lot's dates — never for class attributes like product description, which belong in master data proper (the document header carries those, next in this series). And ILMD is immutable because the lot's birth facts don't change; a wrong entry is corrected the append-only way, by errorDeclaration and a corrective event, never by edit.

The fixture above is yours to check: npx epcis.dev validate against the pinned schema, exit 0, twelve microseconds. Stamp the lot at birth, reference it forever, and the next trace-back that crosses your co-packer's boundary carries its own answers.

Sources: EPCIS 2.0.1 ILMD model (pinned epcis-json-schema.json, ilmd definition; digests at /conformance/); CBV 2.0 §9.2 trade-item master-data attributes; cbvmda namespace mapping in the pinned epcis-context.jsonld; fixture validated with the packaged CLI 2026-07-31, exit 0.

We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.