Evaluation record
The EvaluationRecord is the single normative artifact produced by every flag evaluation in every exd SDK. It is the only contract between SDKs and any downstream consumer. There is no second record shape, no per-sink shape variation, and no transformation layer that changes its semantics in transit.
This invariant is the telemetry analogue of the manifest spec's "single validator" rule: any property the analysis surface relies on MUST be enforced by the SDK at record-emission time.
Wire format
JSON is the canonical wire format. Streaming-bus encodings (Avro, Protobuf) are derived from the JSON schema field-for-field; their schema files are tracked in crates/exd-client/schemas/ and versioned with schema_version.
Records are emitted one per evaluation. Batching is a sink concern (see sinks) and never changes the record shape.
JSON encoding invariants
These rules apply to every record on every sink. Sink implementers and warehouse consumers can rely on them.
| Aspect | Rule |
|---|---|
| Encoding | UTF-8. |
| Whitespace | Insignificant. The reference SDK emits compact JSON (no insignificant whitespace) for records; pretty-printing is reserved for CLI output. |
| Numbers | All integer fields fit in signed 64-bit. Floating-point fields use IEEE 754 double precision. Producers MUST NOT emit NaN, +Infinity, or -Infinity; consumers MAY reject records that contain them. |
| Timestamps | RFC 3339 UTC strings with millisecond precision (2026-05-08T14:22:13.123Z). Producers MUST emit UTC (Z suffix) and SHOULD include millisecond precision; consumers MUST accept any RFC 3339 timestamp. |
| Object key ordering | Insignificant. Consumers MUST NOT depend on key order. |
Schema
{
"schema_version": 1,
"evaluation_id": "01HXYZW3TQK4GVNB...",
"timestamp": "2026-05-08T14:22:13.123Z",
"ingested_at": null,
"namespace": "checkout",
"environment": "production",
"flag_key": "new-checkout-flow",
"variant_key": "variant-b",
"variant_value": { "type": "bool", "value": true },
"evaluation_reason": "matched_rule",
"matched_rule_id": "rule-2",
"manifest_version": 43,
"manifest_etag": "abc123def",
"unit_id_hash": "9f86d081884c7d65...",
"unit_id_type": "user",
"secondary_unit_ids": {},
"context_attributes": { "country": "US", "plan": "pro" },
"sdk_name": "exd-client-rust",
"sdk_version": "0.4.2",
"request_id": null,
"trace_id": null,
"span_id": null
}
Field reference
| Field | Required | Type | Notes |
|---|---|---|---|
schema_version | yes | integer | Currently 1. Producers MUST emit this. |
evaluation_id | yes | string | ULID or UUIDv7. Time-orderable, unique within a producing SDK process. |
timestamp | yes | RFC 3339 string | Wall-clock time of the evaluation. UTC with millisecond precision RECOMMENDED. |
ingested_at | no | RFC 3339 string | null | When a downstream loader received the record. SDK MUST emit null. |
namespace | yes | slug | The flag-namespace slug from the manifest. |
environment | yes | slug | The environment key resolved at evaluation time. |
flag_key | yes | key | The flag key. |
variant_key | yes | key | The variant key returned to the caller. |
variant_value | yes | typed variant (see below) | The typed value of the variant. |
evaluation_reason | yes | enum | One of matched_rule, fallthrough, off, prerequisite_failed, error, sdk_default. See § Evaluation reasons. |
matched_rule_id | conditional | string | null | Required when evaluation_reason = matched_rule; null otherwise. |
manifest_version | yes | integer | The monotonic manifest version active at evaluation time. Same value the manifest spec calls manifest_version; the SDK type Namespace::flag_version() returns this. |
manifest_etag | no | string | null | ETag from the loader if available. Useful for joining records to the exact archive bytes. |
unit_id_hash | conditional | string | null | SHA-256 hex digest of the entity identifier used for bucketing. See § Privacy filtering. Required if and only if a bucketing identifier was present in the evaluation context. |
unit_id_type | conditional | slug | The type tag of the bucketing identifier (user, account, device, session, …). Required when unit_id_hash is present. |
secondary_unit_ids | no | map<string, string> | Map of additional ID types to their SHA-256 hex digests, for cross-unit analysis. Empty object permitted. |
context_attributes | yes | map<string, scalar | string-list> | Privacy-filtered context attributes. Values are typed per the AttrValue enum: string, integer, float, boolean, or StrList (a JSON array of strings). See § Context attribute encoding. Empty object permitted. |
sdk_name | yes | string | E.g. "exd-client-rust", "exd-client-typescript". The value "exd-server" identifies records produced by the server-side evaluation path (POST /evaluate{,/all}). |
sdk_version | yes | string | Semver. |
request_id | no | string | null | Application-level request identifier when known. |
trace_id | no | string | null | OTel trace ID hex string when an OTel context is active. |
span_id | no | string | null | OTel span ID hex string when an OTel context is active. |
Context attribute encoding
context_attributes carries the (privacy-filtered) typed evaluation context. Each value is encoded per its AttrValue variant:
AttrValue variant | JSON encoding |
|---|---|
String(s) | JSON string |
Int(i) | JSON integer |
Float(f) | JSON number |
Bool(b) | JSON boolean |
StrList(v) | JSON array of strings |
StrList is the only collection type in schema version 1. Mixed-type arrays, nested objects, and arrays of non-string scalars are NOT part of the contract; producers MUST NOT emit them and consumers MAY reject records that contain them.
Variant value encoding
variant_value is an inline-table object with a type discriminator and a value payload. The five types correspond to the manifest spec's flag types:
type | value JSON encoding |
|---|---|
"bool" | JSON boolean |
"string" | JSON string |
"int" | JSON integer |
"float" | JSON number |
"json" | any JSON value |
A producer MUST emit type matching the flag's declared flag.type. A flag.type mismatch is an SDK bug.
Evaluation reasons
| Reason | When it applies | matched_rule_id |
|---|---|---|
matched_rule | A rule's predicate matched the context and that rule's variant was returned. | The rule's stable identifier ("rule-<index>" or rule's declared id). |
fallthrough | No rule matched and the env's variant was returned. | null |
off | The flag is in the kill-switch shape (a variant declared on the env block with no rules, or _-default returned). | null |
prerequisite_failed | A prerequisite flag returned a value that failed the dependency check. | null |
error | The SDK encountered an internal error and fell back to a default. The record is still emitted for observability. | null |
sdk_default | The caller passed a default to the eval call site and the SDK returned it because the flag is unknown or the SDK is uninitialized. | null |
This vocabulary is stable per § Stability and evolution. Earlier versions of exd used a single rule_matched string field; producers SHOULD emit evaluation_reason and matched_rule_id separately as of schema version 1.
Privacy filtering
exd records MUST NOT contain raw personally identifiable information at the record-emission boundary. Two filtering rules apply unconditionally:
1. Entity identifier hashing
The bucketing identifier (the value referenced by [segment.bucket].entity_id_attribute) is hashed with SHA-256 and emitted as unit_id_hash in lowercase hex. The raw identifier MUST NOT appear anywhere in the record.
A flag namespace MAY opt out of hashing by setting raw_entity_ids = true in namespace.toml (see namespace § raw_entity_ids). When set, the SDK emits the raw identifier in unit_id_hash (the field name is preserved for schema stability; the contents are no longer a hash). This option is intended for non-personal identifiers (internal account IDs, B2B tenant slugs) and SHOULD NOT be used for any field that could constitute PII.
2. Private attribute filtering
Attributes named in namespace.private_attributes (or per-flag flag.private_attributes) MUST be removed from context_attributes before record emission. Producers MUST also remove these attributes from secondary_unit_ids keys and values. The two lists are unioned at filter time; declaring an attribute on one does not unset the other. See namespace § private_attributes and flag § private_attributes.
Size cap
Records SHOULD NOT contain attributes whose values exceed 1 KiB after JSON serialization. Producers MAY truncate long values to a documented length and emit a T011 diagnostic when reading such records.
Stability and evolution
The schema is versioned by the integer schema_version field. Compatibility rules:
- Backward compatibility. Readers of version
NMUST accept versionN-1records. Producers of versionNMUST NOT remove fields that existed inN-1. - Field addition. New optional fields MAY be added at any version increment. Consumers MUST ignore unknown fields.
- Field removal or repurposing. Requires a major version bump. Producers and consumers from different majors are not interoperable.
- Enum value addition. New
evaluation_reasonvalues MAY be added at minor increments; consumers MUST tolerate unknown values by treating them aserrorfor analysis purposes (and emittingT012if they implement that diagnostic).
Schema version 1 is stable as of the spec acceptance date.
See also
- sinks — how records get from the SDK to a destination.
- warehouse-contract — the column schema for warehouse storage (the inverse of this on-the-wire shape).
- diagnostics — the
T-codevocabulary the analysis surface emits over records. - namespace §
raw_entity_ids/private_attributes— the manifest knobs that gate privacy filtering. - resolution algorithm — the
evaluation_reasonandmatched_rule_idvalues come from this walk.