Skip to content
Get started

Edge evaluation

f69’s evaluator is a small Rust function that compiles to native or WebAssembly. Given a manifest, an entity context, and now_utc (evaluation instant), it returns a JSON value per flag (null, boolean, number, string, array, or object). The rules in this page are the frozen public contract: SDKs that re-implement evaluation client-side must reproduce them exactly, especially the bucket hash. Legacy manifests that only used booleans for default_value and outcomes behave the same on the wire (true / false).

f69-edge uses the server clock (Utc::now()) for each request. The public HTTP API does not accept a client-supplied time for now_utc. The Cloudflare WebAssembly entrypoint takes an explicit evaluated_at (RFC 3339) in the request body so the host and tests are unambiguous.

A manifest is the per-environment snapshot of flags and segments plus a version string:

{
"schema_version": 6,
"manifest_version": "live",
"project": "default",
"environment": "prod",
"flags": [ /* Flag */ ],
"segments": [ /* Segment */ ]
}

Each flag carries a default_value (any JSON) and an ordered list of rules.

Pseudocode:

evaluate(flag, ctx, now_utc) =
for rule in flag.rules:
if rule.when.all(hold_on(ctx, now_utc)):
return resolve(rule.outcome, ctx)
return flag.default_value
  • Rules are evaluated in manifest order.
  • The first rule whose when predicates all hold wins.
  • An empty when list matches unconditionally (catch-all rule).
  • Predicates inside when are ANDed at the top level; use and / or / not for nested logic. Predicate results are always boolean; only the flag payload (default_value, fixed outcome, or rollout variant value) is arbitrary JSON.

Each item in the JSON array includes key, value (any JSON), reason (TARGETING_MATCH, SPLIT, DEFAULT, FALLBACK, or ERROR), and version (the manifest version string). Treat value as a generic JSON value in your SDK, not strictly a boolean.

All predicates are JSON-tagged by op (snake_case) and read from context.attributes[key] or context.id / context.type.

opShapeTrue when
eq / neq{key, value}JSON equality / inequality
in / not_in{key, values[]}attr matches any / no member of values
gt / gte / lt / lte{key, value}numeric comparison via as_f64
entity_id_in{values[]: string}context.id is in values
entity_type_eq{value: string}context.type == value
and / or{predicates[]}every / any child holds (empty and is true; empty or is false)
not{predicate}inner predicate is false
in_segment{segment: string}context is a member of the named segment (segment membership also uses now_utc for time predicates)
bucket{by, seed, range: [u32, u32]}bucket(seed, by, ctx) is in the inclusive [lo, hi] basis-point range
before_instant{at: string} (RFC 3339)now_utc < at (strict)
after_instant{at: string} (RFC 3339)now_utc >= at (inclusive)
local_time_windows{timezone, windows[]}IANA timezone; each window has weekdays (0=Sun … 6=Sat) and start / end as HH:MM; true if any window matches in local time; empty list is false

A missing attribute makes attribute predicates evaluate to false - predicates fail closed, never silently true.

A rule whose outcome is a rollout hashes the context into a basis-point bucket (0 - 9999) and picks a variant by cumulative weight:

{
"type": "rollout",
"by": { "kind": "entity_id" },
"seed": "new_checkout",
"variants": [
{ "weight": 2500, "value": true },
{ "weight": 7500, "value": false }
]
}

Each variant’s value may be any JSON (same as a fixed value outcome), not only booleans.

Rules:

  • Variant weights are basis points and must sum to exactly 10 000.
  • Raising a variant’s weight pulls more entities across the line without reshuffling the ones already selected. Ramps are sticky.
  • by.kind is entity_id (most flags) or attribute (cohort stickiness - everyone in the same workspace flips together).

Phased or scheduled exposure: a static rollout (weights in the manifest) plus republish to change weights is the sticky ramp. Time predicates in when only window which rules apply; they do not interpolate a new percentage on every tick in one published manifest. A future clock-driven percentage ramp in a single publish is a separate design.

The bucket hash is a public, byte-for-byte contract. Changing it would silently reshuffle every live rollout.

  • Hash: SipHash-1-3 (1 compression round, 3 finalization).
  • Key: 128-bit zero key ([0u8; 16]). No per-deployment secret.
  • Input: UTF-8 of the canonical string below.
  • Bucket: (digest % 10_000) as u32.
HashByCanonical input string
entity_id"{seed}:{context.type}:{context.id}"
attribute { key } (present)"{seed}:{stringified_attr_value}"
attribute { key } (missing)"{seed}:"

Stringification rules for attribute values:

  • null → empty string
  • bool"true" / "false"
  • string → the string verbatim (no extra quotes)
  • numbers / arrays / objects → compact JSON (no whitespace)

Pinned golden examples:

seedtypeidbucket
new_checkoutuseru-alice1682
new_checkoutuseru-bob5811
other_flagworkspacews-429570
  • Default to the flag key as the seed, so two concurrent 10 % rollouts target different 10 % cohorts of users.
  • To reshuffle everyone after a botched launch, change the seed ("{flag_key}:v2").
  • To coordinate cohorts across flags, share the seed.

A segment groups entities reusably:

{
"key": "beta-testers",
"included": [ { "type": "user", "id": "u-alice" } ],
"excluded": [ { "type": "user", "id": "u-banned" } ],
"rules": [
[ { "op": "eq", "key": "is_internal", "value": true } ],
[ { "op": "eq", "key": "country", "value": "NG" },
{ "op": "gte", "key": "account_age_days", "value": 30 } ]
]
}

Membership order:

  1. If (type, id) is in excluded → not a member.
  2. Else if it’s in included → member.
  3. Else if any of rules matches → member.
  4. Otherwise → not a member.

Segments may reference other segments via in_segment, but the reference graph must be a DAG.

  • evaluate_flag propagates UnknownFlag(key) to the caller.
  • evaluate_all is fail-closed: if a single flag errors, the runtime returns that flag’s default_value and keeps going. One bad flag cannot break the rest of the manifest.
  • Compile-time errors (UnknownSegment, DuplicateSegment, SegmentCycle, RolloutInvalid, TimePredicateInvalid) are caught when the manifest is prepared, not on the hot path.

The bias is always toward default_value; an evaluation error never replaces a resolved value with an arbitrary substitute beyond that fallback.