Annotations
Annotations are a mutable Record<string, AnnotationValue> that evaluators can write to during condition evaluation. They provide a way to attach metadata — such as rollout buckets, matched segments, or debug info — to a resolution result without affecting the match/no-match outcome.
How annotations flow
- A fresh empty object
{}is created for each variation evaluation - The object is passed to every condition evaluator as the
annotationsparameter - Evaluators can write arbitrary key-value pairs to it during evaluation
- If the variation matches, its annotations are returned in
Resolution.meta.annotations - If the variation does not match, its annotations are discarded
variation evaluation starts
└─ annotations = {}
├─ evaluator A writes annotations.source = "rollout"
├─ evaluator B writes annotations.bucket = 42
└─ variation matches → annotations returned in resultWriting annotations
Any custom evaluator can write to the annotations parameter it receives:
const rolloutEvaluator: ConditionEvaluator = async ({ condition, context, annotations }) => {
const { value } = condition as { type: "percentage"; value: number };
const userId = context.userId;
if (!userId) return false;
const bucket = murmurhash.v3(String(userId)) % 100;
annotations.rollout = { bucket, threshold: value };
return bucket < value;
};The object is shared across all evaluators for a given variation, so multiple evaluators can write to different keys, building up a metadata record as conditions are evaluated.
Reading annotations
Annotations appear in the resolution result under meta.annotations:
const results = await showwhat({
keys: ["checkout_redesign"],
context: { env: "prod", userId: "user-42" },
options: { data, evaluators },
});
const entry = results["checkout_redesign"];
if (entry.success) {
console.log(entry.meta.annotations);
// { bucket: 42, threshold: 50 }
}The checkAnnotations condition
The checkAnnotations condition is a built-in modifier that lets you verify annotations set by previous evaluators. It evaluates its nested conditions against the annotations object as context, rather than the regular evaluation context.
definitions:
checkout_redesign:
variations:
- value: "variant-b"
conditions:
- type: rollout
value: 50
# Verify that the rollout evaluator wrote the expected annotation
- type: checkAnnotations
conditions:
- type: number
key: bucket
op: gte
value: 0
- value: "control"How it works
- Nested conditions are evaluated as an implicit AND (same as variation conditions)
- The current
annotationsobject is used as thecontextfor nested conditions - Nested conditions receive a fresh empty annotations object — they cannot write back to the parent annotations
- Existing condition types (
string,number,bool, etc.) work unchanged insidecheckAnnotations
Condition order matters
Since variation conditions are evaluated left-to-right, the checkAnnotations condition must come after the evaluators whose annotations it verifies. Placing it before will find an empty annotations object.
Flat key lookup
Built-in evaluators use direct property access (annotations[key]), not dot-notation traversal. When writing annotations for use with the checkAnnotations condition, use flat top-level keys:
// Good — evaluators can read these directly
annotations.bucket = 42;
annotations.threshold = 50;
// Nested objects can't be queried with built-in evaluators
// Use a custom evaluator if you need to verify nested structures
annotations.rollout = { bucket: 42, threshold: 50 };Seeding annotations
By default, each variation starts with an empty annotations object. You can provide a createAnnotations factory in ResolverOptions to seed initial values:
const results = await resolve({
definitions,
context: { env: "prod" },
options: {
evaluators,
createAnnotations: (definitionKey) => ({ source: "preview", key: definitionKey }),
},
});The factory is called once per variation attempt with the definition key (or undefined when calling resolveVariation directly without a key). Evaluators can overwrite seeded values during evaluation.
Per-variation lifecycle
Each variation evaluation creates a fresh annotations object (or a fresh copy from createAnnotations). If a variation fails (conditions don't match), its annotations are discarded and the next variation starts with a clean slate:
variations:
- value: "variant-a"
conditions:
- type: rollout # writes annotations.bucket = 80
value: 50 # fails (80 >= 50)
# annotations { bucket: 80 } discarded
- value: "variant-b"
conditions:
- type: rollout # writes annotations.bucket = 80
value: 90 # passes (80 < 90)
# annotations { bucket: 80 } returned with resultNext steps
- Custom Conditions for writing evaluators that use annotations
- Conditions for all built-in condition types
- Variations for how variations are structured and resolved