# Four event types. One decision tree.

*31 July 2026 · the launch journal*

EPCIS 2.0 gives you five event types, four of which do nearly all the work: `ObjectEvent`, `AggregationEvent`, `TransactionEvent`, `TransformationEvent`. Teams mis-pick constantly, and the root cause is always the same framing error: they ask *"what happened to the thing?"* — a question whose answer is a warehouse anecdote — instead of *"what claim will a reader need to verify later?"* — a question whose answer is a field list. An event is not a diary entry. It is evidence, written for a verifier you will never meet: a trading partner's reconciler, a regulator's query, [your own trace three owners later](/journal/model-the-pallet-before-it-exists/).

Frame it as the claim, and four questions pick the type in under a minute.

## The tree

**Q1 — Does the claim change what exists?** Objects created or destroyed (commissioning serials, decommissioning, destruction) → **ObjectEvent** with `action: ADD` or `DELETE`. Merely *seen* — shipped, received, inspected, counted → **ObjectEvent** with `action: OBSERVE`. ObjectEvent is the default; if no later question fires, you were right the first time.

**Q2 — Does the claim change what contains what?** Packing, palletizing, loading a trailer, unpacking → **AggregationEvent**, parent ID plus children, `ADD`/`DELETE` for the change, `OBSERVE` for a checkpoint. If your instinct was "ObjectEvent on the pallet," ask what the verifier needs: the *relationship*, which only AggregationEvent carries.

**Q3 — Is the claim itself about a business transaction?** Not "this shipment relates to a PO" — every event type carries `bizTransactionList` for context. **TransactionEvent** is for when the *association* is the news: these EPCs are hereby assigned to (or released from) this PO, this invoice, this bill of lading. It asserts a legal linkage, not a physical occurrence.

**Q4 — Do inputs differ from outputs?** Materials consumed, different objects produced — batch mixing, assembly that changes identity, repacking bulk into retail units → **TransformationEvent**, input and output lists linked by a shared `transformationID`. The genealogy question — ["which finished lots contain that input?"](/journal/transformation-event-genealogy/) — is only answerable if this type was used at the moment identity changed.

(The fifth type, `AssociationEvent`, is the deliberate fifth wheel: physical *pairing* rather than containment — a sensor bolted to a reusable tote, an engine into an asset registry. Reach for it when the relationship is "attached to," not "contained in.")

## One validating fixture per answer

Every shape below is a fixture in the packaged corpus (`node_modules/epcis.dev/golden-corpus/valid-standard/`), and each validates against the pinned official EPCIS 2.0.1 schema — seven fixtures, seven `valid` lines, exit `0` each, re-run today:

```
$ npx epcis.dev validate golden-corpus/valid-standard/object-event-shipping.json
valid	schema=epcis-json-schema.json	version=2.0.1
```

**ObjectEvent** (`object-event-shipping.json`) — one SGTIN observed shipping, with the PO riding as context, not as the claim:

```json
{ "type": "ObjectEvent", "action": "OBSERVE",
  "epcList": ["urn:epc:id:sgtin:0614141.107346.2018"],
  "bizStep": "shipping", "disposition": "in_transit",
  "readPoint": { "id": "urn:epc:id:sgln:0614141.07346.1234" },
  "bizTransactionList": [{ "type": "po",
      "bizTransaction": "urn:epc:id:gdti:0614141.06012.1234" }] }
```

**AggregationEvent** (`aggregation-event-packing.json`) — the claim is containment: two SGTINs onto an SSCC, `action: ADD`, `bizStep: packing`.

**TransactionEvent** (`transaction-event-po.json`) — the claim is assignment: two EPCs `ADD`ed to a purchase order, with `sourceList`/`destinationList` naming the owning parties by PGLN — [the organization-grain who](/journal/the-company-is-not-the-performer/).

**TransformationEvent** (`transformation-event-production.json`) — inputs (two SGTINs plus 10 kg of an input lot, `urn:epc:class:lgtin:…`) differ from outputs (two new SGTINs), joined by `transformationID` so multi-session runs chain.

## The two classic mis-picks, and their downstream bill

**TransactionEvent as "the shipping event."** The name misleads; people reach for it because shipping feels transactional. But shipping is a thing that *happened to objects* — ObjectEvent, `OBSERVE`, `bizStep: shipping`, PO in `bizTransactionList`. Mis-pick and your partner's reconciler, which diffs the despatch advice against *observations*, finds no observation — the [shipped-versus-seen join](/business-transactions/) breaks at its first input.

**ObjectEvent for repacking.** Bulk case in, twelve retail units out, modeled as an OBSERVE on the case: the twelve output identities now appear from nowhere — commissioned by no event, descended from nothing. Recall scope for the input lot silently excludes every downstream unit. TransformationEvent existed precisely so identity changes leave a verifiable seam.

Both mistakes validate schematically, which is the trap: the schema checks shape, not modeling. The decision tree is the check the schema cannot run — it forces the claim into words *before* the field list, and the type falls out of the claim. Sixty seconds, four questions, and the reader you'll never meet gets evidence instead of anecdote.

The fixtures are in the tarball and the validator answers in microseconds — `npx epcis.dev validate`, or [run the whole corpus](/run-it-yourself/). When the claim needs a performer as well as a type, that's [the dimension the standard doesn't carry](/epcis-who-performer-worker-agent/) — and the layer up a level is [visibility.cloud](https://visibility.cloud/).

*Sources: EPCIS 2.0.1 event types (pinned schema, digests at [/conformance/](/conformance/)); all seven `golden-corpus/valid-standard/` fixtures validated 2026-07-31, exit 0 each; fixture excerpts quoted from the packaged corpus.*

---

**Proof, not adjectives.** This build could not verify the gateway's test suite green, so no sentence on this page claims a passing run — the last verified run is recorded in the repository's own CI, not here. Live on this origin, no key: POST /translate, /validate, /hash. No conformance attestation has ever been issued. The dated ledger is /what-ships-today/.
