Skip to content
Console
Browse documentation
Reference

Feature-flag exposure verification

Prove that your application recorded the exact flag decisions of a controlled test, under which definition version, and whether a local and a remote decision are comparable.

On this page

After your agent wires feature flags into an application, it runs a controlled test and asks Anectico: did the application record these exact flag exposures, with this value, under this flag definition version, and are the local and the remote decision of one test case comparable? The verification read answers for up to 20 exposures at a time. It changes nothing, stores no report and runs no model.

An exposure is a $feature_flag_called event your application recorded when it used a flag value. It is application-reported use: it never proves that a person saw the treatment, and it says nothing about code paths that record no exposure. A decision your agent fetches itself (for example with decide_feature_flags or anectico flags decide) checks Anectico's evaluation, not your application; it is never evidence here.

Surface How
REST POST /api/v1/instrumentation/flags/verify with the JSON request as the body
MCP execute_read_action with action verify_flag_integration and the JSON request as arguments
CLI anectico doctor flags --file request.json (plus the usual --project and --json)

Project-bound keys may omit project_id; naming a different project is refused. The CLI refuses a file whose project_id contradicts --project.

What the application records

Each served flag read records one exposure with these reserved properties. The JavaScript SDK (server-side, with a flag-read key held only in server configuration) records them for you; an application without an SDK sends them itself with the capture API.

Property Meaning
$feature_flag The flag key.
$feature_flag_response The value used: true, false or a variant string. false and an ordinary default are values.
$feature_flag_version The exact flag definition version the value was evaluated under, for example 2026-10-15T09:30:12.123456Z (UTC, six fractional digits). Only for a successful evaluation; never for a fallback or a bootstrap-only value. It versions the flag definition, not person properties, groups, cohorts or audiences.
$feature_flag_evaluation_status evaluated, evaluation_error, unavailable_flag or unsupported_targeting. Missing or unknown is never treated as success.
$feature_flag_is_fallback true when the value is an error or dependency fallback. Missing means unknown, not false.
$feature_flag_evaluation remote or local.
$feature_flag_snapshot_version The local snapshot the value came from (local only).
$feature_flag_test_case_id Your opaque test-case ID, at most 200 printable bytes. It correlates; it proves nothing about execution or inputs.
$feature_flag_input_context { "basis", "required_inputs_present", "input_binding" }, at most 2 KiB, never a property value.

basis is explicit_inputs when the evaluation used only inputs your application supplied, hydrated_context when a remote evaluation also used stored person properties, groups, cohort or audience membership, and unestablished when unknown. A remote decision reports its own basis per flag; it is hydrated_context whenever stored context was consulted, even if your supplied values match it. required_inputs_present is true when your application supplied every targeting input the flag names.

input_binding is your application's own declaration of the explicit inputs it evaluated with: sha256: followed by 64 lowercase hex characters, the SHA-256 of this UTF-8 JSON text with object keys sorted by code point at every level, arrays in order, no whitespace, absent keys omitted and null kept:

{"distinct_id":"test-person","flag_key":"checkout_v2","format":"anectico.flag_inputs.v1","groups":{},"person_properties":{"plan":"pro","seats":3,"trial":null},"project_id":"22222222-2222-4222-8222-222222222222"}

Absent person_properties or groups are written as {}. Anectico compares two bindings for equality only; it never recomputes or authenticates them.

Where the version comes from

POST /api/v1/decide returns, besides feature_flags, project_id, feature_flag_versions (the version per evaluated flag) and feature_flag_results (per flag: evaluation_status, is_fallback and input_context with basis and required_inputs_present). A flag whose stored context could not be read takes its error path: its default value with evaluation_error, is_fallback: true and no version. errors_while_computing_flags remains an aggregate and does not describe one flag. The local-evaluation snapshot carries feature_flag_versions too.

The request

