CEL
Content may include inaccuracies, outdated information, or technical errors. Users are advised to cross-check critical information before implementation.
Common Expression Language is a compact expression language with C-like operators and a small set of collection macros. It reads a model and never changes it.

Writing an expression
A CEL body is a single expression. Feature navigation is a dot, and the operators are the familiar ones:
self.name != ""
self.outgoingRelationships.size() > 0 && self.name.startsWith("SVC-")
Note the differences from the other languages: equality is == and !=, and
the boolean operators are &&, ||, !. There is no ->; collection
operations are written as macros with a dot, like any other call.
Dynamic typing
CEL is compiled before it runs, so a syntax error, or a body that cannot produce a boolean where a boolean is required, is reported in the editor.
Feature navigation is not checked. The contextual object is dynamically typed,
so self.naem compiles and fails only when the expression is evaluated, with a
message naming the missing key. CEL offers no content assist.
Variables in scope
self is the object the expression is evaluated against, dynamically typed.
Which object that is depends on where the expression runs - see
The expression console.
Values are converted into shapes CEL understands. A nested object is navigated
the same way as self, and a multi-valued feature becomes a list. An
enumeration value becomes the literal's name as a string, so a folder's type
compares as self.type == "business". Whole numbers are integers and fractional
numbers are doubles.
Examples
Against ArchiMate, where self is an element:
The element carries a name:
self.name != null && self.name != ""
The element is connected to something:
self.incomingRelationships.size() > 0 || self.outgoingRelationships.size() > 0
Every element carries an owner property:
self.properties.exists(p, p.key == "owner")
The owner property is set exactly once and is not blank:
self.properties.exists_one(p, p.key == "owner")
&& self.properties.filter(p, p.key == "owner").all(p, p.value != "")
Every outgoing relationship ends somewhere named:
self.outgoingRelationships.all(r, r.target.name != "")
The names of everything this element depends on:
self.outgoingRelationships.map(r, r.target.name)
Names follow a convention:
self.name.matches("^[A-Z][A-Za-z ]*$")
Documented elements must be named, and undocumented ones are exempt:
self.documentation == "" ? true : self.name != ""
Against ArchiMate, where self is the model:
Every top-level folder is named:
self.folders.all(f, f.name != "")
The Business folder holds something:
self.folders.exists(f, f.type == "business" && f.elements.size() > 0)
Further reading
Common Expression Language specification
Reference
Variables
| Variable | Value |
|---|---|
self | the contextual object |
data | in a document expression, the list of objects attached to the document |
| the name in Variable | on a repeating document block, the object of the current repetition |
No other name is bound. A name that is not bound is reported when the expression is compiled.
Operator precedence
Highest binding first. Operators on one line bind left to right, except the unary operators and the conditional, which bind right to left.
| Level | Operators | Form |
|---|---|---|
| 1 | a.f a.f(...) a[i] (a) | field selection, method call, index, grouping |
| 2 | !a -a | negation |
| 3 | a * b a / b a % b | multiplicative |
| 4 | a + b a - b | additive |
| 5 | a < b a <= b a > b a >= b a == b a != b a in b | relational |
| 6 | a && b | conjunction |
| 7 | a || b | disjunction |
| 8 | c ? a : b | conditional |
&&, || and ? : evaluate only the operands they need. The conditional is
the only control flow: a body is one expression, and there are no statements, no
loops and no assignment.
Arithmetic operators
| Syntax | Operand types | Returns | Meaning |
|---|---|---|---|
a + b | int, uint, double | same as operands | sum |
a + b | string | string | concatenation |
a + b | bytes | bytes | concatenation |
a + b | list | list | the elements of a followed by those of b |
a + b | timestamp and duration, in either order | timestamp | the instant shifted by the duration |
a + b | duration | duration | sum |
a - b | int, uint, double | same as operands | difference |
a - b | timestamp, timestamp | duration | the time between the two instants |
a - b | timestamp, duration | timestamp | the instant shifted back |
a - b | duration | duration | difference |
a * b | int, uint, double | same as operands | product |
a / b | int, uint, double | same as operands | quotient; integer division truncates toward zero |
a % b | int, uint | same as operands | remainder |
-a | int, double | same as operand | negation |
Both operands must be the same type: 1 + 1.0 does not compile, and neither
does 1 + 1u. An int or uint result outside the range of the type is an
evaluation error, as is / or % by an integer zero. Dividing a double by zero
yields infinity, or NaN when the numerator is also zero.
Comparison and logical operators
| Syntax | Returns | Meaning |
|---|---|---|
a == b | bool | the two values are equal |
a != b | bool | the two values are not equal |
a < b, a <= b, a > b, a >= b | bool | ordering |
!a | bool | negation |
a && b | bool | both hold |
a || b | bool | either holds |
c ? a : b | type of a and b | a when c is true, otherwise b |
== and != accept any two values of the same type, including lists and maps,
which compare element by element. Ordering accepts bool, int, uint, double,
string, bytes, timestamp and duration. Comparing two different types does not
compile, 1 == 1.0 included; wrap an operand in dyn() to defer the choice of
operation to evaluation, where dyn(1) == dyn(1.0) is true.
Navigation and indexing
| Syntax | Returns | Meaning |
|---|---|---|
a.f | the value of f | field selection on an object or a map |
a["f"] | the value of f | the same selection, written as an index |
a[i] | the element | the element of a list at position i, counting from zero |
m[k] | the value | the value a map holds under key k |
has(a.f) | bool | a has a field named f |
x in list | bool | the list contains x |
k in map | bool | the map has key k |
An index outside a list, or a key a map does not hold, is an evaluation error.
has() takes a field selection and nothing else, so has(a.f[0]) does not
compile.
Literals
| Syntax | Type | Notes |
|---|---|---|
42, -7, 0x1F | int | decimal or hexadecimal, 64-bit signed |
42u, 0xFFu | uint | 64-bit unsigned |
1.5, .5, 1e3, 1.5e-3 | double | |
true, false | bool | |
null | null | |
"text", 'text' | string | |
"""text""", '''text''' | string | may span lines |
r"a\d", R'a\d' | string | raw: a backslash is a backslash |
b"ab", b'\x41' | bytes | |
[1, 2] | list | elements need not share a type |
{"a": 1, "b": 2} | map | a duplicate key is an evaluation error |
String escapes: \n, \r, \t, \a, \b, \f, \v, \', \", \\,
\`, \?, the octal \101, the hexadecimal \x41, and the code points
\u00e9 and \U0001F600.
A digit group separator is not part of a numeric literal: write 1000, not
1_000. A // comment runs to the end of the line, and an expression may be
written over several lines.
These identifiers are reserved and cannot be used as a name: as, break,
const, continue, else, false, for, function, if, import, in,
let, loop, namespace, null, package, return, true, var, void,
while. A feature named in, true, false or null is reached by index
rather than by dot, as in self["in"].
Type names and type tests
| Syntax | Returns | Meaning |
|---|---|---|
type(a) | type | the type of a value |
dyn(a) | dyn | the same value, with its type left to evaluation |
type(a) compares against the type names int, uint, double, bool,
string, bytes, list, map, null_type, dyn and type. A model object
answers map. Timestamp and duration have no name that can be written in an
expression.
type(self.name) == string
Conversions
| Syntax | Returns | Accepts |
|---|---|---|
int(a) | int | int, uint, double (truncated toward zero), string, timestamp (seconds since the epoch) |
uint(a) | uint | uint, int, double, string |
double(a) | double | double, int, uint, string |
string(a) | string | string, int, uint, double, bytes, timestamp, duration |
bool(a) | bool | bool, string |
bytes(a) | bytes | bytes, string |
timestamp(a) | timestamp | a string in RFC 3339 form, or a timestamp |
duration(a) | duration | a string such as "90s", "1h30m" or "1.5s", or a duration |
A string that does not parse is an evaluation error.
int("12") + 1
timestamp("2020-03-04T05:06:07Z")
String operations
| Syntax | Returns | Meaning |
|---|---|---|
s.size(), size(s) | int | the number of code points |
s.contains(t) | bool | t occurs somewhere in s |
s.startsWith(t) | bool | s begins with t |
s.endsWith(t) | bool | s ends with t |
s.matches(p), matches(s, p) | bool | the pattern p matches somewhere in s |
matches searches; anchor the pattern with ^ and $ to require the whole
string. Patterns use the RE2 syntax, and a malformed pattern is an evaluation
error.
self.name.matches("^[A-Z][A-Za-z ]*$")
Timestamp and duration operations
Each accessor also takes an IANA time-zone name as its one argument, as in
getHours("America/New_York"); without it the reading is UTC. A duration
accepts only the four accessors marked below, and ignores the argument.
| Syntax | Applies to | Returns | Meaning |
|---|---|---|---|
t.getFullYear() | timestamp | int | the year |
t.getMonth() | timestamp | int | the month, 0 for January |
t.getDayOfYear() | timestamp | int | the day of the year, counting from zero |
t.getDayOfMonth() | timestamp | int | the day of the month, counting from zero |
t.getDate() | timestamp | int | the day of the month, counting from one |
t.getDayOfWeek() | timestamp | int | the day of the week, 0 for Sunday |
t.getHours() | timestamp | int | the hour of the day |
t.getMinutes() | timestamp | int | the minute of the hour |
t.getSeconds() | timestamp | int | the second of the minute |
t.getMilliseconds() | timestamp | int | the milliseconds within the second |
d.getHours() | duration | int | the whole hours the duration spans |
d.getMinutes() | duration | int | the whole minutes the duration spans |
d.getSeconds() | duration | int | the whole seconds the duration spans |
d.getMilliseconds() | duration | int | the milliseconds left over past the last whole second |
duration("1h30m").getMinutes() is 90, and
duration("1.5s").getMilliseconds() is 500.
Collection operations
size applies to a string, bytes, a list and a map, and may be written as a
call or as a method.
| Syntax | Returns | Meaning |
|---|---|---|
c.size(), size(c) | int | the number of elements, or of map entries |
c[i] | the element | the element of a list at position i |
The macros below take a variable name, which stands for the element under consideration, and an expression written in terms of it. Over a map they range over its keys. The variable exists only inside the macro; nothing else in the language introduces a name.
| Syntax | Returns | Meaning |
|---|---|---|
c.all(e, p) | bool | p holds for every element |
c.exists(e, p) | bool | p holds for at least one element |
c.exists_one(e, p) | bool | p holds for exactly one element |
c.filter(e, p) | list | the elements for which p holds |
c.map(e, v) | list | the value of v for each element |
c.map(e, p, v) | list | the value of v for the elements for which p holds |
self.outgoingRelationships.map(r, r.target.name)
self.eStructuralFeatures.map(f, f.derived, f.name)
Macros nest, and their results are ordinary lists:
self.folders.filter(f, f.type == "business").all(f, f.elements.size() > 0)
Model objects
A model object reaches CEL as a map keyed by the names of the structural features of its metaclass, inherited and derived features included. Every form that works on a map therefore works on an object.
| Syntax | Returns | Meaning |
|---|---|---|
self.name | the feature value | the feature named name |
self["name"] | the feature value | the same feature |
has(self.name) | bool | the metaclass declares a feature named name |
"name" in self | bool | the same test |
self.size(), size(self) | int | the number of features |
self.all(f, p) | bool | p holds for every feature name |
has() and in answer for a feature the metaclass declares, whether or not it
holds a value.
Feature values arrive in these shapes:
| Feature | CEL value |
|---|---|
| a reference or containment holding one object | that object, as a map |
| a multi-valued feature | a list, empty when nothing is held |
| an enumeration | a string, the literal's name |
| a string attribute | a string |
| a character attribute | a one-character string |
| a boolean attribute | a bool |
| a whole-number attribute | an int |
| a fractional-number attribute | a double |
A single-valued feature that is not set has no value. Reading it yields no
result, and so does any comparison built on it, including one against null.
Navigation runs through features and nothing else: containers, inverse
references and related objects are reached by naming the feature the metamodel
declares for them, such as incomingRelationships on an ArchiMate element. A
name that is not a feature of the object in hand compiles and fails at
evaluation, with a message naming the missing key.
Syntax the product does not enable
Optional syntax - a.?f, {?"a": v}, optional values and the orValue and
optMap operations - is not enabled. Neither are the CEL extension libraries,
so their functions are not available either, among them join, split,
lowerAscii, upperAscii, replace, indexOf and the math and sets
operations. Protocol-buffer message construction and the int to timestamp
conversion are not available either. Nothing is added to the standard language:
every function and macro a CEL body can call is listed above.