-
Notifications
You must be signed in to change notification settings - Fork 0
CEL Rules Reference
Each flag rule carries a when field that contains a CEL expression. CEL (Common Expression Language) is a formally specified, sandboxed expression language originally designed for Google's policy and admission-control systems (Kubernetes admission webhooks, Envoy, Cloud Armor, Cerbos). The SharpNinja FeatureFlags SDK implements a supported subset of CEL tailored for flag rule evaluation.
CEL was chosen over alternatives such as JSONLogic for the following reasons, documented in the planning artifact:
- Formal grammar - CEL has a complete published grammar. The parser produces a well-defined AST with deterministic precedence rules.
- Deterministic evaluation - Given the same input, CEL always produces the same output. There is no implicit type coercion that can silently change outcomes between versions.
- Sandbox safety - CEL expressions cannot perform I/O, allocate unbounded memory, or call arbitrary functions. The supported function set is closed and declared explicitly.
-
AOT compatible - The evaluator is a tree-walking interpreter over parsed AST nodes. It uses no
Reflection.Emit, no dynamic code generation, and no runtime expression compilation. It is fully compatible with iOS AoT compilation and NativeAOT. - Sub-millisecond compile-and-cache - Each expression is parsed once and cached by text. Subsequent evaluations skip parsing entirely.
- Proven scale - CEL is production-proven in Kubernetes, Envoy, and Cloud Armor.
| CEL type | JSON manifest source | Examples |
|---|---|---|
boolean |
true, false
|
true, false
|
string |
Quoted string literal |
'us', "alpha"
|
number |
Integer or decimal literal |
42, 3.14, -1
|
list |
List literal or context value |
[1, 2, 3], user.roles
|
map |
Map literal or context value |
{"key": value}, context.metadata
|
null |
null literal |
null |
All arithmetic uses decimal arithmetic internally. There is no distinction between integer and floating-point at the expression level; both JSON integer and number flag values map to the CEL number type.
| Operator | Description | Example |
|---|---|---|
== |
Equal | user.region == 'us' |
!= |
Not equal | tier != 'free' |
< |
Less than | score < 50 |
<= |
Less than or equal | score <= 100 |
> |
Greater than | version > 5 |
>= |
Greater than or equal | score >= 80 |
String comparison uses ordinal byte-order comparison. Number comparison uses decimal arithmetic. Comparing a string to a number is a type error.
| Operator | Description | Example |
|---|---|---|
&& |
Logical AND | user.region == 'us' && score >= 50 |
|| |
Logical OR | tier == 'gold' || tier == 'platinum' |
! |
Logical NOT | !feature.disabled |
&& and || require both operands to be boolean. ! requires a boolean operand.
| Operator | Description | Example |
|---|---|---|
+ |
Addition or string concatenation |
score + 5, user.first + '-' + user.last
|
- |
Subtraction or numeric negation |
total - discount, -1
|
* |
Multiplication | quantity * price |
/ |
Division | total / count |
% |
Modulo | index % 2 |
Division and modulo by zero throw an evaluation error. + concatenates strings when either operand is a string.
| Operator | Description | Example |
|---|---|---|
in |
Tests whether a value appears in a list | user.role in ['driver', 'dispatcher'] |
The right-hand side of in must be a list or a context value that holds a list. Strings are not enumerable and cannot be used as the right-hand side of in.
condition ? thenValue : elseValue
The condition must be a boolean expression. The then and else branches may return any type, but both branches should return the same type when used inside a boolean predicate. Ternary is right-associative.
score >= 50 ? 'high' : 'low'
(score >= 80 ? (tier == 'gold' ? true : false) : false) == true
Access a field of a context value or nested map using .:
user.region
user.role
context.metadata.tenantId
If user is set in the evaluation context as a dictionary, user.region reads the value at key "region".
Access a map or list value by key or index using [...]:
user["region"]
tags[0]
{"min": 1, "max": 10}["min"]
String keys look up map entries. Integer indexes access list elements (zero-based). Out-of-range integer indexes throw an evaluation error.
{"key": value, "other": value2}
Map keys must be string expressions. Values may be any type.
{"region": "us", "tier": "gold"}["region"] == "us"
[1, 2, 3].filter(n, {"min": 1, "max": 2}["min"] <= n) .exists(n, n == 1)
[value1, value2, value3]
Elements may be of any type. Lists are the operand for macros and the right-hand side of in.
["driver", "admin"]
[1, 2, 3]
Macros operate on lists. They use the syntax list.macroName(variable, expression) where variable is a locally-scoped loop variable bound to each element.
Macros are limited to 512 iterations per evaluation. Exceeding this limit throws an evaluation error.
Returns true if the predicate is true for at least one element.
user.roles.exists(role, role == 'admin')
tags.exists(t, t == 'beta-tester')
Returns true if the predicate is true for every element. Returns true for an empty list.
permissions.all(p, p != 'root')
Returns a new list containing only elements for which the predicate is true.
scores.filter(s, s > 50)
tags.filter(t, t != 'internal')
filter returns a list and is typically chained with another macro:
scores.filter(s, s > 50).exists(s, s > 80)
Returns a new list by applying the expression to each element.
tags.map(t, t)
roles.map(r, r == 'admin')
map returns a list. It is typically chained with another macro.
Returns true if the predicate is true for exactly one element.
assignments.exists_one(a, a.primary == true)
The following functions are defined by the SharpNinja FeatureFlags SDK. No other function names are accepted; calling an unknown function is a syntax error.
Compares two semantic version strings. Returns a negative integer if a < b, zero if a == b, and a positive integer if a > b. Both arguments must be non-empty strings parseable as semantic versions (e.g. "2.3.4", "v1.0.0-beta.1").
semver_compare(SemanticVersion, '2.3.0') >= 0
semver_compare(SemanticVersion, '3.0.0') < 0
Aliases accepted: version_compare, semverCompare.
Returns true if version satisfies the given constraint. The constraint is a comparison operator followed by a version string. Supported operators: >=, <=, ==, !=, >, <. A bare version without an operator defaults to ==.
semver_satisfies(SemanticVersion, '>=2.3.0')
semver_satisfies(SemanticVersion, '<3.0.0')
semver_satisfies(SemanticVersion, '!=2.0.0')
Alias accepted: semverSatisfies.
Assigns the current evaluation to a deterministic numeric bucket between 0 and 100 (exclusive) and returns true if the bucket value is less than threshold.
The bucket is computed by hashing the concatenation of ProductId, ReleaseId, FlagKey, and the string representation of discriminator using FNV-1a 64-bit. Because all four inputs are fixed for a given (flag, user), the assignment is stable across repeated evaluations. See ADR-001 for the hash algorithm decision record.
threshold must be between 0 and 100 inclusive. A threshold of 0 means no one is included; 100 means everyone is included.
bucket(UserId, 10) -- 10% rollout keyed on UserId
bucket(TenantId, 50) -- 50% rollout keyed on TenantId
bucket(UserId, 100) -- all users
Calling bucket(discriminator) with one argument returns the raw numeric bucket value (a decimal between 0 and 100). This is useful for debugging or for constructing range-based rules:
bucket(UserId) >= 25 && bucket(UserId) < 75
Aliases accepted: percentage, percentage_bucket, percentageBucket.
Context variables are supplied by the calling application using EvaluationContext.Builder(). Any string key may be used. The following well-known keys are recognized by the SDK and resolved automatically.
| Key | Source | Type | Description |
|---|---|---|---|
ProductId |
Manifest + build | string | The product identifier from the manifest. Also available as productId. |
ReleaseId |
Manifest + build | string | The release identifier from the manifest. Also available as releaseId. |
FlagKey |
Runtime | string | The key of the flag currently being evaluated. Also available as flagKey. |
SemanticVersion |
Application | string | The semantic version of the running build. Used with semver_compare and semver_satisfies. |
ReleaseChannel |
Application | string | The release channel (e.g. canary, beta, stable). |
ReleaseBuild |
Application | string | The build identifier from CI. |
Environment |
Application | string | The deployment environment name. |
TenantId |
Application | string | The tenant identifier for multi-tenant deployments. |
Any additional key-value pair set via EvaluationContext.Builder().Set(name, value) is available in rules by name. Nested context values (e.g. a user object containing a dictionary) are accessible via dot notation or index notation.
EvaluationContext context = EvaluationContext.Builder()
.Set("user", new Dictionary<string, object?> {
["region"] = "us",
["role"] = "dispatcher",
["roles"] = new[] { "driver", "admin" },
})
.Set("SemanticVersion", "2.4.1")
.Set("TenantId", "acme-corp")
.Build();The validator enforces type consistency at manifest validation time:
- The
whenexpression must evaluate toboolean. Expressions that statically infer to a non-boolean type (e.g. a bare string literal'not-a-boolean') are rejected with error codeFFMANIFEST_RULE_WHEN_TYPE. - The rule
valuemust match the flag's declaredtype. A flag of typebooleancannot have a rule value of"yes". - Logical operators (
&&,||,!) require boolean operands. - Arithmetic operators (
-,*,/,%) require numeric operands. - The
+operator is valid for number + number (addition) or any combination involving a string (concatenation). - Comparison operators (
<,<=,>,>=) require both operands to be the same type (both numeric or both string). - Macro predicates (
exists,all,exists_one) must evaluate to boolean. - Map literal keys must be string expressions.
When a sub-expression's type cannot be determined statically (because it reads from a context variable), the validator defers the check to runtime.
The following are not supported in v1 and are rejected at validation time:
-
User-defined functions - Only
semver_compare,semver_satisfies, andbucketare recognized. Any other function call is a syntax error. - I/O or side effects - CEL expressions are pure functions over their inputs. There is no file access, network access, or mutable state.
-
String enumeration - Strings cannot be used on the right-hand side of
inor as the target of a macro. Only lists are enumerable. - Division or modulo by zero - These throw an evaluation error at runtime.
- Index out of bounds - Accessing a list element beyond its length throws an evaluation error.
{ "when": "user.region == 'us'", "value": false }{
"when": "user.region == 'us' && tier == 'enterprise'",
"value": true
}{ "when": "user.role in ['driver', 'dispatcher']", "value": true }{ "when": "user.roles.exists(r, r == 'admin')", "value": true }{ "when": "bucket(UserId, 50)", "value": "on" }{ "when": "semver_satisfies(SemanticVersion, '>=2.3.0')", "value": true }{ "when": "semver_compare(SemanticVersion, '2.3.0') >= 0", "value": true }{ "when": "Environment == 'Production'", "value": false }{ "when": "TenantId == 'acme-corp'", "value": "custom-theme" }{ "when": "(score >= 50 ? 'high' : 'low') == 'high'", "value": "high" }{
"when": "user.region == 'us' && score + 5 >= 15 && user.role in ['driver', 'dispatcher'] && user.roles.exists(role, role == 'admin') && semver_satisfies(SemanticVersion, '>=2.3.0')",
"value": true
}{ "when": "scores.filter(s, s > 50).exists(s, s > 80)", "value": true }{ "when": "{\"a\": 1, \"b\": 2}[\"a\"] == 1", "value": true }