Custom Conditions
showwhat ships with a handful of built-in condition types, but the real power is in extending it with your own. The condition system is fully open — any object with a type string passes schema validation, and you provide the evaluator function that decides what it means.
How evaluation works
When the resolver encounters a condition, it looks up the evaluator in the evaluators map by condition.type:
- If the type is
andoror, the resolver handles it internally (recursive composite evaluation) - Otherwise, it calls
evaluators[condition.type](...) - If no evaluator is found for the type, the condition returns
false(fail-closed)
This fail-closed default means typos in condition types or missing evaluators safely prevent a variation from matching, rather than accidentally enabling it.
Interfaces and types
ConditionEvaluator
Every condition evaluator is a single async function:
Each evaluator receives a single args object and returns Promise<boolean>.
| Parameter | Type | Description |
|---|---|---|
condition | unknown | Raw condition object from your definition |
context | Readonly<Context> | Resolution context |
annotations | Record<string, AnnotationValue> | Mutable record — write metadata here during evaluation |
deps | Readonly<Dependencies> | Injected runtime utilities (hash functions, fetchers) |
depth | string | Depth tracking string for nested conditions |
Return true to match, false to skip the variation.
ConditionEvaluators
ConditionEvaluators is a Record<string, ConditionEvaluator> mapping condition type strings to evaluator functions.
registerEvaluators infers the condition key union from the input, so you get type-safe registration without an explicit key generic.
Writing and registering custom conditions
Write an async function that receives an args object containing the condition, context, and annotations, then returns a boolean. Use registerEvaluators to merge your evaluators with the built-ins — see the percentage rollout example below for a complete example.
registerEvaluators returns a new ConditionEvaluators map that includes all built-in evaluators plus yours. You cannot override the reserved composite types and and or — attempting to do so throws an error.
Passing evaluators
Pass your evaluators via the evaluators option:
import { showwhat } from "showwhat";
const result = await showwhat({
keys: ["new_checkout"],
context: { env: "prod", userId: "user-42" },
options: { data, evaluators: myEvaluators },
});Or at the resolver level:
import { resolve } from "showwhat";
const results = await resolve({
definitions,
context: { env: "prod", userId: "user-42" },
options: { evaluators: myEvaluators },
});Writing definitions with custom types
Custom condition types work in YAML/JSON definitions just like built-in ones. Any object with a type field that isn't a reserved built-in will pass schema validation:
definitions:
new_checkout:
variations:
- value: true
conditions:
- type: percentage
value: 25
- value: falseCustom conditions compose with and/or and nest freely:
definitions:
dark_mode:
variations:
- value: true
conditions:
- type: and
conditions:
- type: env
value: prod
- type: percentage
value: 50
- value: falseFallback evaluator
If you need to handle unknown condition types gracefully — for example, in a preview or simulator — pass a fallback evaluator via options.fallback. When the resolver encounters a condition type with no registered evaluator, it calls the fallback instead of throwing.
const result = await showwhat({
keys: ["my_flag"],
context: { env: "prod" },
options: {
data,
fallback: async ({ condition, context }) => {
return false;
},
},
});Without a fallback, unknown condition types return a ResolutionError for the affected key.
Annotations
Evaluators can write metadata to the annotations record during evaluation. See Annotations for the full lifecycle, reading results, and the built-in annotations condition type.
Dependency injection
Custom evaluators often need runtime utilities — hash functions, async fetchers, lookup tables — that don't belong in the evaluation context. The deps parameter lets you inject these at the call site and access them in every evaluator.
Passing deps
Pass deps alongside context at any entry point:
import { showwhat, registerEvaluators } from "showwhat";
const result = await showwhat({
keys: ["new_checkout"],
context: { env: "prod", userId: "user-42" },
deps: { hash: murmurhash.v3 },
options: { data, evaluators: myEvaluators },
});deps is optional and defaults to {}. It is passed as Readonly to evaluators — evaluators should read from it, not mutate it.
Typed deps with evaluator contracts
Each evaluator can define an interface for the deps it requires and cast internally:
// rollout-evaluator.ts
interface RolloutDeps {
hash: (id: string) => number;
}
const rolloutEvaluator: ConditionEvaluator = async ({ condition, context, deps, annotations }) => {
const { value } = condition as { type: "rollout"; value: number };
const { hash } = deps as RolloutDeps;
const userId = String(context.userId);
const bucket = hash(userId) % 100;
annotations.bucketId = bucket;
return bucket < value;
};Users can type-check their deps at the call site by annotating the variable:
import type { RolloutDeps } from "./rollout-evaluator";
import type { SegmentDeps } from "./segment-evaluator";
// TypeScript validates the shape here
const deps: RolloutDeps & SegmentDeps = {
hash: murmurhash.v3,
fetchSegments: mySegmentFetcher,
};
const result = await showwhat({
keys: ["feature"],
context: myContext,
deps,
options: { data, evaluators },
});deps vs context
context | deps | |
|---|---|---|
| Contains | Evaluation data (env, userId, region) | Runtime utilities (functions, clients) |
| Mutability | Readonly | Readonly |
| Schema-validated | Yes | No |
| Used by built-in evaluators | Yes | No |
Summary
| API | Purpose |
|---|---|
ConditionEvaluator | Type signature for evaluator functions |
ConditionEvaluators | Evaluators map — Record<string, ConditionEvaluator> |
Annotations | Type for evaluator-written metadata |
Dependencies | Type for injected runtime utilities |
registerEvaluators(extra) | Merge custom evaluators with built-ins |
options.evaluators | Pass your evaluators to showwhat() or resolve() |
deps | Pass runtime utilities to showwhat() or resolve() |
Examples
Percentage rollouts
Percentage rollouts assign users to buckets deterministically. Inject the hash function via deps so your evaluator stays pure and testable:
import type { ConditionEvaluator } from "showwhat";
interface RolloutDeps {
hash: (id: string) => number;
}
const rolloutEvaluator: ConditionEvaluator = async ({ condition, context, deps }) => {
const { value } = condition as { type: "percentage"; value: number };
const { hash } = deps as RolloutDeps;
const userId = context.userId;
if (!userId) return false;
return hash(String(userId)) % 100 < value;
};import murmurhash from "murmurhash";
import { showwhat, registerEvaluators } from "showwhat";
const myEvaluators = registerEvaluators({ percentage: rolloutEvaluator });
const result = await showwhat({
keys: ["checkout_redesign"],
context: { env: "prod", userId: "user-42" },
deps: { hash: murmurhash.v3 },
options: { data, evaluators: myEvaluators },
});definitions:
checkout_redesign:
variations:
- value: "variant-b"
conditions:
- type: env
value: prod
- type: percentage
value: 10 # 10% rollout
- value: "control"TIP
showwhat handles the evaluation — tracking impressions, measuring conversion, and statistical analysis are your responsibility. Pipe results into your analytics tool of choice.
User targeting
A condition that targets users based on attributes from context:
const myEvaluators = registerEvaluators({
userAttribute: async ({ condition, context }) => {
const { attribute, value } = condition as {
type: "userAttribute";
attribute: string;
value: string | string[];
};
const actual = context[attribute];
if (!actual) return false;
const allowed = Array.isArray(value) ? value : [value];
return allowed.includes(String(actual));
},
});definitions:
premium_feature:
variations:
- value: true
conditions:
- type: userAttribute
attribute: plan
value: [pro, enterprise]
- value: falseNext steps
- Conditions for all built-in condition types
- Custom Data Sources to pair custom evaluators with your own storage
- Context for details on the context object and when to use
depsvscontext