Skip to main content

CEL

This document was generated using AI assistance

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.

A CEL expression in the console, answered with the names its macro collects

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

VariableValue
selfthe contextual object
datain a document expression, the list of objects attached to the document
the name in Variableon 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.

LevelOperatorsForm
1a.f a.f(...) a[i] (a)field selection, method call, index, grouping
2!a -anegation
3a * b a / b a % bmultiplicative
4a + b a - badditive
5a < b a <= b a > b a >= b a == b a != b a in brelational
6a && bconjunction
7a || bdisjunction
8c ? a : bconditional

&&, || 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

SyntaxOperand typesReturnsMeaning
a + bint, uint, doublesame as operandssum
a + bstringstringconcatenation
a + bbytesbytesconcatenation
a + blistlistthe elements of a followed by those of b
a + btimestamp and duration, in either ordertimestampthe instant shifted by the duration
a + bdurationdurationsum
a - bint, uint, doublesame as operandsdifference
a - btimestamp, timestampdurationthe time between the two instants
a - btimestamp, durationtimestampthe instant shifted back
a - bdurationdurationdifference
a * bint, uint, doublesame as operandsproduct
a / bint, uint, doublesame as operandsquotient; integer division truncates toward zero
a % bint, uintsame as operandsremainder
-aint, doublesame as operandnegation

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

SyntaxReturnsMeaning
a == bboolthe two values are equal
a != bboolthe two values are not equal
a < b, a <= b, a > b, a >= bboolordering
!aboolnegation
a && bboolboth hold
a || bbooleither holds
c ? a : btype of a and ba 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.

SyntaxReturnsMeaning
a.fthe value of ffield selection on an object or a map
a["f"]the value of fthe same selection, written as an index
a[i]the elementthe element of a list at position i, counting from zero
m[k]the valuethe value a map holds under key k
has(a.f)boola has a field named f
x in listboolthe list contains x
k in mapboolthe 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

SyntaxTypeNotes
42, -7, 0x1Fintdecimal or hexadecimal, 64-bit signed
42u, 0xFFuuint64-bit unsigned
1.5, .5, 1e3, 1.5e-3double
true, falsebool
nullnull
"text", 'text'string
"""text""", '''text'''stringmay span lines
r"a\d", R'a\d'stringraw: a backslash is a backslash
b"ab", b'\x41'bytes
[1, 2]listelements need not share a type
{"a": 1, "b": 2}mapa 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

SyntaxReturnsMeaning
type(a)typethe type of a value
dyn(a)dynthe 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

SyntaxReturnsAccepts
int(a)intint, uint, double (truncated toward zero), string, timestamp (seconds since the epoch)
uint(a)uintuint, int, double, string
double(a)doubledouble, int, uint, string
string(a)stringstring, int, uint, double, bytes, timestamp, duration
bool(a)boolbool, string
bytes(a)bytesbytes, string
timestamp(a)timestampa string in RFC 3339 form, or a timestamp
duration(a)durationa 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

SyntaxReturnsMeaning
s.size(), size(s)intthe number of code points
s.contains(t)boolt occurs somewhere in s
s.startsWith(t)bools begins with t
s.endsWith(t)bools ends with t
s.matches(p), matches(s, p)boolthe 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.

SyntaxApplies toReturnsMeaning
t.getFullYear()timestampintthe year
t.getMonth()timestampintthe month, 0 for January
t.getDayOfYear()timestampintthe day of the year, counting from zero
t.getDayOfMonth()timestampintthe day of the month, counting from zero
t.getDate()timestampintthe day of the month, counting from one
t.getDayOfWeek()timestampintthe day of the week, 0 for Sunday
t.getHours()timestampintthe hour of the day
t.getMinutes()timestampintthe minute of the hour
t.getSeconds()timestampintthe second of the minute
t.getMilliseconds()timestampintthe milliseconds within the second
d.getHours()durationintthe whole hours the duration spans
d.getMinutes()durationintthe whole minutes the duration spans
d.getSeconds()durationintthe whole seconds the duration spans
d.getMilliseconds()durationintthe 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.

SyntaxReturnsMeaning
c.size(), size(c)intthe number of elements, or of map entries
c[i]the elementthe 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.

SyntaxReturnsMeaning
c.all(e, p)boolp holds for every element
c.exists(e, p)boolp holds for at least one element
c.exists_one(e, p)boolp holds for exactly one element
c.filter(e, p)listthe elements for which p holds
c.map(e, v)listthe value of v for each element
c.map(e, p, v)listthe 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.

SyntaxReturnsMeaning
self.namethe feature valuethe feature named name
self["name"]the feature valuethe same feature
has(self.name)boolthe metaclass declares a feature named name
"name" in selfboolthe same test
self.size(), size(self)intthe number of features
self.all(f, p)boolp 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:

FeatureCEL value
a reference or containment holding one objectthat object, as a map
a multi-valued featurea list, empty when nothing is held
an enumerationa string, the literal's name
a string attributea string
a character attributea one-character string
a boolean attributea bool
a whole-number attributean int
a fractional-number attributea 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.