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 − 1if 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 uses99.90for readability only.) - Time.
meta.created_atis Unix epoch milliseconds (UTC). Time values insidedataSHOULD 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— never0/1or"true".
Producer rules
A conformant producer MUST:
- Set
jobto a non-empty URN. An empty URN is a programming error and MUST raise. - Set
trace_idto a UUID — reuse an inbound one if continuing a trace, otherwise generate a new v4 UUID. - Emit
dataas a pure JSON object (see the data rules above). - Populate every required
metafield —meta.idunique per message,meta.langmatching the producing language,meta.schema_version = 1,meta.created_atin Unix ms. - Initialize top-level
attemptsto0. - Encode as UTF-8 JSON and publish via the broker binding.
Consumer rules
A conformant consumer MUST:
- Read the URN from
job, acceptingurnas an alias ifjobis absent. - Reject/quarantine any envelope whose
meta.schema_versionit does not support. - Route to the handler mapped to the URN. If none is mapped, apply the configured
unknown-URN strategy (
fail·delete·release·dead-letter). - Preserve
trace_idfor logging and for any downstream messages. - Treat
metaanddataas read-only; only the broker/worker mutatesattempts. - 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_versionand provide a migration/compat path for older consumers.
Continue to URN naming.