Every scan is a potential event.
31 July 2026 · the launch journal
A scan that ends at "decoded successfully" has produced a string. A scan that ends at a validated EPCIS event has produced evidence: what was seen, when, where, in which process step — in the shape every conformant system on earth can load. The distance between the two is one mechanical pipeline, and this entry walks it end to end with real output at each stage, because the pipeline is exactly the kind of thing a coding agent gets asked to build in an afternoon and exactly the kind of thing that should not be improvised.
Stage one: the canonical AI map decides the grain
Input: a parsed scan — both wire grammars land on one AI map, so this pipeline starts syntax-free:
{ "01": "09506000134352", "10": "ABC123", "17": "270331" }
The map mechanically decides the identifier grain, and the grain decides which event field carries it:
- 01 + 21 (serial) → an SGTIN, instance grain → one entry in epcList.
- 01 + 10 (lot), no serial → an LGTIN, class grain → a quantityList element with quantity and unit.
- 00 (SSCC) → the logistic unit itself → epcList, and the aggregation machinery one entry over.
This mark carries a lot, no serial. So: urn:epc:class:lgtin:9506000.013435.ABC123, quantity 1, in quantityList. No heuristics, no configuration — the printer's AI choices are the grain decision, which is why the pipeline must never flatten the map.
Stage two: context the mark cannot supply
The mark says what. The scanning context supplies the other dimensions: eventTime from the device clock with its eventTimeZoneOffset; readPoint from lane provisioning (an SGLN — which door of which site); bizStep/disposition from the workflow the scan happened inside (receiving/in_progress at a dock lane), CBV bare words per the pinned schema. A pipeline that cannot say where its bizStep came from ships events that validate and still lie; context fields are provisioning data, versioned with the lane.
And one dimension the standard's core fields do not carry: who performed the observation. EPCIS 2.0's dimensions run what/when/where/why/how; the parties in sourceList/destinationList are organizations, at PGLN grain. The person or agent holding the scanner has no core field — the case for why that grain matters is the performer pillar. The standard's own extension mechanism is the sanctioned answer: a namespaced field under a declared JSON-LD context. So the constructed event carries spine:who — the attested observer — under the declared spine context:
{
"@context": [
"https://ref.gs1.org/standards/epcis/epcis-context.jsonld",
{ "spine": "https://epcis.dev/ns/spine/v1" }
],
"type": "EPCISDocument",
"schemaVersion": "2.0",
"epcisBody": { "eventList": [ {
"type": "ObjectEvent",
"eventID": "urn:uuid:7a000039-0001-4000-8000-000000000001",
"eventTime": "2026-07-31T15:42:07.000Z",
"eventTimeZoneOffset": "-05:00",
"quantityList": [
{ "epcClass": "urn:epc:class:lgtin:9506000.013435.ABC123",
"quantity": 1, "uom": "EA" }
],
"action": "OBSERVE",
"bizStep": "receiving",
"disposition": "in_progress",
"readPoint": { "id": "urn:epc:id:sgln:0614141.00888.DOCK-2" },
"spine:who": "https://id.org.ai/agent/scan-station-04"
} ] }
}
The observer identity is an id.org.ai identifier — Agent. Human. Thing. — because the thing holding the scanner is a station agent here, a human with a handheld tomorrow, and the field does not care which.
Stage three: born conformant, or not born
The law of the pipeline: validation happens before anything leaves the function. Not at the server, not in a nightly job — at construction, so a malformed event is a bug report at the lane, never a record in the store:
$ npx epcis.dev validate scan-constructed-event.json --project
valid schema=epcis-json-schema.json version=2.0.1
Two things happened in that one line. The event validated against the sha256-pinned official GS1 EPCIS 2.0.1 schema (pins here). And --project checked the extension law: strip every spine: member and the projection still validates as pure EPCIS 2.0 — the extension rides along conformantly or the event is rejected. A partner's stock validator loads this event and never notices the extension it does not speak.
Stage four: capture, and the two grains
$ npx epcis.dev capture scan-constructed-event.json
accepted captureID=142dcbee-06e8-48ea-8252-985e8ecb68cf events=1
eventID urn:uuid:7a000039-0001-4000-8000-000000000001
At the door, the trust boundary does its work. The caller asserts spine:who — the observer that was there. The gateway stamps spine:capturedBy — the warrantor account the capture key resolves to — and recordTime, server-side; caller-supplied values for stamped fields are stripped, never trusted. Two fields, two meanings, never merged: who answers who saw it, capturedBy answers which account stands behind the record entering the store. An agent capturing under its operator's account is the normal case, and the separation is precisely what a dispute needs intact. Read-back at the default grain serves the org-level view derived from grant chains; actor-grain fields are a scoped opt-in — organization grain is derived at read time, never stamped on the event.
Idempotency rides for free: the event's identity is its CBV 2.0 §8.9 content hash, so the lane's retry buffer can replay the whole day and the store converges.
The block an agent actually needs
The whole pipeline, as the session tools see it: parse to AI map at barcoding.dev; construct; validate --project; capture. The last two run today, locally, no account — npx epcis.dev or the MCP door (validate and translate are pure and annotated read-only; capture is the write, gated exactly where a write should be). Fixtures to test against ship in the package's golden corpus.
For the hosted chain — lanes feeding a durable stream with scoped capture keys, the attested-observer envelope end to end — say what your agent will capture. The event this pipeline emits is the same record the receiving reconciliation, the trace, and the executive views at visibility.cloud all read, because there is only one record.
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.