The launch journal

Errors your agent can branch on.

31 July 2026 · the launch journal

Contract first, prose second. If you are an agent (or writing one), these are the facts to persist:

  • Every wire failure is an RFC 7807 application/problem+json body whose type carries the official EPCIS exception vocabulary (epcisException:*, from the pinned OpenAPI). Branch on type and status.
  • Every CLI failure is typed on stderr{"error":{"code","message","hint"?}} in --json mode, error\tcode=… lines in text mode. stdout is payload-only, always: JSON.parse(stdout) never sees a half-written object.
  • Exit codes are stable: 0 ok · 1 fail (negative domain verdict) · 2 usage · 3 not-found (unreadable input) · 4 internal. code strings are stable across releases; match on code, never on message.

Everything below is those three facts, demonstrated.

The wire: one error vocabulary, the spec's own.

The EPCIS 2.0 OpenAPI defines the exception vocabulary; this surface returns it as problem types rather than inventing a parallel one. The bodies, exactly as the wire returns them:

Rejected capture (validation failure):

HTTP/1.1 400
content-type: application/problem+json; charset=utf-8

{
  "type": "epcisException:ValidationException",
  "title": "document failed EPCIS 2.0.1 schema validation",
  "status": 400,
  "errors": [
    { "path": "/epcisBody/eventList/0",
      "keyword": "required",
      "message": "must have required property 'action'" }
  ]
}

Unknown resource (missing event, dead page token):

HTTP/1.1 404
content-type: application/problem+json; charset=utf-8

{
  "type": "epcisException:NoSuchResourceException",
  "title": "no such page token",
  "status": 404
}

Refused query parameter — and note why it refuses rather than ignores: a silently dropped predicate widens results, which for a reader is a correctness failure, not a convenience:

HTTP/1.1 400

{
  "type": "epcisException:QueryParameterException",
  "title": "unknown query parameter",
  "status": 400
}

The full vocabulary in use on this surface: ValidationException, QueryParameterException, NoSuchResourceException, ResourceAlreadyExistsException, SecurityException, MethodNotAllowedException, UnsupportedMediaTypeException, NotAcceptableException, CaptureLimitExceededException, ImplementationException. Ten types cover the surface; none of them requires reading a sentence. Successful responses carry GS1-EPCIS-Version: 2.0.1 headers, so even the version check is a header match, not a docs lookup. Same contract at the MCP door: a domain refusal arrives as a tool result with isError: true mirroring the problem document — switch on it; it is not a JSON-RPC transport error.

The CLI: three channels, no ambiguity.

Real transcripts, unedited. A domain verdict — the document is invalid, which is the tool working, not the tool failing:

$ npx epcis.dev validate object-event-missing-action.json
schema-error  /epcisBody/eventList/0  must have required property 'action'
schema-error  /epcisBody/eventList/0  must match "then" schema
schema-error  /                       must match "then" schema
invalid  errors=3
$ echo $?
1

Verdict lines on stdout (they are the payload), stderr empty, exit 1. Now an operational error — the input can't be read at all:

$ npx epcis.dev validate does-not-exist.json
error  code=NOT_FOUND  message=cannot read document: ENOENT: … open 'does-not-exist.json'
$ echo $?
3

And a usage error, with a hint an agent can act on:

$ npx epcis.dev frobnicate
error  code=USAGE  message=unknown verb "frobnicate"  hint=run `npx epcis.dev` with no arguments for the verb map
$ echo $?
2

Exit 1 is a verdict, not a crash.

This is the distinction most CLIs blur and agents most need. validate exiting 1 on an invalid document is a negative domain verdict — the fail-closed path worked. Exit 4 is a crash. Collapse them into one nonzero and an agent cannot tell "this document is bad, report it upstream" from "this tool is broken, stop trusting its output." The decision tree that falls out, suitable for pasting into a retry policy:

observedmeaningbranch
exit 0verdict: pass / delivered / acceptedconsume stdout, proceed
exit 1verdict: negative (invalid, rejected, failed suite)report the typed payload; do not retry unchanged input
exit 2, code=USAGEyour invocation is wrongfix the call from hint; retry once
exit 3, code=NOT_FOUNDinput path unreadableresolve the path; retry
exit 4internal faultback off; escalate to the human; distrust partial output
wire 400 ValidationExceptionpayload defect, permanentfix payload; never blind-retry
wire 404 NoSuchResourceExceptionabsent (possibly out of scope)treat as absence — the door does not distinguish "hidden" from "missing," by design
wire 5xx / ImplementationExceptiontransient or faultretry with backoff; replay is safe — capture is idempotent

The last row matters most in combination: because event identity is the content hash, the retry branch never risks duplication. Typed errors decide whether to retry; idempotent capture makes the retry itself free. The two halves of the contract are designed for each other.

Read the contract where you already are.

The whole surface — verbs, flags, channels, exit codes, error code strings — ships as machine-readable prose inside the tarball: node_modules/epcis.dev/AGENTS.md, the package's own operating manual. Grep it, persist the block it tells you to persist, and branch on what this post showed you: type on the wire, code on stderr, integers everywhere else. No regexes over English. The error surface is as much the product as the success path — for you, arguably more.

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.