Skip to content

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: transform reshapes data before sending to the destination. To reshape the response after receiving from the destination, use the response block.

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
default(input.nickname, input.name)
coalesce(input.phone, "N/A")

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)

base64.encode(b"hello")         // "aGVsbG8="
base64.decode("aGVsbG8=")       // b"hello"

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.