Transforms¶
Transforms reshape data between source and target using CEL (Common Expression Language) expressions. CEL is sandboxed, compiled at startup, and cached — meaning any expression error is caught before the service starts, and transforms run at near-native speed.
Note:
transformreshapes data before sending to the destination. To reshape the response after receiving from the destination, use theresponseblock.
Basic Transform Block¶
flow "create_user" {
from {
connector = "api"
operation = "POST /users"
}
transform {
id = "uuid()"
email = "lower(trim(input.email))"
name = "input.name"
created_at = "now()"
status = "input.age >= 18 ? 'active' : 'pending'"
}
to {
connector = "db"
target = "users"
}
}
Each line assigns a CEL expression to an output field. Expressions have access to several context variables.
Context Variables¶
| Variable | Description |
|---|---|
input |
Incoming data (request body, message payload, query result) |
output |
Output fields computed above this line in the same block |
enriched |
Data fetched from external services via enrich blocks |
step |
Results of named step blocks |
New to input and output? Input and Output explains where they come from, what output means in each block, and why the field name on the left is never written as output.name.
Named (Reusable) Transforms¶
Define a transform once, reference it from multiple flows:
# transforms/user_transforms.mycel
transform "normalize_user" {
email = "lower(trim(input.email))"
name = "trim(input.name)"
created_at = "now()"
}
transform "add_audit_fields" {
created_by = "ctx.user_id"
created_at = "now()"
updated_at = "now()"
}
Use in a flow:
flow "create_user" {
from {
connector = "api"
operation = "POST /users"
}
transform {
use = "transform.normalize_user"
id = "uuid()" # Add or override fields
}
to {
connector = "db"
target = "users"
}
}
Operators¶
# Arithmetic
input.price * input.quantity
input.total / 100
input.count + 1
input.balance - input.withdrawal
input.value % 2
# Comparison
input.age >= 18
input.status == "active"
input.role != "admin"
# Logical
input.active && input.verified
input.admin || input.moderator
!input.deleted
# Ternary
input.age >= 18 ? "adult" : "minor"
# String concatenation
input.first_name + " " + input.last_name
Mycel Built-in Functions¶
Identity and Timestamps¶
| Function | Returns | Description |
|---|---|---|
uuid() |
string | Generate a UUID v4 |
now() |
string | Current time in RFC3339 format |
now_unix() |
int | Current Unix timestamp (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 |
substring(s, start, end) |
(string, int, int) → string | Extract substring by byte indices |
split(s, sep) |
(string, string) → list | Split string into list |
join(list, sep) |
(list, string) → string | Join list items into string |
len(s) |
(string) → int | String length in bytes |
hash_sha256(s) |
(string) → string | SHA-256 hash (hex encoded) |
format_date(date, fmt) |
(string, string) → string | Reformat an ISO date string |
lower(trim(input.email))
replace(lower(input.name), " ", "_")
split(input.email, "@")[1] // Get domain part
join(input.tags, ", ")
hash_sha256(input.password)
// format_date tokens: YYYY MM DD HH mm ss
format_date(input.created_at, "YYYY-MM-DD")
format_date(now(), "DD/MM/YYYY")
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 |
Map Functions¶
| Function | Signature | Description |
|---|---|---|
merge(m1, m2) |
(map, map) → map | Merge two (or three, or four) maps; later values win |
omit(m, key) |
(map, string, ...) → map | Return map without specified keys (up to 3 keys) |
pick(m, key) |
(map, string, ...) → map | Return map with only specified keys (up to 3 keys) |
merge(step.order, { "customer": step.customer })
omit(input, "password", "secret")
pick(input, "id", "email", "name")
Array Functions¶
| Function | Signature | Description |
|---|---|---|
first(list) |
(list) → any | First element, or null if empty |
last(list) |
(list) → any | Last element, or null if empty |
flatten(list) |
(list of lists) → list | Flatten one level of nesting |
unique(list) |
(list) → list | Remove duplicate values |
reverse(list) |
(list) → list | Reverse list order |
pluck(list, key) |
(list, string) → list | Extract a field from each map in a list |
sort_by(list, key) |
(list, string) → list | Sort list of maps by a key (ascending) |
sum(list) |
(list) → number | Sum numeric values in a list |
avg(list) |
(list) → double | Average of numeric values in a list |
min_val(list) |
(list) → any | Minimum value in a list |
max_val(list) |
(list) → any | Maximum value in a list |
first(input.items)
last(input.items)
unique(input.tags)
reverse(input.ids)
pluck(input.orders, "total")
sort_by(input.products, "price")
sum(pluck(input.items, "price"))
avg(pluck(input.scores, "value"))
GraphQL Field Selection Functions¶
These are primarily useful when building GraphQL-aware flows to avoid over-fetching data.
| Function | Signature | Description |
|---|---|---|
has_field(input, path) |
(map, string) → bool | True if a field path was requested in the GraphQL query |
field_requested(input, path) |
(map, string) → bool | Alias for has_field |
requested_fields(input) |
(map) → list | Get all requested field paths |
requested_top_fields(input) |
(map) → list | Get only top-level requested fields |
has_field(input, "orders") // true if "orders" was queried
has_field(input, "orders.items") // true for nested field requests
CEL Standard Extensions¶
All CEL standard extensions are enabled.
Strings Extension (ext.Strings)¶
"hello".charAt(1) // "e"
"hello world".indexOf("o") // 4
"hello world".lastIndexOf("o") // 7
"hello".upperAscii() // "HELLO"
"HELLO".lowerAscii() // "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"
Encoders Extension (ext.Encoders)¶
Math Extension (ext.Math)¶
math.abs(-5) // 5
math.ceil(4.2) // 5
math.floor(4.8) // 4
math.round(4.5) // 5
math.sign(-10) // -1
math.greatest(1, 5, 3) // 5
math.least(1, 5, 3) // 1
math.isNaN(0.0/0.0) // true
math.isInf(1.0/0.0) // true
Lists Extension (ext.Lists)¶
[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]
Sets Extension (ext.Sets)¶
sets.contains([1, 2, 3], [2, 3]) // true (subset check)
sets.equivalent([1, 2], [2, 1]) // true (same elements)
sets.intersects([1, 2], [2, 3]) // true (any common element)
CEL Built-ins¶
Type Conversions¶
int(input.string_number) // "42" → 42
double(input.integer) // 42 → 42.0
string(input.number) // 42 → "42"
String Methods¶
input.name.startsWith("Dr.")
input.email.endsWith("@gmail.com")
input.text.contains("keyword")
input.code.matches("[A-Z]{3}[0-9]+") // regex match
input.name.size() // length
List Operations¶
input.items.size()
input.items[0]
"admin" in input.roles // contains check
input.items.exists(x, x > 10)
input.items.all(x, x > 0)
input.items.filter(x, x > 5)
input.items.map(x, x * 2)
Enrichment in Transforms¶
Named transforms can include enrich blocks to fetch external data:
transform "with_pricing" {
enrich "pricing" {
connector = "pricing_service"
operation = "getPrice"
params {
product_id = "input.id"
}
}
id = "input.id"
name = "input.name"
price = "enriched.pricing.price"
currency = "enriched.pricing.currency"
}
Real-World Examples¶
User Registration¶
transform {
id = "uuid()"
email = "lower(trim(input.email))"
username = "lower(replace(trim(input.username), ' ', '_'))"
password = "hash_sha256(input.password)"
role = "default(input.role, 'user')"
is_active = "true"
created_at = "now()"
}
Order Summary¶
transform {
order_id = "uuid()"
customer_id = "input.customer_id"
items_count = "input.items.size()"
subtotal = "sum(pluck(input.items, 'price'))"
tax = "output.subtotal * 0.21"
total = "output.subtotal + output.tax"
status = "'pending'"
created_at = "now()"
}
Fields are evaluated top to bottom in the order you wrote them, and each result
is added to output before the next line runs — so tax can reference
output.subtotal and total can reference both. A field cannot reference one
declared below it; that value is simply absent. See
Input and Output.
Conditional Routing¶
transform {
tier = "input.spend > 10000 ? 'platinum' : input.spend > 1000 ? 'gold' : 'standard'"
discount = "input.spend > 10000 ? 0.20 : input.spend > 1000 ? 0.10 : 0"
vip = "input.spend > 10000"
}
Error Handling¶
CEL expressions are compiled when Mycel loads the configuration. Errors surface before the service starts:
Error: failed to compile expression for 'email': CEL compile error:
undeclared reference to 'lowwer' (did you mean 'lower'?)
Run mycel validate to catch expression errors before deployment.