Transforms
Transforms are the core of fyrn’s data mapping. They reshape, convert, and validate data as it flows from source to target using pipe syntax.
Pipe syntax
Section titled “Pipe syntax”<field_or_literal> | <transform1> | <transform2>(arg) | <transform3>(arg1, arg2)Transforms chain left-to-right. The output of each transform feeds into the next.
mapping: # Single transform currency: source.currency | uppercase
# Chained transforms display_name: source.raw_name | trim | uppercase | required
# Transform with arguments total: source.total_price | decimal(2)
# Literal value piped through transform processed_at: '"" | now'Argument types
Section titled “Argument types”- Numbers: bare digits —
decimal(2),round(3),substring(0, 4) - Quoted strings: quotes are stripped —
replace("+", "00") - Bare strings: passed as-is —
default(N/A) - Multiple args: comma-separated —
pad_left(10, "0")
String transforms
Section titled “String transforms”uppercase
Section titled “uppercase”Converts to uppercase via String(value).toUpperCase().
mapping: currency: source.currency | uppercase # "usd" → "USD"lowercase
Section titled “lowercase”Converts to lowercase via String(value).toLowerCase().
mapping: slug: source.name | lowercase # "Hello World" → "hello world"Strips leading and trailing whitespace.
mapping: name: source.raw_name | trim # " Alice " → "Alice"replace(search, replacement)
Section titled “replace(search, replacement)”Replaces the first occurrence of search with replacement.
mapping: phone: 'source.phone | replace("+", "00")' # "+358401234567" → "00358401234567"replace_all(search, replacement)
Section titled “replace_all(search, replacement)”Replaces all occurrences.
mapping: slug: 'source.title | lowercase | replace_all(" ", "-")' # "Hello World Test" → "hello-world-test"substring(start, end?)
Section titled “substring(start, end?)”Extracts a substring. 0-indexed, end is exclusive and optional (omit to go to end of string).
mapping: country_code: source.phone | substring(0, 4) # "+358401234567" → "+358"split(delimiter?)
Section titled “split(delimiter?)”Splits a string into an array. Default delimiter: ,.
mapping: tags: source.tag_string | split(",") # "a,b,c" → ["a", "b", "c"]join(delimiter?)
Section titled “join(delimiter?)”Joins an array into a string. Default delimiter: ,. No-op if input is not an array.
mapping: full_address: 'source.address_parts | join(", ")' # ["123 Main St", "Suite 4", "NYC"] → "123 Main St, Suite 4, NYC"pad_left(length, char?)
Section titled “pad_left(length, char?)”Left-pads string to length with char (default: space).
mapping: employee_code: 'source.emp_id | pad_left(10, "0")' # "123" → "0000000123"pad_right(length, char?)
Section titled “pad_right(length, char?)”Right-pads string to length with char (default: space).
mapping: field: 'source.code | pad_right(6, ".")' # "abc" → "abc..."Numeric transforms
Section titled “Numeric transforms”decimal(places?)
Section titled “decimal(places?)”Formats a number to fixed decimal places. Returns a string. Default: 2 places. Non-numeric input returns original string.
mapping: total: source.total_price | decimal(2) # 149.5 → "149.50"round(places?)
Section titled “round(places?)”Rounds to N decimal places. Returns a number. Default: 0 places.
mapping: rating: source.average_rating | round(1) # 3.14159 → 3.1
unit_price: source.unit_price | to_float | round(2) # "19.999" → 20.0Absolute value.
mapping: adjustment: source.adjustment_amount | abs # -42 → 42Rounds down to nearest integer.
mapping: whole_units: source.fractional_qty | floor # 3.7 → 3Rounds up to nearest integer.
mapping: billable_hours: source.tracked_hours | ceil # 3.2 → 4Type conversion transforms
Section titled “Type conversion transforms”to_integer
Section titled “to_integer”Converts to integer (truncates decimals). Throws on non-numeric input.
mapping: quantity: source.qty | to_integer # "42" → 42 # 3.99 → 3 # "-5.9" → -5to_float
Section titled “to_float”Converts to float. Throws on non-numeric input.
mapping: unit_price: source.unit_price | to_float # "3.14" → 3.14to_boolean
Section titled “to_boolean”Coerces to boolean.
- Truthy:
true,"true","yes","1",1 - Falsy:
false,"false","no","0",0,null,undefined,"" - All other values:
Boolean(value)
mapping: is_priority: source.priority_flag | to_boolean # "yes" → true # 0 → false # "hello" → trueDate and time transforms
Section titled “Date and time transforms”date(format?)
Section titled “date(format?)”Formats a date string. Default format: full ISO string.
Format tokens (all UTC-based):
| Token | Meaning | Example |
|---|---|---|
YYYY | 4-digit year | 2026 |
MM | 2-digit month (zero-padded) | 01 |
DD | 2-digit day (zero-padded) | 15 |
HH | 2-digit hours (zero-padded) | 10 |
mm | 2-digit minutes (zero-padded) | 30 |
ss | 2-digit seconds (zero-padded) | 00 |
mapping: order_date: source.created_at | date("YYYY-MM-DD") # "2026-01-15T10:30:00Z" → "2026-01-15"
month: source.created_at | date("YYYY-MM") # "2026-01-15T10:30:00Z" → "2026-01"Returns the current timestamp as an ISO 8601 string. Ignores the piped value — typically used with a literal:
mapping: processed_at: '"" | now' # → "2026-02-26T12:00:00.000Z"timestamp
Section titled “timestamp”Converts a date string to Unix epoch milliseconds. Throws on invalid date strings.
mapping: created_epoch: source.created_at | timestamp # "2026-01-01T00:00:00.000Z" → 1767225600000timezone(tz)
Section titled “timezone(tz)”Converts a UTC date to a target IANA timezone. Output format: YYYY-MM-DDTHH:mm:ss.
mapping: local_time: 'source.created_at | timezone("Europe/Helsinki")' # "2026-06-15T12:00:00Z" → "2026-06-15T15:00:00"date_add(amount, unit?)
Section titled “date_add(amount, unit?)”Adds time to a date. Returns ISO string.
Units: days (default), hours, minutes, seconds.
mapping: due_date: 'source.invoice_date | date_add(30, "days")' shift_end: 'source.shift_start | date_add(8, "hours")'date_sub(amount, unit?)
Section titled “date_sub(amount, unit?)”Subtracts time from a date. Same units as date_add. Returns ISO string.
mapping: grace_start: 'source.due_date | date_sub(3, "days")'Null handling transforms
Section titled “Null handling transforms”default(fallback)
Section titled “default(fallback)”Returns the fallback value when input is null or undefined. Note: 0, "", and false are not replaced.
mapping: notes: source.notes | default("N/A") # null → "N/A" # 0 → 0 (not replaced) # "" → "" (not replaced)coalesce(field1, field2, …)
Section titled “coalesce(field1, field2, …)”Returns the first non-null/undefined value. Checks the piped value first, then each argument in order.
Arguments containing dots or starting with source/result/item are resolved as field paths. Otherwise treated as literal strings.
mapping: contact: 'source.contact_name | coalesce(source.company_name, source.email, "Unknown Partner")' # If contact_name is null but company_name is "Acme Corp" → "Acme Corp"required
Section titled “required”Passes value through if non-null/undefined. Throws "Required field is missing or null" if the value is null or undefined. Note: 0, "", and false pass through.
mapping: order_id: source.id | required email: source.customer.email | requiredomit_if_null
Section titled “omit_if_null”Excludes the field from output entirely when the value is null, undefined, or empty string "". 0 and false pass through.
mapping: notes: source.notes | omit_if_null ref: source.ref_code | trim | omit_if_nullLookup transforms
Section titled “Lookup transforms”lookup(table_name, default?)
Section titled “lookup(table_name, default?)”Translates a value using a named lookup table. Returns the mapped value, or the default if the key is not found, or undefined if no default is provided. Throws if table_name is empty.
mapping: warehouse: 'source.country | lookup("country-warehouses", "default-wh")' # Given table: {US: "warehouse-us", FI: "warehouse-eu"} # "US" → "warehouse-us" # "JP" → "default-wh" (fallback) # "FI" → "warehouse-eu"Conditions
Section titled “Conditions”Conditions are used in when: fields on steps, switch cases, and fan-out targets.
Operators
Section titled “Operators”| Type | Example |
|---|---|
| Comparison | source.amount > 1000 |
| Equality | source.status == "active" |
| Inequality | source.status != "draft" |
| Tagged | tagged(high-value) |
| AND | source.country == "FI" and source.amount > 1000 |
| OR | source.type == "order" or source.type == "return" |
| NOT | not tagged(processed) |
| Grouped | (source.type == "order" or source.type == "return") and source.status != "draft" |
Operator precedence (highest to lowest): not → and → or. Use parentheses to override.
Keywords and, or, not are only matched at word boundaries — they won’t trigger inside field names like source.command or source.not_null.
Expression types
Section titled “Expression types”Beyond pipe transforms, mappings support several expression types:
Field access
Section titled “Field access”mapping: order_id: source.id email: source.customer.email first_item: source.items[0] all_items: source.items[]Data context prefixes:
| Prefix | Description |
|---|---|
source. | Fields from the inbound payload |
result. | Fields from a call step result (via store_as) |
item. | Default iterator variable in array maps |
env. | Environment variables |
Template strings
Section titled “Template strings”Interpolate field references and transforms inside {{ }}:
mapping: message: "Order {{source.id}} totaling {{source.total | decimal(2)}}"Conditional (ternary)
Section titled “Conditional (ternary)”mapping: status: source.paid ? "confirmed" : "pending"Literals
Section titled “Literals”mapping: fixed_string: "always-this-value" fixed_number: 42 negative: -10.5 flag: trueArray mapping
Section titled “Array mapping”mapping: # Default iterator (item.*) line_items: source.line_items[] -> each: sku: item.sku qty: item.quantity price: item.price | decimal(2)
# Named iterator line_items: source.line_items[] -> each as line: sku: line.sku qty: line.quantity
# Nested (requires "as <name>" at depth > 0) orders: source.orders[] -> each as order: id: order.id items: order.lines[] -> each as line: order_ref: order.id sku: line.skuRules:
- Top-level array maps can omit
as <name>(defaults toitem.*) - Nested levels (depth > 0) must use
as <name> - Reserved names (cannot be used with
as):source,result,env,item - Maximum nesting depth: 5 levels
- Inner loops inherit all outer loop scopes
Mapping reference shorthand
Section titled “Mapping reference shorthand”Reference a shared/reusable mapping definition:
mapping: use standard-order-mappingNull handling summary
Section titled “Null handling summary”| Transform | null/undefined input |
|---|---|
String transforms (uppercase, lowercase, etc.) | Returns undefined |
Numeric transforms (decimal, round, etc.) | Returns undefined |
to_integer, to_float | Returns undefined |
to_boolean | Returns false |
now | Ignores input — always returns current timestamp |
default(val) | Returns val |
required | Throws error |
omit_if_null | Field excluded from output |
coalesce(...) | Falls through to args |
lookup(table) | Returns default arg or undefined |
replace, replace_all | Returns undefined |
Error behavior
Section titled “Error behavior”| Transform | Error condition | Result |
|---|---|---|
to_integer | Non-numeric string (e.g., "abc") | Throws Cannot convert "abc" to integer |
to_float | Non-numeric string | Throws Cannot convert "xyz" to float |
timestamp | Invalid date string | Throws Cannot convert "not-a-date" to timestamp |
required | null or undefined | Throws Required field is missing or null |
lookup | Empty/missing table name | Throws lookup() requires a table name |
When a transform throws, the error is collected. If any errors accumulate, the mapping returns an error result instead of the output.
Unknown transform names do not throw — the value passes through unchanged.
Complete example
Section titled “Complete example”flow: shopify-to-erpversion: 1source: connector: shopify-production trigger: webhook event: orders/createtarget: connector: erp-system endpoint: /api/sales-orders method: POSTmapping: order_id: source.id | required customer_email: source.customer.email | lowercase | required customer_name: source.customer.name | trim display_name: source.raw_name | trim | uppercase total: source.total_price | decimal(2) currency: source.currency | uppercase status: source.paid ? "confirmed" : "pending" processed_at: '"" | now' notes: source.notes | omit_if_null warehouse: 'source.shipping_address.country | lookup("country-warehouses", "default-wh")' contact: 'source.contact_name | coalesce(source.company_name, "Unknown")' due_date: 'source.created_at | date_add(30, "days") | date("YYYY-MM-DD")' pay_period: 'source.period_start_utc | date("YYYY-MM")' local_start: 'source.period_start_utc | timezone("Europe/Helsinki")' formatted_code: 'source.raw_code | trim | uppercase | pad_left(8, "0")' line_items: source.line_items[] -> each: sku: item.sku | required quantity: item.quantity | to_integer unit_price: item.price | to_float | round(2) subtotal: item.price | decimal(2)on_error: retry: 3x exponential(30s) then: dead-letter