CEL (Common Expression Language) evaluates expressions against data—simple values, a Protobuf message, or a JSON object.
CEL's fast, portable, and safe: it's used in Kubernetes admission control, Google Cloud IAM conditions, Firebase security rules, Envoy Proxy routing, and Protovalidate's constraint rules.
Let's explore CEL together, starting with a simple User message:
{
"name": "Alice",
"roles": ["admin", "editor", "viewer"],
"age": 30,
"email": "[email protected]",
"created": timestamp("2025-12-14T00:00:00Z"),
"email_verified": timestamp("2025-12-14T18:30:00Z")
}
Strings and numbers
A basic comparison: is the user over 18?
Check the user's email domain with a string function.
Collections
Does the user have a specific role?
in checks membership in a list.
What if the match isn't exact?
exists() tests whether any element satisfies a condition.
The user has three roles—what if we only want the elevated ones?
filter() narrows a list to matching elements.
Timestamps and durations
Did the user verify their email within 24 hours of signing up? CEL handles time natively—subtract two timestamps to get a duration, then compare.
Logical operators
Logical operators combine checks into a single expression.
The conditional operator allows branching logic.
Transforming data
CEL expressions return any type. Build a map that strips PII from the user.
{"roles": user.roles, "is_adult": user.age >= 18}
// result: {"roles": ["admin", "editor", "viewer"], "is_adult": true} (map)
Annotate each role with whether it's elevated.
map() transforms a collection into a new one.
user.roles.map(r, {"role": r, "elevated": r != "viewer"})
// result: [{"role": "admin", "elevated": true}, {"role": "editor", "elevated": true}, {"role": "viewer", "elevated": false}] (list)
map() can also filter—select elevated roles and grant write access in one step.