Skip to content

EventEnvelope Reference

EventEnvelope is the immutable message container between composable functions — the only thing that ever passes from one function to another. It is the Rust port of the Java org.platformlambda.core.models.EventEnvelope, and it has the same three parts:

Part Contents
metadata routing (id, to, from, reply_to), correlation (cid), tracing (trace_id, trace_path, span_id), status, exec_time
headers HashMap<String, String> — parameters and metadata about the body
body a dynamic rmpv::Value — the analog of Java's untyped Object payload

Envelopes are built with fluent setters and read with getters; every accessor documented here exists on the struct in crates/platform-core/src/envelope.rs.

use platform_core::EventEnvelope;

let event = EventEnvelope::new()
    .set_to("v1.get.profile")
    .set_header("type", "query")
    .set_body(ProfileRequest { id: 12 })?;

Constructing and addressing

EventEnvelope::new()

Returns Notes
EventEnvelope fresh envelope with a generated id, empty headers, Nil body

Every new envelope gets a unique dash-less UUID as its id.

set_to(route) / to()

Setter Getter
set_to(&str) -> Self to() -> Option<&str>

The destination route name. PostOffice::send returns a 400 error when to is missing and 404 when the route is not registered.

set_from(route) / from()

Setter Getter
set_from(&str) -> Self from() -> Option<&str>

The sender's route. Inside a traced request the platform stamps this automatically on outbound events (when unset), so replies and telemetry can attribute the caller.

set_reply_to(route) / reply_to()

Setter Getter
set_reply_to(&str) -> Self reply_to() -> Option<&str>

Where the function's return value is delivered. PostOffice::request sets this to a temporary RPC inbox automatically; event interceptors read it to reply manually.

set_correlation_id(cid) / correlation_id()

Setter Getter
set_correlation_id(&str) -> Self correlation_id() -> Option<&str>

