# The 856 is already an aggregation tree

> transactions.dev · by visibility.cloud — 2026-07-31
> Written for the in-house supply-chain platform team — the integration engineer who owns the trading-partner map.

One thesis, and it is structural: **the X12 856's HL hierarchy is the same
shape as an EPCIS aggregation tree, so compiling an ASN into
AggregationEvents is a projection, not an interpretation.** You are not
inventing events from a document. You are reading a tree that was always
there.

## The shape that was always there

An 856 — the Advance Ship Notice — is built around the HL loop: a runtime
parent-pointer hierarchy in which each HL segment names its own id, its
parent's id, and a level code. In retail practice the levels run Shipment →
Order → Tare → Pack → Item. The pallet (Tare) and case (Pack) levels carry
SSCCs in their MAN segments; the Item level carries the GTIN in LIN and the
quantity in SN1. We will not reproduce the segment dictionary here — X12's
element detail is licensed content, and the canonical public reference is
[Stedi's 856 page](https://www.stedi.com/edi/x12-008010/856), which we link
rather than copy, deliberately.

Now put that beside EPCIS 2.0. An AggregationEvent says: this parent
identifier (an SSCC), these children (EPCs or quantity elements), aggregated
at this time, at this place, for this business step. A shipment's physical
packing — pallet contains cases, case contains eaches — is exactly the HL
tree with the paperwork removed.

So the compile is mechanical:

- Shipment HL → one ObjectEvent, `bizStep` shipping, `disposition`
  in_transit.
- Each Tare or Pack HL carrying an SSCC → one AggregationEvent whose
  `parentID` is that SSCC and whose children are the child HLs' identifiers.
- Each Item HL → `childEPCs` when serials are present, `childQuantityList`
  keyed by GTIN and lot when they are not.

`transactions.dev join` performs that projection and nothing else. No
heuristics, no vendor schema: the output validates against the pinned
official EPCIS 2.0 JSON schema before it leaves the tool — the same pinned
artifacts [epcis.dev](https://epcis.dev/conformance/) publishes digests for.
Superset where the document carries more than the standard asks, and
conformant always: the projection validates, or the verb exits nonzero.

## The proof artifact

Here is the worked example, end to end. One pallet, one case level, one
line item, from a synthetic ASN in our own corpus (self-authored fixtures —
the corpus never republishes a partner's guide):

```
$ npx transactions.dev join 856-ship-notice-01.edi --json
```

The second event out — the case-level AggregationEvent, verbatim:

```json
{
  "type": "AggregationEvent",
  "eventTime": "2026-07-30T18:00:00.000Z",
  "eventTimeZoneOffset": "+00:00",
  "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 }
  ],
  "bizTransactionList": [
    {
      "type": "desadv",
      "bizTransaction": "urn:epcglobal:cbv:bt:0614141000012:SHIP20260730001"
    }
  ]
}
```

Read the joins off the fixture. The SSCC from the MAN segment became
`parentID`. The serialized line's `REF*SE` values became `childEPCs` at
SGTIN grain; the lot-only line's LIN GTIN and SN1 quantity became a
`childQuantityList` entry at LGTIN grain — serials win when they exist,
because that is the finest grain the document carries. The BSN02 shipment
identifier became a `bizTransactionList` entry typed `desadv` — CBV 2.0 §7.3
defines it, and its own words are: *"Also called an 'Advanced Shipment
Notice,' but the value desadv is always used regardless of local
nomenclature."* The purchase-order reference and the N1 loops' GLNs are on
the shipment `ObjectEvent` above this one, as `po` and `bol` entries and as
`sourceList`/`destinationList` per CBV §7.4: an order HL is a
reference-scoping node, not a physical grouping, so it emits no event of its
own and its reference rides the shipment. Every field the compile writes is
core-spec machinery. No extension namespace appears in that document.

Identifiers are GS1 Digital Link URIs rather than EPC URNs, and that is a
ruling rather than a taste: the URN forms need a GS1 Company Prefix length,
which a bare GTIN-14 or SSCC-18 on an EDI wire simply does not carry. Digital
Link needs no such split, and the pinned schema takes any URI at every one of
these join points. Minting a prefix boundary we cannot read would be
fabricating a fact about the sender's company prefix in order to make an
identifier look more canonical.

## Why this lands on your desk

You are the platform team. Somewhere upstream of you there is an EDI
provider, or a mapping tool, or a person who left; downstream there is a 2.0
mandate, an FSMA-shaped ask, or a partner who has started sending Digital
Link URIs. The trap is treating those as two projects — "the EDI
integration" and "the EPCIS integration" — staffed and budgeted twice.

The tree argument collapses them into one. The ASN you already receive is
the aggregation structure the event mandate wants; what was missing was a
compiler with a conformance gate on the exit. That is the whole scope of the
`join` verb: documents in, events out, validated against the pinned schema,
refused loudly when the document's tree is malformed (an HL that names a
missing parent is a typed error, not a guess).

What `join` is not: an EDI network, a VAN, a mapping consultancy, or a place
your partner relationships live. Your 850s and 856s keep flowing exactly as
they do today. The compile is a read-side projection over documents you
already possess — which is also why it composes with the capture interface at
[epcis.dev](https://epcis.dev/business-transactions-in-epcis/) instead of
competing with it: the events `join` emits are the events it accepts.

## Where the difference shows up

Run the compile against last month's ASNs and you have an event history you
never captured: every pallet's claimed contents, timestamped, party-attributed,
queryable by SSCC. Then start comparing it with what your scanners actually
saw — that is the [reconcile verb](/blog/reconcile-is-a-verdict/), and it is
where the executive story on
[visibility.cloud](https://visibility.cloud) begins. The barcode side of
that same join — what the scan resolves to before it becomes an event — is
[barcoding.dev](https://barcoding.dev)'s layer.

The 856 was never a flat file. It was a tree wearing one.

---
Access: https://transactions.dev/get-access/ · All posts: https://transactions.dev/blog/ · Machine face: https://transactions.dev/llms.txt
