The blog  ·  2026-07-31

Shipment, Order, Pack, Item: where the HL mapping stops being derivation

Written for ED-1 — the supply-chain ISV / traceability-vendor engineering org — the integration engineer who reads HL loops with a pencil and wants the tree, not the folklore.

One thesis: almost every step from HL loop to AggregationEvent is a derivation an engineer can check against the file, and exactly one step is a ruling — tare-versus-pack SSCC nesting — so what a governed pipeline owes you is not cleverness but bookkeeping: the ruling named, pinned, and changed only by journal. That the 856's parent-pointer hierarchy is structurally an EPCIS aggregation tree is this site's standing premise, argued as the projection claim in the aggregation-tree post; this page takes it as read and walks the mechanics to the one place derivation runs out.

HL mechanics, in original prose

The HL segment is a linked list pretending to be a hierarchy. Three elements do the work: HL01 is the node's own sequence number, HL02 points at the parent's HL01 (empty for the root), and HL03 declares what kind of node this is — shipment, order, tare, pack, item. The nesting is not expressed by position or indentation; it is expressed by those parent pointers, which means the hierarchy is a runtime structure you build, not a grammar you match. (Element-level X12 detail belongs to the licensed reference — the segment authority for the 856 is Stedi's 856 page, linked here and never republished.)

A synthetic, license-clean fragment from the golden join corpus — 856-ship-notice-01.edi, annotated:

HL*1**S~                          node 1, no parent — Shipment
HL*2*1*O~                         node 2, parent 1  — Order
PRF*PO-8812~                        the order's PO reference
HL*3*2*P~                         node 3, parent 2  — Pack
MAN*GM*006141411234567890~          the pack's SSCC
HL*4*3*I~                         node 4, parent 3  — Item
LIN**UP*614141123452*LT*L42~        GTIN (UPC form) + lot
SN1**12*EA~                         quantity 12 each
HL*5*3*I~                         node 5, parent 3  — Item
LIN**UP*614141765430~               GTIN, serialized below
SN1**2*EA~
REF*SE*1001~
REF*SE*1002~

Read the pointers and the tree is: a shipment containing an order line context, containing one pack (an SSCC-labeled unit), containing two item nodes. A deeper document adds a T (tare) level between order and pack — a pallet of cases — and nothing about the reading changes: the pointers still define the tree, and every SSCC-bearing node is still one aggregation.

The isomorphism, level by level

EPCIS aggregation speaks parent and children: an AggregationEvent asserts that a parent identifier (typically an SSCC) contains child EPCs or child quantity classes. The 856's levels map onto that grain directly:

  • S (shipment) — the top handling context; compiles to the shipping ObjectEvent that carries the document's references.
  • O (order) — not a physical node at all; contributes its PRF as a po entry on bizTransactionList and emits no event.
  • T (tare) / P (pack) — physical packaging nodes; each SSCC-bearing node compiles to one AggregationEvent with that SSCC as parentID.
  • I (item) — the leaves; they emit no events and resolve to childEPCs (serialized) or childQuantityList (lot/class grain) on the nearest ancestor's event.

And the compiled output for the fragment above, from the corpus fixture's expected EPCIS document:

{
  "type": "AggregationEvent",
  "action": "ADD",
  "bizStep": "packing",
  "disposition": "in_progress",
  "parentID": "https://id.gs1.org/00/006141411234567890",
  "childEPCs": [
    "https://id.gs1.org/01/00614141765430/21/1001",
    "https://id.gs1.org/01/00614141765430/21/1002"
  ],
  "childQuantityList": [
    { "epcClass": "https://id.gs1.org/01/00614141123452/10/L42",
      "quantity": 12 }
  ]
}

Nothing in that event was invented. The parent came from MAN, the serial children from LIN plus its serial references, the class child from LIN's lot qualifier plus SN1's count. The costume changed; the tree did not.

Where the identifiers live

Three segments carry the GS1 identity the tree hangs on — the identifier grammar itself is barcoding.dev's layer:

  • MAN, with the GM qualifier, carries the SSCC-18 that becomes parentID (as a canonical GS1 Digital Link URI, AI 00).
  • LIN qualifiers UP / EN / UK carry UPC-12, EAN-13, or GTIN-14; all normalize to GTIN-14 before entering an EPC URI — the join-key post covers the indicator-digit and check-digit traps.
  • SN1 carries the quantity that becomes the childQuantityList entry's count at lot grain.

The one place the mapping is a ruling

Almost everything above is derivation: the tree is in the file, the grain is in the standard. But one decision is not derivable: when both a tare node and its pack children carry SSCCs, does the compile emit one event per packaging node (nested aggregation) or flatten to the top unit? The pipeline's answer — one AggregationEvent per SSCC-bearing node, nesting preserved — is recorded in the pinned join map with a provenance block, and it changes only by journaled human ruling, never silently. A malformed tree (an orphan HL02, a cycle, a level-order violation) compiles to nothing: a typed refusal, because a custody tree with a broken pointer is not evidence, it is a bug wearing evidence's clothes.

That honesty is the difference between a mapping and a heuristic — and it is this page's whole point: every other line above is checkable derivation; this line is governance, and it is labeled as such. The projection argument itself stays with the aggregation-tree post, the canonical statement of the premise. The full run, envelope to validated EPCIS document with the verdicts printed, is the compile post, and what the events make possible at the dock is the reconcile post.

When you have an 856 of your own to hold against this mapping, bring it to the door — the onboarding flow asks which sets are on the wire today and who owns the trading-partner map.


880 words. The access flow is at /get-access/; the family's event door is epcis.dev.