Skip to content

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.

<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'
  • 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")

Converts to uppercase via String(value).toUpperCase().

mapping:
currency: source.currency | uppercase
# "usd" → "USD"

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"

Replaces the first occurrence of search with replacement.

mapping:
phone: 'source.phone | replace("+", "00")'
# "+358401234567" → "00358401234567"

Replaces all occurrences.

mapping:
slug: 'source.title | lowercase | replace_all(" ", "-")'
# "Hello World Test" → "hello-world-test"

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"

Splits a string into an array. Default delimiter: ,.

mapping:
tags: source.tag_string | split(",")
# "a,b,c" → ["a", "b", "c"]

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"

Left-pads string to length with char (default: space).

mapping:
employee_code: 'source.emp_id | pad_left(10, "0")'
# "123" → "0000000123"

Right-pads string to length with char (default: space).

mapping:
field: 'source.code | pad_right(6, ".")'
# "abc" → "abc..."

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"

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.0

Absolute value.

mapping:
adjustment: source.adjustment_amount | abs
# -42 → 42

Rounds down to nearest integer.

mapping:
whole_units: source.fractional_qty | floor
# 3.7 → 3

Rounds up to nearest integer.

mapping:
billable_hours: source.tracked_hours | ceil
# 3.2 → 4

Converts to integer (truncates decimals). Throws on non-numeric input.

mapping:
quantity: source.qty | to_integer
# "42" → 42
# 3.99 → 3
# "-5.9" → -5

Converts to float. Throws on non-numeric input.

mapping:
unit_price: source.unit_price | to_float
# "3.14" → 3.14

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" → true

Formats a date string. Default format: full ISO string.

Format tokens (all UTC-based):

TokenMeaningExample
YYYY4-digit year2026
MM2-digit month (zero-padded)01
DD2-digit day (zero-padded)15
HH2-digit hours (zero-padded)10
mm2-digit minutes (zero-padded)30
ss2-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"

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" → 1767225600000

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"

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")'

Subtracts time from a date. Same units as date_add. Returns ISO string.

mapping:
grace_start: 'source.due_date | date_sub(3, "days")'

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)

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"

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 | required

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_null

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 are used in when: fields on steps, switch cases, and fan-out targets.

TypeExample
Comparisonsource.amount > 1000
Equalitysource.status == "active"
Inequalitysource.status != "draft"
Taggedtagged(high-value)
ANDsource.country == "FI" and source.amount > 1000
ORsource.type == "order" or source.type == "return"
NOTnot tagged(processed)
Grouped(source.type == "order" or source.type == "return") and source.status != "draft"

Operator precedence (highest to lowest): notandor. 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.


Beyond pipe transforms, mappings support several expression types:

mapping:
order_id: source.id
email: source.customer.email
first_item: source.items[0]
all_items: source.items[]

Data context prefixes:

PrefixDescription
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

Interpolate field references and transforms inside {{ }}:

mapping:
message: "Order {{source.id}} totaling {{source.total | decimal(2)}}"
mapping:
status: source.paid ? "confirmed" : "pending"
mapping:
fixed_string: "always-this-value"
fixed_number: 42
negative: -10.5
flag: true
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.sku

Rules:

  • Top-level array maps can omit as <name> (defaults to item.*)
  • 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

Reference a shared/reusable mapping definition:

mapping: use standard-order-mapping

Transformnull/undefined input
String transforms (uppercase, lowercase, etc.)Returns undefined
Numeric transforms (decimal, round, etc.)Returns undefined
to_integer, to_floatReturns undefined
to_booleanReturns false
nowIgnores input — always returns current timestamp
default(val)Returns val
requiredThrows error
omit_if_nullField excluded from output
coalesce(...)Falls through to args
lookup(table)Returns default arg or undefined
replace, replace_allReturns undefined
TransformError conditionResult
to_integerNon-numeric string (e.g., "abc")Throws Cannot convert "abc" to integer
to_floatNon-numeric stringThrows Cannot convert "xyz" to float
timestampInvalid date stringThrows Cannot convert "not-a-date" to timestamp
requirednull or undefinedThrows Required field is missing or null
lookupEmpty/missing table nameThrows 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.


flow: shopify-to-erp
version: 1
source:
connector: shopify-production
trigger: webhook
event: orders/create
target:
connector: erp-system
endpoint: /api/sales-orders
method: POST
mapping:
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