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:
[](#__codelineno-0-1){ [](#__codelineno-0-2) "name": "Alice", [](#__codelineno-0-3) "roles": ["admin", "editor", "viewer"], [](#__codelineno-0-4) "age": 30, [](#__codelineno-0-5) "email": "alice@example.com", [](#__codelineno-0-6) "created": timestamp("2025-12-14T00:00:00Z"), [](#__codelineno-0-7) "email_verified": timestamp("2025-12-14T18:30:00Z") [](#__codelineno-0-8)}
Strings and numbers#
A basic comparison: is the user over 18?
[](#__codelineno-1-1)user.age >= 18 [](#__codelineno-1-2)// result: true (bool)
Check the user's email domain with a string function.
[](#__codelineno-2-1)user.email.endsWith("@example.com") [](#__codelineno-2-2)// result: true (bool)
Collections#
Does the user have a specific role? in checks membership in a list.
[](#__codelineno-3-1)"admin" in user.roles [](#__codelineno-3-2)// result: true (bool)
What if the match isn't exact? exists() tests whether any element satisfies a condition.
[](#__codelineno-4-1)user.roles.exists(r, r.startsWith("ad")) [](#__codelineno-4-2)// result: true (bool)
The user has three roles—what if we only want the elevated ones? filter() narrows a list to matching elements.
[](#__codelineno-5-1)user.roles.filter(r, r != "viewer") [](#__codelineno-5-2)// result: ["admin", "editor"] (list)
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.
[](#__codelineno-6-1)user.email_verified - user.created < duration("24h") [](#__codelineno-6-2)// result: true (bool)
Logical operators#
Logical operators combine checks into a single expression.
[](#__codelineno-7-1)user.age >= 18 && "admin" in user.roles [](#__codelineno-7-2)// result: true (bool)
The conditional operator allows branching logic.
[](#__codelineno-8-1)user.age >= 18 ? "adult" : "minor" [](#__codelineno-8-2)// result: "adult" (string)
Transforming data#
CEL expressions return any type. Build a map that strips PII from the user.
[](#__codelineno-9-1){"roles": user.roles, "is_adult": user.age >= 18} [](#__codelineno-9-2)// 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.
[](#__codelineno-10-1)user.roles.map(r, {"role": r, "elevated": r != "viewer"}) [](#__codelineno-10-2)// 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.
[](#__codelineno-11-1)user.roles.map(r, r != "viewer", r + ":write") [](#__codelineno-11-2)// result: ["admin:write", "editor:write"] (list)
Source involved in this report: Read Original Article