BNDRY uses CEL (Common Expression Language) to power our entity risk ratings and policy engine.
CEL is a simple expression language that can be used to evaluate expressions in a safe and efficient way.
| Comment | // (line comments only) |
|---|---|
| Boolean | true, false |
| Integer | 42, -17, 0x2A (hex) |
| Unsigned | 42u, 0x2Au |
| Float | 3.14, -0.5, 6.022e23 |
| String | "foo", 'bar', """multiline""" |
| Bytes | b"data", b'data' |
| List | [1, 2, 3] |
| Map | {"a": 1, "b": 2} |
| Null | null |
Strings can be enclosed in single quotes or double quotes. For multiline strings, use triple quotes (""" or ''').
"Hello\nWorld"
'Single quoted'
"""Multiline
string"""Raw strings (prefixed with r) don't interpret escape sequences:
r"\n\t" // literal backslash-n-backslash-tBytes literals are prefixed with b:
b"hello"
b'\xF0\x9F\xA4\xAA' // UTF-8 encoding of 🤪| Arithmetic | +, -, *, /, % (modulus) |
|---|---|
| Comparison | ==, !=, <, >, <=, >= |
| Logical | ! (not), && (and), || (or) |
| Conditional | ? : (ternary) |
| Membership | in |
| Indexing | [], . |
The in operator checks if an item exists in a list or a key exists in a map.
2 in [1, 2, 3] // true
"name" in {"name": "John"} // trueAccess fields of messages or map values with . or []:
user.name
user["name"]Access list elements with []:
items[0] // first element
items[-1] // last elementThe ternary operator evaluates a condition and returns one of two values:
score >= 60 ? "pass" : "fail"The && and || operators provide commutative evaluation (may evaluate in any order):
user.admin && user.active
error || true // returns true, ignoring errorFor traditional short-circuit evaluation, use the ternary operator:
// Short-circuit AND
condition ? second_condition : false
// Short-circuit OR
condition ? true : second_conditionChecks if a string contains a substring.
"hello world".contains("world") // trueChecks if a string starts with a prefix.
"hello world".startsWith("hello") // trueChecks if a string ends with a suffix.
"hello world".endsWith("world") // trueTests if a string matches a regular expression pattern (RE2 syntax).
"foobar".matches("foo.*") // true
matches("test123", "[a-z]+[0-9]+") // trueReturns the length of a string in Unicode code points.
"hello".size() // 5
size("world!") // 6Returns the number of elements in a list.
[1, 2, 3].size() // 3
size([]) // 0Checks if a value exists in a list.
2 in [1, 2, 3] // true
5 in [1, 2, 3] // falseReturns the number of entries in a map.
{"a": 1, "b": 2}.size() // 2Checks if a key exists in a map.
"name" in {"name": "John", "age": 30} // trueTests whether a field is set in a message or whether a key exists in a map.
has(user.address)
has(map.key_name)Tests if all elements satisfy a condition.
[1, 2, 3].all(x, x > 0) // true
{"a": 1, "b": 2}.all(k, k != 'c') // trueTests if any element satisfies a condition.
[1, 2, 3].exists(x, x > 2) // true
[].exists(x, x > 0) // falseTests if exactly one element satisfies a condition.
[1, 2, 3].exists_one(x, x == 2) // true
[2, 2, 3].exists_one(x, x == 2) // falseTransforms each element using an expression.
[1, 2, 3].map(x, x * 2) // [2, 4, 6]
["a", "b"].map(s, s.upperAscii()) // ["A", "B"]With filter predicate:
[1, 2, 3, 4].map(x, x % 2 == 0, x * 2) // [4, 8]Returns elements that satisfy a condition.
[1, 2, 3, 4].filter(x, x % 2 == 0) // [2, 4]
{"a": 1, "b": 2}.filter(k, k == "a") // ["a"]Creates a timestamp from an RFC3339 string.
timestamp("2023-08-26T12:39:00-07:00")Creates a duration from a string (supports h, m, s, ms, us, ns).
duration("1h30m")
duration("500ms")
duration("-1.5h")Get components of a timestamp:
timestamp("2023-12-25T12:30:45Z").getFullYear() // 2023
timestamp("2023-12-25T12:30:45Z").getMonth() // 11 (December, 0-indexed)
timestamp("2023-12-25T12:30:45Z").getDate() // 25
timestamp("2023-12-25T12:30:45Z").getHours() // 12
timestamp("2023-12-25T12:30:45Z").getMinutes() // 30
timestamp("2023-12-25T12:30:45Z").getSeconds() // 45
timestamp("2023-12-25T12:30:45Z").getDayOfWeek() // 1 (Monday)
timestamp("2023-12-25T12:30:45Z").getDayOfYear() // 358With timezone:
timestamp("2023-12-25T00:00:00Z").getDate("America/Los_Angeles") // 24Convert or extract from durations:
duration("1h30m").getHours() // 1
duration("1h30m").getMinutes() // 90
duration("1.234s").getMilliseconds() // 234Add or subtract time values:
timestamp("2023-01-01T00:00:00Z") + duration("24h")
timestamp("2023-01-10T12:00:00Z") - timestamp("2023-01-10T00:00:00Z")
duration("1h") + duration("30m")Converts to a signed integer.
int("123") // 123
int(3.14) // 3
int(timestamp("2023-08-26T12:00:00Z")) // Unix epoch secondsConverts to an unsigned integer.
uint("123") // 123u
uint(3.14) // 3uConverts to a double.
double(10) // 10.0
double("3.14") // 3.14Converts to a string.
string(123) // "123"
string(true) // "true"
string(b'hello') // "hello"
string(duration("1m1ms")) // "60.001s"Converts to bytes.
bytes("hello") // b'hello'
bytes("🤪") // b'\xF0\x9F\xA4\xAA'Converts to boolean.
bool("true") // true
bool("FALSE") // falseReturns the type of a value.
type(123) // int
type("hello") // string
type([1, 2]) // listMarks a value as dynamically typed (for type-checking purposes).
dyn([1, 3.14, "foo"]) // list(dyn)Returns the number of bytes.
b'hello'.size() // 5
size(b'\xF0\x9F\xA4\xAA') // 4Check field existence before accessing:
has(user.profile) && user.profile.name == "John"// Filter then transform
orders.filter(o, o.total > 100).map(o, o.id)
// Count matching items
users.filter(u, u.age >= 18).size()
// Check all items
items.all(item, item.quantity > 0)// Check if key exists
has(config.timeout)
// Get value with default
has(settings.retries) ? settings.retries : 3
// Filter map to list of keys
{"a": 10, "b": 5, "c": 20}.filter(k, map[k] > 10) // ["c"]// Check if timestamp is in range
now > timestamp("2023-01-01T00:00:00Z") &&
now < timestamp("2024-01-01T00:00:00Z")
// Calculate age
duration(now - user.birthdate).getHours() / 24 / 365// Email-like pattern
user.email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
// Required fields
user.name.size() > 0 && user.email.size() > 0CEL expressions can produce errors at runtime. Common errors include:
no_matching_overload: Function called with wrong argument typesno_such_field: Accessing a non-existent field- Division by zero
- Type conversion failures
- Overflow errors
Use logical operators to handle potential errors:
// This won't error even if division by zero occurs on left side
x / 0 || true // returns true
// Safe field access
has(obj.field) && obj.field > 10Use
has()before accessing optional fieldshas(user.address) && user.address.city == "NYC"Prefer explicit type conversions
int(userInput) + 5 // instead of relying on implicit conversionUse macros for list/map operations
items.all(x, x.valid) // instead of manual iterationChain operations clearly
users .filter(u, u.active) .map(u, u.email) .exists(e, e.endsWith("@company.com"))Handle null values explicitly
user.name != null && user.name.size() > 0