Avro Value protocol
Durable Workflow 2.0 uses one fixed recursive
durable_workflow.protocol.Value
schema for every Avro payload. Workflow inputs and results, activity values and
failure details, signals, queries, updates, replay history, and externally
stored payloads all use the same schema. Applications do not publish their own
Avro schemas, and the platform does not require a network schema registry.
The value union has distinct named branches for null, boolean, signed 64-bit
integer, finite double, bytes, UTF-8 string, list, and string-keyed map. That
keeps 7 distinct from 7.0, text distinct from bytes, and lists distinct from
maps in PHP, Python, and Rust.
Wire frame
The blob field is base64 around standard Avro single-object bytes:
C3 01 || 8-byte little-endian CRC-64-AVRO fingerprint || Avro datum
Schema v1 has fingerprint e2a33dff55802237. SDKs bundle the immutable schema
for each supported fingerprint, select the writer schema from the frame, and
resolve it against the current reader. An unknown fingerprint or incompatible
new branch fails as unsupported_payload_schema; decoders do not guess or
fall back to JSON.
Future value kinds are new uniquely named record branches appended to the union. Released branches are never reordered or reused.
Value policy
- Map keys must be strings. SDKs reject other keys instead of stringifying them.
- Integers must fit the signed 64-bit Avro
longrange. - Doubles must be finite; NaN and infinities are rejected.
- Python
bytesand RustAvroValue::Bytesselect Avrobytes. PHP callers useAvroBinaryValue::fromBytes()because a PHP string alone cannot declare whether it is text or binary. - Decimal, arbitrary-precision integer, date/time, UUID, enum, dataclass, Pydantic, and domain objects require explicit adapters to a canonical value kind.
Avro is the only Durable Workflow 2.0 payload codec. HTTP request and response
documents remain JSON transport, but every durable value inside those
documents uses this fixed schema and single-object frame. A json codec tag,
an unknown codec, or an untagged raw durable blob fails closed with
unsupported_payload_codec; runtimes never transcode or guess.
JSON inspection projection
Run descriptions retain input_envelope, output_envelope, and result
envelopes as the lossless payload authority. JSON-facing inspection surfaces
such as the CLI and Waterline render values that JSON cannot represent with a
typed projection:
{"$type":"bytes","base64":"AP8="}
{"$type":"map","entries":[{"key":"0","value":"zero"}]}
The map projection is used for empty maps and numeric-looking string keys that PHP arrays cannot retain without changing their type. Ordinary scalars, lists, and unambiguous string-keyed maps remain ordinary JSON values. Consumers that need the original typed value decode the accompanying envelope rather than the display projection.
Repeatable benchmark
Each SDK ships the same representative-value benchmark and enforces a budget for its selected production path:
# PHP SDK checkout
composer benchmark-avro-value
# Python SDK checkout
python benchmarks/avro_value.py --enforce
# Rust SDK checkout
cargo run --release --example avro_value_benchmark -- --enforce
The JSON output compares compact JSON, the removed JSON-in-Avro wrapper, and
the fixed typed schema. It reports raw datum, framed payload, and actual
{codec, blob} HTTP-envelope sizes together with end-to-end adapter,
encode, and decode latency. AVRO_VALUE_ENCODE_BUDGET_US and
AVRO_VALUE_DECODE_BUDGET_US can tighten the defaults on a qualification
runner. Release CI executes these commands with budget enforcement; a
production-path regression must be explained or corrected before release.
The old wrapper implementation exists only inside the benchmark, not as a
runtime compatibility path.
Deployment preflight
Server bootstrap inventories every persisted payload_codec and verifies the
single-object magic and fixed-schema fingerprint of inline frames,
nested-history envelopes, and external payload references. Deployment stops
before the new runtime starts if any active or replay-relevant non-Avro payload,
untagged payload, corrupt reference, or obsolete frame exists. Active runs may
drain on the current prerelease; retained terminal and replay-relevant state
must follow the backup-first
prerelease history migration.
Exporting a run does not alter the rejected database state. Never delete
history to bypass the preflight.