Skip to content

Evaluation

Evaluation is the act of resolving a flag or config to a concrete value for a given context. It's a pure function: same ruleset + same context = same result, every time, with a structured reason explaining why.

The hot path

http
POST /api/v1/envs/{envId}/evaluate
Content-Type: application/json
Authorization: Bearer act_…

{
  "context": { "userId": "u_42", "plan": "pro", "country": "US" },
  "keys": ["new-onboarding", "checkout.max-items"]
}

Response:

json
{
  "environmentId": "9f7a32b5-…",
  "version": 142,
  "evaluations": {
    "new-onboarding": {
      "value": true,
      "defaultValue": false,
      "reason": { "kind": "rule_match", "ruleIndex": 0, "matchedValue": true }
    },
    "checkout.max-items": {
      "value": 50,
      "defaultValue": 100,
      "reason": { "kind": "rule_match", "ruleIndex": 1, "matchedValue": 50 }
    }
  }
}

evaluations is a flat map keyed by primitive key. SDKs partition the map into flags and configs at the call site using the (key → kind) info they already loaded.

Selecting keys

keys controls which primitives are evaluated:

  • ["a", "b"] — evaluate exactly these keys. Missing keys come back as null envelopes.
  • "*" or omitted — evaluate every flag and config in the environment.

For SDK callers, "*" once at startup (then on every push update once SSE lands) is the canonical pattern: bulk-resolve everything, cache in process memory, refresh on push.

The reason envelope

Every evaluation comes back with a reason describing why that value was returned. Possible shapes:

json
{ "kind": "default" }

…no rule matched (or no rules exist). The default value was returned.

json
{ "kind": "rule_match", "ruleIndex": 0, "matchedValue": true }

…rule at index 0 matched and the rule's value was returned. matchedValue repeats the value for symmetry with the env-default case.

json
{ "kind": "rule_match", "ruleIndex": 0, "matchedValue": true,
  "trace": { /* tree of which sub-conditions matched */ } }

…with verboseReason: true on the request, the same envelope plus a full predicate-tree trace showing which sub-conditions evaluated to which booleans. Used by the dashboard's "Why did I get this value?" panel.

json
{ "kind": "fallback", "error": "ruleset_not_cached" }

…the environment's materialized ruleset isn't cached (only happens on a fresh environment that hasn't been written to yet). The default-of-default is returned (false for boolean, "" for string flags / configs, 0 for number, null for json).

Time-travel

Pass asOf: "2026-04-15T18:00:00Z" to evaluate against the ruleset that was live at that timestamp. The audit log is the source — every change since the cutoff is unwound and the evaluator runs against the historical state.

json
{
  "context": { "userId": "u_42" },
  "keys": ["new-onboarding"],
  "asOf": "2026-04-15T18:00:00Z"
}

Useful for debugging "why did this user see X yesterday?" without reconstructing state by hand. Capped at 90 days of history.

Counterfactual preview

To answer "what would happen if I made this change?" — without making the change — there's a separate endpoint:

http
POST /api/v1/envs/{envId}/evaluate/preview

You hand it a transient ruleset and an array of contexts. It runs both the live ruleset and your hypothetical against each context and returns side-by-side {live, preview} envelopes. The transient ruleset is never persisted and the env's version is unchanged. See Agents and AI and API reference: Evaluation for details.

History

To see how a single context's resolution changed over time:

http
POST /api/v1/envs/{envId}/evaluate/history

Pass a context, a since timestamp, and (optionally) a single primitiveKey. The endpoint replays the audit log within (since, until] and emits per-primitive (timestamp, version, value, reason) change-points. A capped result set sets truncated: true rather than returning a 500. Window capped at 90 days.

Versioning and SSE

Every environment carries a monotonic version that increments on every committed write. The version is in:

  • The version field of every /evaluate response.
  • The ETag: W/"<version>" header on GET /api/v1/envs/{envId}/ruleset/version — a cheap version-only endpoint for fallback polling. Send If-None-Match: W/"<N>" and you'll get 304 Not Modified if the version hasn't moved.
  • The audit row's version column.

SDKs use the version to invalidate cached evaluations: see a higher version, refetch.

Validation helpers

Two endpoints help dashboards and rule builders give inline feedback before saving:

  • POST /api/v1/validate/regex — does this regex compile under the portable RE2 subset? Returns { ok, error? }. Called on every keystroke in the rule builder.
  • POST /api/v1/envs/{envId}/validate/condition — is this condition tree valid for this primitive type? Returns per-path errors for inline display.
  • POST /api/v1/envs/{envId}/validate/evaluate — run a single draft rule against a context, either spliced into an existing primitive's rules or in isolation. Used by "preview this rule against my test contexts" UI.

See also