{
  "window_start": "2026-10-15T09:00:00Z",
  "window_end": "2026-10-15T10:00:00Z",
  "exposures": [
    {
      "message_id": "test-run-42-flag-local",
      "source_id": "7f1e2d3c-4b5a-4968-8776-5544332211aa",
      "flag_key": "checkout_v2",
      "expected_value": true,
      "expected_flag_version": "2026-10-15T09:30:12.123456Z",
      "evaluation_mode": "local",
      "test_case_id": "case-42",
      "expected_evaluation_status": "evaluated",
      "expected_is_fallback": false,
      "expected_input_context": {
        "basis": "explicit_inputs",
        "required_inputs_present": true,
        "input_binding": "sha256:a3a347b524720b40b8f3ccf93b65a347f7d558d302905652a2949a85fbbaa92a"
      },
      "distinct_id": "test-person"
    }
  ]
}
Field Meaning and bounds
window_start, window_end Event-time window [start, end), millisecond precision, at most one hour.
exposures 1 to 20; the answer keeps their order. Each (message_id, source_id) appears once.
message_id, source_id The exact message_id the exposure was sent with and the source that admitted it (as for event verification).
flag_key The exact flag key.
expected_value Required: true, false or a variant string. false is an expectation; null is refused.
expected_flag_version Required exact version in the six-digit form above.
evaluation_mode remote or local.
test_case_id Required. All exposures with the same flag_key and test_case_id must name the same distinct_id and the same session_id expectation.
expected_evaluation_status, expected_is_fallback Required; false must be written.
expected_input_context Required basis; required_inputs_present and input_binding are compared exactly, including absence.
distinct_id, session_id The declared identity (checked as an alias) and an optional session (present means check it).

The answer

Per exposure, every check has a state (observed_match, mismatch, not_observed, withheld, unavailable, ambiguous) and a reason: event, distinct_id, session_id, identity, flag_key, value, definition_version, evaluation_mode, test_case_id, evaluation_status, is_fallback, input_basis, required_inputs_present and input_binding. An exposure without a recorded version is a definition_version mismatch with reason unversioned. The outcome is verified only when every check matched.

recorded shows what the exposure recorded: the value's value_presence and value_type (the value itself is never returned), the definition_version, evaluation_mode, snapshot_version, evaluation_status, is_fallback and input_context, each only when well formed. The recorded test-case ID is never returned. current_definition gives the flag's version now, as context for a version mismatch; a current definition never certifies an older evaluation.

comparisons has one entry per (flag_key, test_case_id):

  • state is established only for exactly one selected local and one selected remote exposure, both fully matched, both evaluated with is_fallback recorded as false, both under the same version, both with basis explicit_inputs, required_inputs_present: true and equal input_binding. agreement then says whether the two recorded values are equal (agree) or not (disagree).
  • Otherwise state is not_comparable with reasons: different_definition_versions, missing_authoritative_version, missing_inputs, unsuccessful_evaluation, unsupported_local_targeting, comparison_pair_incomplete, comparison_pair_ambiguous or context_not_established. These explain why the decisions cannot be compared; they do not mean the treatments differ.
  • local_index and remote_index name the exact pair; input_binding_assurance is always sender_declared.

What an established comparison means: your application recorded, for both decisions, the same definition version, successful evaluation and the same declaration of explicit inputs. It does not authenticate your application, its inputs or any stored context, and two equal fallbacks, a snapshot digest, a version or a test-case ID alone never establish it. A remote decision that used stored person, group, cohort or audience context is context_not_established; today's context is never reread as proof of the context at evaluation time.

Local evaluation supports person-property targeting only. Flags that target groups, cohorts or pinned audiences return unsupported_targeting locally (no value, no exposure); evaluate those remotely.

Scopes

Scope What it adds
flags:read Required. (MCP also needs mcp:read.)
analytics:read Required. The exposure read.
persons:read Canonical identity. Without it identity is withheld.
agents:content:read The recorded flag fields. Without it every flag-field check is withheld and nothing recorded is returned. The project's content policy, sensitive declarations and stored redactions can still withhold a field.

A withheld check never lets an exposure verify or a comparison be established. If a scope, the content policy or the selected exposures change while the read runs, no report is released.

Keep the keys apart: the application uses a project-bound key with only flags:read for decisions and a separate capture key for exposures, both held in server configuration. Your agent's verification key is a third credential; it grants no capture.

Limits

The limits are those of event verification (one hour, 20 exposures, the same scan, memory, time and concurrency caps) and are returned under limits. A reached limit makes every exposure unavailable, never not_observed. Flag and event verifications of one project share the two concurrent reads.

Using it in a test

  1. In the application, read each flag with a test-case ID (getFeatureFlagDetails(key, { testCaseId }) remotely, evaluate(key, distinctId, properties, { testCaseId }) locally) and keep the exposure's message_id.
  2. For a comparison, evaluate locally and remotely with the same explicit inputs and record both.
  3. Verify with the versions and bindings your application recorded. If an exposure is not_observed right after the test, wait briefly and verify again with the same request.
  4. Report exactly what was verified and what an established or not comparable result means.

To verify flag exposures as the flags job with a single verdict, see integration job verification. What a verified exposure does not prove, and how long to wait, are on What integration verification proves and what it does not.