CEL Functions Reference
Complete reference for all functions available in Mycel transforms and conditions.
Context Variables
| Variable |
Description |
input |
Incoming data (request body, message payload, query result) |
output |
Output fields computed above this line, or the previous stage's result — see Input and Output |
enriched |
Data fetched from enrich blocks |
step |
Results from named step blocks |
result |
Flow result (in after aspects) |
error |
Error object: error.message, error.code, error.type |
drop |
Deflected message: drop.reason, drop.policy, drop.message_id (in on_drop aspects) |
_flow |
Flow name (in aspects) |
_operation |
Operation name (in aspects) |
_target |
Target name (in aspects) |
_timestamp |
Unix timestamp (in aspects) |
ctx is declared in the expression environment but never populated — it always
evaluates as an empty map. Read request headers from input.headers.
For where each variable comes from and what output means in each block, see
Input and Output.
Mycel Built-in Functions
Identity and Time
| Function |
Signature |
Returns |
Description |
uuid() |
() → string |
UUID v4 string |
Generate a random UUID |
now() |
() → string |
RFC3339 string |
Current UTC time |
now_unix() |
() → int |
Unix timestamp |
Current time as Unix seconds |
uuid() // "550e8400-e29b-41d4-a716-446655440000"
now() // "2025-12-29T15:04:05Z"
now_unix() // 1735488245
String Functions
| Function |
Signature |
Description |
lower(s) |
(string) → string |
Convert to lowercase |
upper(s) |
(string) → string |
Convert to uppercase |
trim(s) |
(string) → string |
Remove leading/trailing whitespace |
replace(s, old, new) |
(string, string, string) → string |
Replace all occurrences of old with new |
split(s, sep) |
(string, string) → list(string) |
Split string by separator |
join(list, sep) |
(list(string), string) → string |
Join list items with separator |
substring(s, start, end) |
(string, int, int) → string |
Extract substring (byte indices) |
len(s) |
(string) → int |
String length in bytes |
hash_sha256(s) |
(string) → string |
SHA-256 hash, hex encoded (64 characters). For fingerprints — passwords belong to auth, which uses Argon2id |
format_date(date, fmt) |
(string, string) → string |
Reformat ISO date string |
lower("HELLO") // "hello"
upper("hello") // "HELLO"
trim(" hello ") // "hello"
replace("hello", "l", "L") // "heLLo"
split("a,b,c", ",") // ["a", "b", "c"]
join(["a", "b", "c"], "-") // "a-b-c"
substring("hello", 1, 4) // "ell"
len("hello") // 5
hash_sha256("abc") // "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
// format_date tokens: YYYY MM DD HH mm ss
format_date("2025-01-15T10:30:00Z", "YYYY-MM-DD") // "2025-01-15"
format_date(now(), "DD/MM/YYYY HH:mm") // "15/01/2025 10:30"
Default and Null Handling
| Function |
Signature |
Description |
default(value, fallback) |
(any, any) → any |
Return fallback if value is null or empty string |
coalesce(value, fallback) |
(any, any) → any |
Alias for default |
default(input.nickname, input.name) // Use name if nickname is null/empty
coalesce(input.phone, "N/A") // "N/A" if phone is null/empty
Map Functions
| Function |
Signature |
Description |
merge(m1, m2) |
(map, map) → map |
Merge two maps; m2 values override m1 |
merge(m1, m2, m3) |
(map, map, map) → map |
Merge three maps |
merge(m1, m2, m3, m4) |
(map, map, map, map) → map |
Merge four maps |
omit(m, k1) |
(map, string) → map |
Return map without key k1 |
omit(m, k1, k2) |
(map, string, string) → map |
Return map without keys k1 and k2 |
omit(m, k1, k2, k3) |
(map, string, string, string) → map |
Return map without up to 3 keys |
pick(m, k1) |
(map, string) → map |
Return map with only key k1 |
pick(m, k1, k2) |
(map, string, string) → map |
Return map with only keys k1 and k2 |
pick(m, k1, k2, k3) |
(map, string, string, string) → map |
Return map with only up to 3 keys |
merge(step.order, {"customer": step.customer})
omit(input, "password")
omit(input, "password", "secret_token")
pick(input, "id", "email")
Array Functions
| Function |
Signature |
Description |
as_list(any) |
(any) → list |
A list stays a list; anything else becomes a list of one; nothing becomes the empty list |
first(list) |
(list) → any |
First element, or null if empty |
last(list) |
(list) → any |
Last element, or null if empty |
flatten(list) |
(list(list)) → list |
Flatten one level of nesting |
unique(list) |
(list) → list |
Remove duplicate values |
reverse(list) |
(list) → list |
Reverse list order |
pluck(list, key) |
(list(map), string) → list |
Extract a field from each map in a list |
sort_by(list, key) |
(list(map), string) → list |
Sort list of maps by field (ascending) |
sum(list) |
(list(number)) → number |
Sum numeric values |
avg(list) |
(list) → double |
Average of numeric values |
min_val(list) |
(list) → any |
Minimum value |
max_val(list) |
(list) → any |
Maximum value |
as_list(step.items) // safe to iterate whatever it holds
first(input.items) // first item or null
last(input.items) // last item or null
flatten([[1, 2], [3, 4]]) // [1, 2, 3, 4]
unique([1, 2, 2, 3]) // [1, 2, 3]
reverse([1, 2, 3]) // [3, 2, 1]
pluck(input.orders, "total") // [100, 200, 150]
sort_by(input.products, "price") // sorted by price asc
sum(pluck(input.items, "price")) // sum of prices
avg(pluck(input.scores, "value")) // average score
min_val(pluck(input.bids, "amount")) // minimum bid
max_val(pluck(input.bids, "amount")) // maximum bid
GraphQL Field Selection
| Function |
Signature |
Description |
has_field(input, path) |
(map, string) → bool |
True if field path was requested in the GraphQL query |
field_requested(input, path) |
(map, string) → bool |
Alias for has_field |
requested_fields(input) |
(map) → list(string) |
All requested field paths |
requested_top_fields(input) |
(map) → list(string) |
Top-level requested fields only |
has_field(input, "orders") // true if "orders" was queried
has_field(input, "orders.items") // true if nested field was requested
requested_fields(input) // ["id", "name", "orders", "orders.total"]
requested_top_fields(input) // ["id", "name", "orders"]
CEL Standard Extensions
ext.Strings — Extended String Operations
| Function |
Signature |
Description |
s.charAt(i) |
(string, int) → string |
Character at index |
s.indexOf(sub) |
(string, string) → int |
First index of substring (-1 if not found) |
s.lastIndexOf(sub) |
(string, string) → int |
Last index of substring |
s.upperAscii() |
(string) → string |
ASCII uppercase |
s.lowerAscii() |
(string) → string |
ASCII lowercase |
s.replace(old, new) |
(string, string, string) → string |
Replace first occurrence |
s.replace(old, new, n) |
(string, string, string, int) → string |
Replace up to n occurrences |
s.split(sep) |
(string, string) → list |
Split string |
s.substring(start, end) |
(string, int, int) → string |
Extract substring |
s.trim() |
(string) → string |
Trim whitespace |
s.reverse() |
(string) → string |
Reverse string |
list.join(sep) |
(list, string) → string |
Join list with separator |
"hello".charAt(1) // "e"
"hello world".indexOf("o") // 4
"hello world".lastIndexOf("o") // 7
"hello".upperAscii() // "HELLO"
"hello".replace("l", "L") // "heLLo"
"a,b,c".split(",") // ["a", "b", "c"]
"hello".substring(1, 4) // "ell"
" hello ".trim() // "hello"
"hello".reverse() // "olleh"
["a", "b", "c"].join("-") // "a-b-c"
ext.Encoders — Base64
| Function |
Signature |
Description |
base64.encode(bytes) |
(bytes) → string |
Base64 encode |
base64.decode(s) |
(string) → bytes |
Base64 decode |
base64.encode(b"hello") // "aGVsbG8="
base64.decode("aGVsbG8=") // b"hello"
ext.Math — Mathematical Functions
| Function |
Signature |
Description |
math.abs(n) |
(number) → number |
Absolute value |
math.ceil(n) |
(double) → double |
Ceiling |
math.floor(n) |
(double) → double |
Floor |
math.round(n) |
(double) → double |
Round to nearest |
math.sign(n) |
(number) → number |
Sign (-1, 0, 1) |
math.greatest(...) |
(number, ...) → number |
Maximum of arguments |
math.least(...) |
(number, ...) → number |
Minimum of arguments |
math.isNaN(n) |
(double) → bool |
Is NaN? |
math.isInf(n) |
(double) → bool |
Is infinite? |
math.abs(-5) // 5
math.ceil(4.2) // 5.0
math.floor(4.8) // 4.0
math.round(4.5) // 5.0
math.sign(-10) // -1
math.greatest(1, 5, 3) // 5
math.least(1, 5, 3) // 1
ext.Lists — Extended List Operations
| Function |
Signature |
Description |
list.slice(from, to) |
(list, int, int) → list |
Slice from index to index |
list.flatten() |
(list(list)) → list |
Flatten one level |
lists.range(n) |
(int) → list(int) |
Generate [0, 1, ..., n-1] |
[1, 2, 3, 4, 5].slice(1, 4) // [2, 3, 4]
[[1, 2], [3, 4]].flatten() // [1, 2, 3, 4]
lists.range(5) // [0, 1, 2, 3, 4]
ext.Sets — Set Operations
| Function |
Signature |
Description |
sets.contains(a, b) |
(list, list) → bool |
True if a contains all elements of b |
sets.equivalent(a, b) |
(list, list) → bool |
True if a and b have same elements |
sets.intersects(a, b) |
(list, list) → bool |
True if a and b share any element |
sets.contains([1, 2, 3], [2, 3]) // true
sets.equivalent([1, 2], [2, 1]) // true
sets.intersects([1, 2], [2, 3]) // true
CEL Built-ins
Type Conversions
int("42") // 42
double(42) // 42.0
string(42) // "42"
bool("true") // true (from a string; there is no int conversion)
bytes("hello") // b"hello"
String Methods (CEL built-ins)
"hello".startsWith("he") // true
"hello".endsWith("lo") // true
"hello".contains("ell") // true
"ABC123".matches("[A-Z]{3}[0-9]+") // true (regex)
"hello".size() // 5
List Built-ins
[1, 2, 3].size() // 3
[1, 2, 3][0] // 1
"admin" in input.roles // membership check
// Comprehension macros
input.items.exists(x, x.price > 100) // any item over $100?
input.items.all(x, x.available) // all items available?
input.items.filter(x, x.price > 50) // items over $50
input.items.map(x, x.price * 1.1) // apply 10% markup
Operators
// Arithmetic
input.price * input.quantity
input.total + input.tax
input.balance - input.withdrawal
input.count / 2
input.value % 10
// Comparison
input.age >= 18
input.status == "active"
input.score != 0
input.amount > 100
input.priority < 5
// Logical
input.active && input.verified
input.admin || input.moderator
!input.deleted
// Conditional (ternary)
input.age >= 18 ? "adult" : "minor"
// Null coalescing
input.nickname ?? input.name
input.body.payload.jobId ?? ''
input.list ?? []
?? is preprocessed by Mycel before reaching CEL. When the left-hand side is a simple dotted path (e.g. input.body.payload.jobId), the rewrite uses CEL's has() macro to check every parent segment, so a missing intermediate field falls back cleanly instead of raising "no such key". For other left-hand sides (function calls, parenthesized expressions) the rewrite is coalesce(lhs, rhs), which catches present-but-null and present-but-empty-string values but evaluates lhs eagerly.
Chaining is right-associative: a ?? b ?? c behaves like a ?? (b ?? c).
?? binds tighter than the ternary ?:, as in JavaScript and C#: c ? a ?? 1 : b ?? 2 is c ? (a ?? 1) : (b ?? 2). To coalesce a whole ternary, parenthesize it: (c ? a : b) ?? d.
Inside a map literal, ?? applies to the value, not the entry, so a nested object with a default per field is written the obvious way:
hours = "{'open': input.open ?? '', 'close': input.close ?? ''}"
Before 3.6.2
The rewrite assumed the operand started at the beginning of the enclosing argument, so inside a map literal it swallowed the key — coalesce('open': input.open, '') — and the expression failed to compile at request time, since mycel validate does not compile CEL. It read as map literals being unsupported. The same assumption made ?? span both branches of a ternary written at the same depth.
Common Patterns
Email normalization
Slug generation
lower(replace(trim(input.name), " ", "-"))
Conditional field with fallback
default(input.display_name, input.first_name + " " + input.last_name)
Extract domain from email
split(input.email, "@")[1]
Compute order total
sum(pluck(input.items, "price"))
unique(flatten([input.tags, input.extra_tags]))
Conditional status based on multiple fields
input.paid && input.shipped ? "completed" : input.paid ? "processing" : "pending"