Skip to main content

Evaluation context

The evaluation context is the typed key-value map that callers pass at flag evaluation time. It is the input to every segment predicate and bucket assignment. It is supplied by the SDK or the server-side caller; it is never persisted in the manifest.

This page is the reference for how attribute names, types, and missing values are resolved by predicates and buckets.


What the context is

A flat map of string keys to typed values that describes the entity being evaluated against — typically a user, device, organization, or agent run.

{
"user.id": "u_abc123",
"user.country": "US",
"user.plan": "pro",
"user.created_at_days": 47,
"user.beta_opt_in": true,
"app.version": "2.4.1",
"device.os": "ios"
}

Keys are strings; values are scalars (string, integer, float, boolean) or — if the SDK supports it — string sets. The context MUST be flat: no nested objects. Hierarchical data is encoded by namespacing the key (user.country as a single flat key, not as a sub-object under user). The dot is a convention for organizing the schema, not a structural separator the SDK looks at.


Attribute types

The manifest matches against four context value types:

TypeDescriptionExample
stringUTF-8 string"US"
integerSigned 64-bit integer47
floatIEEE 754 double3.14
booleantrue / falsetrue

A string_set type and its matching operators (set_contains, set_not_contains) are reserved for a future minor version. Multi-valued attributes today are typically encoded by issuing several scalar attributes with conventional prefixes (e.g., user.role.admin = true, user.role.beta_tester = true).


Attribute lookup

Every predicate atom names the attribute it tests via the attribute field. The lookup is exact — the SDK uses the attribute string as a literal key into the flat context map.

If the key is not present, the attribute is missing; see § Missing-attribute behavior.

Namespacing convention

Attribute names typically use a dot-separated prefix:

PrefixUsed for
user.*The current end-user (e.g., user.id, user.country, user.plan)
app.*The calling application (e.g., app.version, app.platform)
org.*The user's organization or tenant within the calling app (e.g., org.id, org.plan, org.region)
device.*The device or environment (e.g., device.os, device.user_agent, device.locale)
request.*The current request (e.g., request.id, request.geo_country)
entity.*Generic entity fields when none of the above fit

The prefix is not parsed by the SDK; it is a convention to organize a team's context schema. Predicates can reference any attribute key regardless of its prefix.

Reserved attribute names

The schema does not reserve any attribute names. The bucket field entity_id_attribute names which attribute carries the bucket identity, so authors are free to choose user.id, device.id, entity.id, or any other path that reads naturally for their context schema.


Missing-attribute behavior

In an attribute atom

OperatorIf attribute is missing
eq, neqfalse
gt, gte, lt, ltefalse
in, not_infalse
contains, not_contains, starts_with, ends_withfalse
semver_*false
is_setfalse
is_not_settrue

A missing attribute never errors. The atom evaluates to false (or true for is_not_set); the surrounding compound expression continues evaluating normally.

In a bucket segment

If the attribute named by entity_id_attribute is missing, its value is not a string, or its value is the empty string "", the entity is not a member of the bucket segment.

  • Integer or boolean values are treated as missing for bucket purposes — buckets require a non-empty string identifier.
  • The empty-entity-id rejection prevents an "anonymous" entity from being deterministically placed in the same bucket and quietly skewing percentage rollouts.

In a segment reference

{ segment = "<key>" } evaluates the referenced segment in full against the context. If the referenced segment cannot determine membership (predicate is false, or bucket attribute is missing), it returns false.


Type-mismatch behavior

If the runtime attribute type does not match what the operator expects (for example, user.country is an integer but the predicate uses op = "contains"), the atom evaluates to false. The predicate does not error.

For eq and neq with non-numeric operand pairs whose TOML types differ (e.g., a string attribute compared against an integer operand), the atom evaluates to false for eq and true for neq. The numeric-widening rule (below) only applies when both sides are numeric.

Inferred attribute types and runtime mismatch

The linter infers an attribute type per attribute name from the operator + operand pairs the manifest uses, and surfaces cross-file inconsistencies as E034. The inferred map travels with a successfully loaded manifest; at evaluation time, an attribute whose runtime type disagrees with the inferred type short-circuits the rule:

  • The plain entry point Namespace::eval returns an EvalResult whose rule_matched is RuleMatched::AttrTypeMismatch { attribute, expected, actual }, with the SDK fallback value (the same zero value used for RuleMatched::SdkDefault).
  • The typed entry points (eval_bool, eval_string, eval_i64, eval_f64, eval_json) raise the mismatch as EvalError::AttrTypeMismatch.

Attributes the manifest does not constrain (used only via is_set / is_not_set, or never referenced) are NOT type-checked at runtime. The check is purely additive: any attribute the manifest does not mention is passed through unchanged.


Non-finite attribute values

A float attribute value that is NaN, +Inf, or -Inf at evaluation time MUST cause every comparison atom referencing that attribute to evaluate to false, with three exceptions:

  • is_set returns true (the attribute is present).
  • is_not_set returns false.
  • neq returns true (consistent with IEEE 754 — NaN compares unequal to everything, including itself; the spec extends this to ±Inf for cross-implementation determinism).

This rule pins behavior that would otherwise drift across implementations following IEEE 754 directly. Under raw IEEE 754, NaN > x, NaN < x, NaN >= x, NaN <= x, and NaN == x are all false while NaN != x is true; ±Inf follows ordinary ordering rules. The spec adopts the IEEE 754 outcome for eq / neq but also forces gt / gte / lt / lte / semver_* to return false on NaN or ±Inf so attribute-side non-finite values can never bypass a numeric guard.

Authors SHOULD ensure SDK code never passes non-finite floats into the evaluation context — they are almost always a calculation bug upstream. The rule above guarantees deterministic decisions when they do appear.

Non-finite floats remain forbidden as variant values at lint time (E029); this section covers only attribute-side behavior at evaluation time.


Numeric comparison nuance

All numeric operators (eq, neq, gt, gte, lt, lte) accept either integer or float on either side. When attribute and operand differ in numeric type, both sides are widened to IEEE 754 double for the comparison. eq between 1 (integer attribute) and 1.0 (float operand) is true.

Even with widening defined, declare numeric attributes and operands with consistent types where possible — schemas are easier to reason about when the wire types match.


Telemetry implications

The evaluation context is not persisted anywhere by exd. Telemetry events record only:

  • exd.flag_key
  • exd.variant_key
  • exd.rule_matched
  • exd.flag_version
  • exd.entity_id — only the bucket attribute's value, not the full context
  • (Optional) context_attributes — the SDK can include the non-private subset; see namespace § private_attributes for the suppression mechanism.

Sensitive context attributes (PII, tokens, secrets) MAY appear in evaluations but MUST NOT appear in any logs the SDK or server emits. Configure [namespace].private_attributes and [flag].private_attributes for everything you don't want emitted.


  • Pick a small, stable schema. Five to fifteen attributes covers most products; sprawl makes context construction in code harder.
  • Always set the entity-id attribute. Bucket segments require it. Pick a stable, opaque identifier (UUID, ULID, internal user id), not an email or username.
  • Namespace attributes. Use user.*, org.*, app.* prefixes consistently. Reviewers reading a predicate should be able to tell at a glance what the attribute represents.
  • Encode booleans as booleans. A user.beta_opt_in of true / false is more idiomatic than a string "true" / "false". The string form would force every predicate to use eq against a string literal.
  • Avoid uppercase attribute values for case-sensitive operators. Either canonicalize on the calling side (country.toLowerCase()) or use a normalized derived attribute.

See also