Show HN: CEL by Example
Source: Hacker News
Example User message
{
"name": "Alice",
"roles": ["admin", "editor", "viewer"],
"age": 30,
"email": "alice@example.com",
"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?
user.age >= 18
// result: true (bool)
Check the user’s email domain with a string function.
user.email.endsWith("@example.com")
// result: true (bool)
Collections
Does the user have a specific role? in checks membership in a list.
"admin" in user.roles
// result: true (bool)
What if the match isn’t exact? exists() tests whether any element satisfies a condition.
user.roles.exists(r, r.startsWith("ad"))
// result: true (bool)
The user has three roles—what if we only want the elevated ones? filter() narrows a list to matching elements.
user.roles.filter(r, r != "viewer")
// 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.
user.email_verified - user.created = 18 && "admin" in user.roles
// result: true (bool)
The conditional operator allows branching logic.
user.age >= 18 ? "adult" : "minor"
// result: "adult" (string)
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.
user.roles.map(r, r != "viewer", r + ":write")
// result: ["admin:write", "editor:write"] (list)