The launch journal

FSMA 204 is an event schema if you squint correctly.

Read the FSMA 204 rule as a compliance document and you get a records-request checklist. Read it as an engineer and you notice something more useful: the rule's whole vocabulary — Critical Tracking Events that happen to food, Key Data Elements that must be recorded about each — is an event schema wearing regulatory prose. Every CTE is an event type; every KDE is a field. And the event standard your supply chain already has a door for carries all of it conformantly. This post is the mapping table, mechanical enough to hand to the engineer who asked "so what do we actually store?"

One framing rule before the table, because it decides whether your project survives review: the argument for doing this is operational — records that answer trace questions in minutes instead of days — never the deadline. The deadline is softer than the vendors selling it admit, and the honest date record is its own post.

CTEs are event shapes

FSMA 204 CTEEPCIS event shapewhy this shape
HarvestingObjectEvent, action: ADD, bizStep: commissioning, lot born in ILMDfirst record of the lot's existence
CoolingObjectEvent, action: OBSERVE, the cooling step and locationan observation of an existing lot
Initial packingObjectEvent (ADD, packing) + AggregationEvent for case/pallet containmentidentity plus containment, asserted separately
ShippingObjectEvent, action: OBSERVE, bizStep: shipping, sourceList/destinationListcustody asserted org-grain, both ends
ReceivingObjectEvent, action: OBSERVE, bizStep: receiving, sourceList/destinationListthe mirror record, at the receiver's point
TransformationTransformationEvent, input lots → output lotsinputs ≠ outputs is exactly what this type exists for

Nothing exotic: two event types and one aggregation pattern cover the rule. The transformation row deserves its own note — input/output lot genealogy is the standard's answer to "which finished lots contain that ingredient," with no side genealogy database.

KDEs are field placements

KDEconformant homenotes
Traceability lot code (TLC)lot-grain identity: LGTIN in quantityList / epcClass; stamped in ilmd at commissioningone lot, one identifier, one story — the TLC post works this in full
TLC source / source referencesourceList party (PGLN) + location master dataorganisation-grain, per CBV §7.4
Location descriptionreadPoint / bizLocation as SGLN; attributes in location master data (CBV §9.3)a GLN is a description that joins
Date of the eventeventTime (+ eventTimeZoneOffset)the rule's date, with its timezone honest
Quantity and unit of measurequantityList: quantity + uomthe same grain the diff and the trace use
Reference document type & numberbizTransactionListpo, desadv, recadv typesthe document join, designed into CBV
Product descriptiontrade-item master data (CBV §9.2), shipped in the document headertravels with the record, no side lookup

The receiving CTE, as one conformant event

The row most estates fail first is receiving, so here it is whole — TLC at lot grain, quantity and UoM, both reference documents, both parties, the receiving location as an SGLN:

{
  "type": "ObjectEvent",
  "eventTime": "2026-07-31T13:42:00.000Z",
  "eventTimeZoneOffset": "-05:00",
  "quantityList": [
    { "epcClass": "urn:epc:class:lgtin:0614141.107346.7A-4412",
      "quantity": 96, "uom": "EA" }
  ],
  "action": "OBSERVE",
  "bizStep": "receiving",
  "disposition": "in_progress",
  "readPoint": { "id": "urn:epc:id:sgln:0614141.00888.0" },
  "bizTransactionList": [
    { "type": "po",     "bizTransaction": "urn:epc:id:gdti:0614141.06012.1234" },
    { "type": "desadv", "bizTransaction": "urn:epc:id:gdti:0614142.11711.5501" }
  ],
  "sourceList": [
    { "type": "owning_party", "source": "urn:epc:id:pgln:0614142.00001" },
    { "type": "location",     "source": "urn:epc:id:sgln:0614142.00777.0" }
  ],
  "destinationList": [
    { "type": "owning_party", "destination": "urn:epc:id:pgln:0614141.00001" },
    { "type": "location",     "destination": "urn:epc:id:sgln:0614141.00888.0" }
  ]
}
$ npx epcis.dev validate fsma-receiving-cte.json
valid	schema=epcis-json-schema.json	version=2.0.1
$ npx epcis.dev hash fsma-receiving-cte.json
hash	0	ni:///sha-256;6e82b882880ed2501c5f83bce669a4fe34ccc2c8a2e2d1b6084d433592e6a8aa?ver=CBV2.0

Validated against GS1's own pinned 2.0.1 schema, identified by its CBV §8.9 content hash. No extension fields were needed for any KDE — the rule fits inside the standard's core.

The surplus is the point

Map the tables both directions and notice the asymmetry: a conformant EPCIS record carries more than the rule asks. The rule wants a date; the event carries a timezone-honest instant. The rule wants a location description; the event carries a GLN that joins across parties. The rule wants reference documents; the event carries typed references your trading partners' systems can match on. Build to the rule and you have records. Build to the standard and you have records that also run your receiving diff, your recall scope query, and your partner reconciliation — the operational return that survives any deadline movement.

And the famous ask — trace data as a sortable spreadsheet within 24 hours — stops being a fire drill. It is a query over events: filter by lot, join the aggregation history, project the KDE columns. One EPC, one call, the whole chain is the same machinery pointed at a recall instead of an audit.

Validate the mapping today: the fixture above runs through npx epcis.dev validate with no account — exit code is the verdict. Keep the records queryable: stored traces ride the capture spine — get a capture key and say what you will capture.

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.