The correlation id. On the RPC path this is transport plumbing (the caller's id is kept, or one is minted); the worker copies the request's correlation id onto the reply. The business correlation-id of the current request is read through PostOffice::my_correlation_id instead.

Tracing metadata

set_trace(trace_id, trace_path) / trace_id() / trace_path()

Setter Getters
set_trace(&str, &str) -> Self trace_id() -> Option<&str>, trace_path() -> Option<&str>

The distributed-trace identity and path (e.g. GET /api/greeting/{user}). Normally the platform manages these: the REST edge starts a trace when the endpoint declares tracing: true, and every PostOffice send/RPC inside a traced function propagates them.

set_span_id(span_id) / span_id()

Setter Getter
set_span_id(&str) -> Self span_id() -> Option<&str>

The sender's span id, carried so the receiver knows its own parent span — this is how parent-child span lineage chains across hops (and across HTTP boundaries via the W3C traceparent header).

Status and errors

set_status(code) / status() / has_error()

Method Behavior
set_status(i32) -> Self set an HTTP-style status code
status() -> i32 unset means 200
has_error() -> bool status() >= 400

Status codes are HTTP-style throughout the platform: a status of 400 or higher is an error. A function returning Err(AppError) becomes an error reply whose status is the AppError's code and whose body is its message; a timed-out RPC yields status 408.

let reply = po.request(event, Duration::from_secs(5)).await?;
if reply.has_error() {
    log::warn!("call failed with status {}", reply.status());
}

Headers

set_header(key, value) / headers() / header(key)

Method Behavior
set_header(&str, &str) -> Self add or replace one header
headers() -> &HashMap<String, String> all headers
header(&str) -> Option<&str> one header

Headers carry parameters and metadata about the body — both are delivered to the target function (handle_event receives the headers map alongside the input).

Body

set_body(value)

Signature Returns
set_body<T: Serialize>(self, value: T) Result<Self, AppError>

Serializes any serde-serializable value into the dynamic body — the analog of Java's setBody(Object) PoJo transport. Fails with status 500 if the value cannot be represented.

set_raw_body(value)

Signature Returns
set_raw_body(self, value: rmpv::Value) Self

Sets the body from an already-dynamic value — no serialization step, no Result.

body() / body_as::<T>()

Method Behavior
body() -> &rmpv::Value the raw dynamic body
body_as<T: DeserializeOwned>() -> Result<T, AppError> deserialize into a concrete type (Java getBody(Class))
let request: serde_json::Value = input.body_as()?;      // free-form
let profile: Profile = reply.body_as()?;                // typed

Nesting limit

A payload nested deeper than 64 maps and lists is refused as a decoding error, the envelope map being level 1, so a body may nest 63 levels (serializer::MAX_DEPTH, the Java engine's MsgPack.MAX_DEPTH). The limit keeps a crafted payload from exhausting the decoder's stack. EventEnvelope::from_bytes reports Nesting deeper than 64 levels, and the Event API answers such a request with HTTP 400. A header that promises more than the input holds is refused at the header, before anything is allocated for its declared length or count, and so is the format byte 0xc1, which the specification never uses; the shared vector file msgpack-hostile-header-vectors.json pins the same inputs in both engines.

Empty input decodes as an empty map, so an empty byte array is an empty envelope and never a decoding error, as the Java engine - the reference implementation - reads it; the canonical package decoder refuses it, because a package is never empty.

Bytes after the top-level container, well-formed or not, are refused as a decoding error: a payload holds exactly one value, in the event codec as in the canonical package decoder. EventEnvelope::from_bytes reports Unexpected bytes after the value at offset N, and the Event API answers such a request with HTTP 400; the Java engine, the reference implementation, applies the same rule.

Performance metadata

exec_time()

Signature Returns
exec_time(&self) Option<f32> (milliseconds)

The function execution time, stamped by the worker on every reply and rounded to three decimal places at the source — the reply envelope, the telemetry dataset, and the Playground traveler all report the same value.

Fields set automatically

Application code normally sets only to, headers, and body (plus reply_to for manual replies). The platform manages the rest:

  • PostOffice (outbound, inside a traced request): trace id/path, the sender's span id, from (when unset), and the business correlation-id (when the event carries none).
  • PostOffice::request: reply_to (a temporary inbox) and the correlation id.
  • The worker (on every reply): the request's correlation id, from (the executing route), to (the reply route), exec_time, and the trace triple so the next hop chains correctly.

Serialization

The body is an rmpv::Value (MessagePack's dynamic value type), so a body is already in wire shape in memory — typed values pass through serde on set_body/body_as.

  • In-process events are never serialized. The in-memory bus moves the envelope directly; to_bytes()/from_bytes() (MsgPack) are used only on the spill path — when a route's elastic queue buffers a burst to disk.
  • JSON at HTTP boundaries. REST automation renders a map/array body as application/json, a string body as text/plain, and a binary body as application/octet-stream; websocket frames and outbound HTTP client bodies are JSON as well.
  • Null omission — absent means null. At every boundary — the wire (JSON and MsgPack alike) and every in-memory event-bus hop — null map key-values are omitted by default, so a consumer must treat an absent key and a null value as the same thing; the behavior is deterministic and identical to the Java engine on every delivery path. Set serializer.null.transport: true to transport nulls explicitly. Array elements are never dropped (ordering is preserved), and an empty [] or {} is a real value. See the Configuration Reference.

to_bytes() / from_bytes(bytes)

Method Behavior
to_bytes() -> Result<Vec<u8>, AppError> encode the envelope as MsgPack
EventEnvelope::from_bytes(&[u8]) -> Result<Self, AppError> decode an envelope

Reserved envelope tags

The wire's optional tags field (map of str → str) carries engine-managed metadata. Tags are never visible to a user function — the engine consumes them at delivery. Two tag names are reserved with cross-language semantics that a conforming implementation must honor (see the Java repo's wire-format specification):

  • rpc — marks an RPC request (value = timeout in ms). The serving engine suppresses the worker-side trace record for an RPC-served execution; the caller's round-trip record is the single record for that span.
  • my_cid — carries the business correlation-id across touch points and Event-over-HTTP hops. Metadata is never transported as envelope headers; the receiving engine injects the read-only my_correlation_id key into the function's input header copy from this tag at delivery.

Rust port

The envelope encodes as the standard event envelope wire format — a MsgPack map with descriptive string keys shared verbatim with the Java engine (normative spec: the Java repo's Event Envelope Wire Format; golden conformance vectors are kept byte-identical in both repositories). Java's legacy compact flag-keyed encoding is not decoded (v1 is standard-only). Envelope features tied to unported subsystems do not exist here: serialized exception/stack-trace transport (getException, stack.trace.transport.size), broadcast flags, and the Kv helper. tags are engine-reserved (see above), and envelope-level annotations ride REPLY envelopes only (the worker attaches PostOffice::annotate_trace values; the RPC caller folds them into the trace record and strips them). Trace annotation and business-context APIs are on the PostOffice — see the API Overview.


Adapted from the mercury-composable guide docs/guides/event-envelope-reference.md; keys/APIs enumerated from this repository's source.