Append-only means you correct out loud.
31 July 2026 · the launch journal
Every operational record contains mistakes. The design question is not how to prevent them — you cannot — but what the record looks like after one is fixed. A store that fixes mistakes with UPDATE has a second, quieter problem: after the fix, there is no evidence the mistake ever happened, which means there is no evidence about anything. If rows can be edited, any row might have been. The moment a dispute reaches the record, "we corrected it" and "we rewrote it" are indistinguishable — and that ambiguity is exactly what the counterparty's lawyer needs.
EPCIS ships the alternative in the core standard: errorDeclaration. A mistake is corrected by declaring it — a new event that marks the erroneous one, states a reason from the CBV's error-reason vocabulary, and links the corrective events forward. Nothing is edited. Nothing is deleted. The record stays trustworthy precisely because it visibly contains its own mistakes.
The worked correction, captured for real
An operator books a shipment out of the wrong flow — the event says shipping when the goods were actually being received. Three events tell the whole story, and here is the capture transcript, run through the local gateway that ships in the package:
$ npx epcis.dev capture correction-doc.json
accepted captureID=ab6d179a-4ac1-41dc-be26-c2ee731103b3 events=2
eventID urn:uuid:5e900006-0000-4000-8000-000000000002
eventID urn:uuid:5e900006-0000-4000-8000-000000000003
Event …0001 is the original, wrong observation — bizStep: shipping — already in the store, untouched forever. Event …0002 is the corrective event: the same EPC at the same time, with what actually happened (receiving). Event …0003 is the declaration — a new event, with its own fresh eventID, that repeats the erroneous content and carries the declaration block:
"errorDeclaration": {
"declarationTime": "2026-07-19T12:15:00.000Z",
"reason": "incorrect_data",
"correctiveEventIDs": ["urn:uuid:5e900006-0000-4000-8000-000000000002"]
}
reason comes from CBV's standardized error-reason identifiers — incorrect_data when the observation was wrong, did_not_occur when the whole event was phantom. correctiveEventIDs points forward at the replacement. After the correction lands, all three events are present on every read: the corpus sequence fixture (golden-corpus/sequences/correction-errordeclaration.json in the package) asserts exactly that with three present: true reads.
The hash fact that makes this airtight
The CBV 2.0 §8.9 event hash — the computed identity every event carries here — deliberately excludes three things: eventID, recordTime, and errorDeclaration. The hash identifies the observation as originally captured. Watch what that implies, with real output:
$ npx epcis.dev hash erroneous-original.json
hash 0 ni:///sha-256;649a245b6e626325c60ca71777cfb66f386efd6fcc722fab7c2fecdfc56cd0c6?ver=CBV2.0
$ npx epcis.dev hash declaration-event.json
hash 0 ni:///sha-256;649a245b6e626325c60ca71777cfb66f386efd6fcc722fab7c2fecdfc56cd0c6?ver=CBV2.0
The declaration event restates the erroneous observation and adds the declaration block — and hashes identically to the original, because the declaration is about the observation, not part of it. The declared event's identity is unchanged. That is the point: a correction never mints a new version of history. It adds testimony about the history that exists.
The same fact closes the back door. The EPCIS 1.2 idiom — re-submitting an existing eventID with an errorDeclaration bolted on, an in-place edit wearing a conformance costume — is deterministically rejected at this capture door. The append-only law here is checkable: any (eventID, hash) pair once read must be byte-identical on every future read. Since the hash ignores errorDeclaration, a same-eventID declaration would alias the original under the same identity with different bytes — so the gateway refuses it, and the refusal is a pinned corpus fixture (sequences/errordeclaration-edit-attempt.json). One eventID, one immutable row, forever. Declarations are inserts with fresh IDs, full stop.
Read semantics: two views, one record
Append-only does not mean your operational dashboards display known-wrong events forever. It means the record keeps them while views decide. The query surface carries declaration-aware predicates from the standard's own parameter set — EQ_errorReason, EQ_correctiveEventID, and time-bounded reads over declarationTime — so the two consumers that matter each get their shape:
- Operational reads exclude declared-erroneous events and follow correctiveEventIDs to the replacement — the trace shows what actually happened.
- Audit reads include everything: the mistake, when it was declared, why, by what it was corrected, and the fact that the original bytes never moved.
Same table, two projections. The alternative — an "audit log" bolted beside an editable store — reproduces the original problem one layer up: now the log and the table can disagree, and you need a third thing to say which is right. An append-only record with declared errors has nothing to reconcile, because there is only one thing.
Why a reviewer should care
When your security or architecture review asks "how are corrections handled," most vendors answer with process — who is allowed to edit, and how edits are logged. The structural answer is stronger: corrections are inserts; edits are impossible; the erroneous event, the declaration, and the correction are three content-addressed rows anyone can re-verify with npx epcis.dev hash. In a dispute, you do not attest that the record was handled properly. You hand over the record, and it demonstrates itself.
Validate the fixtures on your own machine — the corpus ships in the npm package, and the schema it validates against is sha256-pinned on the conformance page:
npx epcis.dev validate node_modules/epcis.dev/golden-corpus/valid-standard/object-event-error-declaration.json
The computed-identity law this post leans on is the §8.9 event hash; the same identity is what makes capture idempotent under retries. The full local toolkit is on run it yourself, and the business reading of an unrewritable record — evidence, not attestation — is visibility.cloud's brief.
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.