The Envelope

The single, frozen definition of the bytes that travel on the queue. Every SDK, in every language, MUST produce and consume exactly this shape.

Status: Authoritative · Frozen · schema_version: 1

Canonical envelope

{
  "job": "urn:babel:orders:created",
  "trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
  "data": {
    "order_id": 1042,
    "amount": 99.90
  },
  "meta": {
    "id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
    "queue": "default",
    "lang": "php",
    "schema_version": 1,
    "created_at": 1749132727000
  },
  "attempts": 0
}

Top-level key order is not significant (JSON objects are unordered), but producers SHOULD emit keys in the order above for readable logs.

Field reference

Field Type Required Mutable Description
job string no The message URN — its language-agnostic identity. See URN naming. The canonical field name is job; consumers MUST also accept urn as an inbound alias. Never a PHP/Java/… class name.
trace_id string (UUID) no Cross-service correlation id. Generated by the original producer; preserved and forwarded unchanged by every SDK across every hop. Enables end-to-end distributed tracing.
data object no The business payload — a pure, JSON-encodable key/value object. No language-specific types (no PHP objects, closures, resources; no binary without base64). Numbers follow the data rules.
meta object no¹ Producer-set descriptive metadata. Immutable after production. See the meta block.
attempts integer yes Transport-level delivery/retry counter, starting at 0. Mutated by the broker/worker on each (re)delivery. Deliberately top-level, kept out of the immutable meta block.

¹ meta is conceptually immutable end-to-end. The only counter that legitimately changes during a message’s life is top-level attempts — it is transport state, not producer metadata.

The meta block

Field Type Required Description
id string (UUID) Unique id for this specific message. Distinct from trace_id.
queue string The logical queue name the message was produced onto (not the broker key).
lang string (enum) Producer language tag — one of php, go, python, java, dotnet, node. For observability and debugging.
schema_version integer Envelope schema version. Currently 1. Consumers MUST reject (or quarantine) versions they do not understand.
created_at integer Production time as Unix epoch milliseconds, UTC (milliseconds, not seconds).

Producers MAY add extra meta.* keys for their own observability, but consumers MUST ignore unknown meta keys (forward-compatibility), and those keys MUST NOT be required for correct routing or processing.

id vs trace_id — do not conflate

meta.id trace_id
Scope one message one causal chain (may span many messages/services)
Generated by the producing SDK, per message the first producer in the chain; reused thereafter
Lifetime dies with the message propagates across every hop
Use dedupe, ack/nack, logs for one message end-to-end tracing/observability

When a consumer produces a downstream message while handling one, it SHOULD copy the inbound trace_id onto the new envelope and mint a new meta.id.

Cross-language data rules

Because the same data is decoded by six languages, these are part of the contract, not implementation details:

  • Encoding. UTF-8 JSON only — no trailing commas, no comments, no NaN/Infinity. The reference encoders emit unescaped unicode and slashes so output is byte-identical across SDKs.
  • Numbers. Integers must fit signed 64-bit; do not exceed 2^53 − 1 if any consumer is JavaScript/Node unless carried as a string. Floats are IEEE-754 doubles — never put currency or exact-decimal values in floats; use integer minor units (e.g. cents) or strings. (The example uses 99.90 for readability only.)
  • Time. meta.created_at is Unix epoch milliseconds (UTC). Time values inside data SHOULD be Unix ms (integer) or RFC 3339 / ISO 8601 UTC strings — agreed per URN, never a locale-formatted string.
  • Binary. Raw bytes are not allowed in JSON. Base64-encode and document the field per URN.
  • Booleans / null. Use JSON true/false/null — never 0/1 or "true".

Producer rules

A conformant producer MUST:

  1. Set job to a non-empty URN. An empty URN is a programming error and MUST raise.
  2. Set trace_id to a UUID — reuse an inbound one if continuing a trace, otherwise generate a new v4 UUID.
  3. Emit data as a pure JSON object (see the data rules above).
  4. Populate every required meta field — meta.id unique per message, meta.lang matching the producing language, meta.schema_version = 1, meta.created_at in Unix ms.
  5. Initialize top-level attempts to 0.
  6. Encode as UTF-8 JSON and publish via the broker binding.

Consumer rules

A conformant consumer MUST:

  1. Read the URN from job, accepting urn as an alias if job is absent.
  2. Reject/quarantine any envelope whose meta.schema_version it does not support.
  3. Route to the handler mapped to the URN. If none is mapped, apply the configured unknown-URN strategy (fail · delete · release · dead-letter).
  4. Preserve trace_id for logging and for any downstream messages.
  5. Treat meta and data as read-only; only the broker/worker mutates attempts.
  6. Tolerate unknown extra keys (forward-compatibility) — never hard-fail on them.

Forbidden fields

These appeared in early drafts and are not part of the contract. Do not emit or rely on them:

Forbidden field Use instead
timestamp (top-level) meta.created_at (Unix ms)
meta.max_retries consumer-side config / per-URN policy
meta.attempts top-level attempts
meta.source meta.lang
meta.ts meta.created_at
urn as the canonical producer field job (with urn accepted only as an inbound alias)

Optional dead_letter block

Messages sitting on a dead-letter queue carry one extra, optional top-level field, dead_letter, describing why they failed. It appears only on DLQ messages — never on a normal queue — and is additive, so the envelope stays at schema_version: 1. Normal consumers ignore it.

"dead_letter": {
  "reason": "failed",
  "error": "Payment gateway timeout",
  "exception": "App\\Exceptions\\GatewayTimeout",
  "failed_at": 1749132730000,
  "original_queue": "orders",
  "attempts": 3,
  "lang": "php"
}

Change control

  • Any change to this envelope is architecturally significant.
  • Additive, optional, backward-compatible fields → keep schema_version = 1.
  • Removing/renaming/retyping a field, or changing semantics → bump schema_version and provide a migration/compat path for older consumers.

Continue to URN naming.