Expressions

Expressions let you access upstream data, transform values, and build dynamic content inside your workflows — without writing a full Code node.

Two contexts — Template fields (URLs, JSON bodies, email subjects) use {"{{ }}"} syntax. Expression fields (If/Switch conditions) accept bare expressions directly.


Data Access

Use dot notation to access fields on any upstream node output:

input.body.name
webhook.query.id
http_request.data.results

Use bracket notation for keys with special characters:

input.headers["Content-Type"]
input.body["my-field"]

Use index notation for arrays:

input.body.items[0]
input.body.users[0].email

What is input?

input is the data wired to the current node's input handle. It's whatever the upstream node sent through the edge.

For a webhook trigger, input contains body, headers, query, method, and path. For other nodes, input is whatever that node emitted.

Upstream Aliases

Every node has an emit alias — a human-readable name for its output. You can reference any upstream node's data by its alias:

| Node | Alias | Example | |------|-------|---------| | Webhook Trigger | webhook | webhook.body.email | | API Trigger | api_request | api_request.query.id | | Transform | transform | transform.name | | HTTP Request | http_request | http_request.data | | Code | code | code.result |

If multiple nodes share the same alias (e.g. two Transform nodes), the second gets a suffix: transform, transform_1.

Safe Access

For paths that might be missing, use .get() with a default:

input.body.get("optional_field", "default_value")

Or use the safe_get() helper for deep paths:

safe_get(input, "body.user.address.city", "Unknown")

Or use coalesce() for first-non-null:

coalesce(input.body.nickname, input.body.name, "Anonymous")

Environment Variables

Access secrets and config values with env():

env("API_KEY")
env("DATABASE_URL", "postgres://localhost/dev")

env() checks workflow secrets first, then falls back to OS environment variables. The second argument is a default.


Template Fields vs Expression Fields

Template fields — use {"{{ }}"}

Used in: URLs, HTTP headers, email subjects, JSON body templates, Transform object mode.

Hello {"{{ input.body.name }}"}, your order #{"{{ input.body.order_id }}"} is ready
https://api.example.com/users/{"{{ input.body.user_id }}"}

When the entire field is a single {"{{ }}"} block, the original type is preserved (dict, list, number). When mixed with text, the result is always a string.

Expression fields — bare expressions

Used in: If conditions, Switch conditions.

input.body.status == 200

No {"{{ }}"} needed. Just write the expression directly.

Old workflows with {"{{ }}"} in If/Switch conditions still work — the runtime strips them automatically.


Operators

Comparison

| Operator | Meaning | Example | |----------|---------|---------| | == | Equal | input.body.status == 200 | | != | Not equal | input.body.role != "guest" | | > | Greater than | input.body.age > 18 | | < | Less than | input.body.price < 100 | | >= | Greater or equal | input.body.score >= 80 | | <= | Less or equal | input.body.count <= 10 | | in | Membership | "admin" in input.body.roles |

Logic

| Operator | Meaning | Example | |----------|---------|---------| | and | Both true | input.body.active and input.body.verified | | or | Either true | input.body.role == "admin" or input.body.role == "owner" | | not | Negate | not is_empty(input.body.tags) |

Arithmetic

| Operator | Example | |----------|---------| | + | input.body.price + input.body.tax | | - | input.body.total - input.body.discount | | * | input.body.price * input.body.quantity | | / | input.body.completed / input.body.total * 100 | | ** | 2 ** 10 | | % | input.body.index % 2 |


Type Conversion & Checking

| Function | Description | Example | |----------|-------------|---------| | to_int(val) | Convert to int (default 0) | to_int(input.body.quantity) | | to_float(val) | Convert to float (default 0.0) | to_float(input.body.price) | | to_str(val) | Convert to string | to_str(input.body.id) | | to_bool(val) | Convert to bool (handles "true"/"yes") | to_bool(input.body.active) | | is_empty(val) | True for None, "", [], | is_empty(input.body.tags) | | is_number(val) | True for int, float, or numeric string | is_number(input.body.price) | | typeof(val) | Type name: "string", "number", "list" | typeof(input.body.data) |


Comparison Helpers

Python's == is strictly typed — "200" == 200 is False. When working with webhook payloads where values may arrive as strings, use these:

| Function | Description | Example | |----------|-------------|---------| | eq(a, b) | Smart equality (coerces string/number) | eq(input.body.status, 200) | | neq(a, b) | Smart not-equal | neq(input.body.status, 200) |

eq("200", 200)      # True
eq(3.14, "3.14")    # True
eq(None, "")        # False

When to use eq() vs ==: Use == when both sides have the same type. Use eq() when a value might be a string or number — common with query parameters, form data, and webhook payloads.


String Helpers

| Function | Description | Example | |----------|-------------|---------| | upper(s) | Uppercase | upper(input.body.name) | | lower(s) | Lowercase | lower(input.body.email) | | title(s) | Title Case | title(input.body.name) | | strip(s) | Remove whitespace | strip(input.body.text) | | slug(s) | Slugify | slug(input.body.title) | | truncate(s, n) | Truncate to n chars | truncate(input.body.desc, 100) | | split(s, sep) | Split into list | split(input.body.csv, ",") | | join(items, sep) | Join list into string | join(input.body.tags, ", ") | | replace(s, old, new) | Replace substring | replace(input.body.text, "old", "new") | | starts_with(s, prefix) | Check prefix | starts_with(input.body.url, "https") | | ends_with(s, suffix) | Check suffix | ends_with(input.body.file, ".pdf") | | contains(s, needle) | Check membership | contains(input.body.email, "@") | | match(s, pattern) | Regex match | match(input.body.text, "\\d{4}") | | extract_email(s) | First email from text | extract_email(input.body.message) | | extract_url(s) | First URL from text | extract_url(input.body.message) |


Number & Math

| Function | Description | Example | |----------|-------------|---------| | abs(n) | Absolute value | abs(input.body.diff) | | round(n, d) | Round to d decimals | round(input.body.price, 2) | | min(a, b) | Minimum | min(input.body.x, input.body.y) | | max(a, b) | Maximum | max(input.body.scores) | | sum(items) | Sum of list | sum(input.body.amounts) | | clamp(val, lo, hi) | Clamp to range | clamp(input.body.value, 0, 100) | | percentage(part, total) | Percentage | percentage(input.body.done, input.body.total) | | to_fixed(val, d) | Round to string | to_fixed(input.body.price, 2) |

The Python math module is also available: math.sqrt(), math.ceil(), math.floor(), math.pi.


Date & Time

| Function | Description | Example | |----------|-------------|---------| | now() | Current UTC datetime (ISO) | now() | | today() | Current UTC date (YYYY-MM-DD) | today() | | format_date(dt, fmt) | Format date string | format_date(input.body.created, "%B %d, %Y") | | parse_date(s) | Parse to ISO format | parse_date(input.body.date) | | add_days(dt, n) | Add N days | add_days(now(), 7) | | add_hours(dt, n) | Add N hours | add_hours(now(), -2) | | time_diff(dt1, dt2, unit) | Difference | time_diff(now(), input.body.created, "hours") |

Common format codes: %Y (year), %m (month), %d (day), %H (hour), %M (minute), %B (month name).


Data Structure Helpers

| Function | Description | Example | |----------|-------------|---------| | keys(obj) | Dict keys as list | keys(input.body.data) | | values(obj) | Dict values as list | values(input.body.scores) | | length(obj) | Safe len (0 for None) | length(input.body.items) | | flatten(lst) | Flatten one level | flatten(input.body.nested) | | unique(lst) | Deduplicate | unique(input.body.tags) | | sort_by(lst, key) | Sort dicts by key | sort_by(input.body.users, "name") | | group_by(lst, key) | Group dicts by key | group_by(input.body.orders, "status") | | pluck(lst, key) | Extract field from each | pluck(input.body.users, "email") | | pick(obj, ...) | Select keys | pick(input.body, "name", "email") | | omit(obj, ...) | Remove keys | omit(input.body, "password") | | merge(d1, d2) | Merge dicts | merge(input.body.defaults, input.body.overrides) | | coalesce(a, b, ...) | First non-None | coalesce(input.body.nick, input.body.name, "Anon") |


JSON & Encoding

| Function | Description | Example | |----------|-------------|---------| | to_json(obj) | Serialize to JSON | to_json(input.body.data) | | from_json(s) | Parse JSON string | from_json(input.body.json_text) | | json_path(obj, path) | Deep dot-path access | json_path(input, "body.user.city") | | base64_encode(s) | Base64 encode | base64_encode(input.body.data) | | base64_decode(s) | Base64 decode | base64_decode(input.body.encoded) | | url_encode(s) | URL percent-encode | url_encode(input.body.query) | | url_decode(s) | URL percent-decode | url_decode(input.body.encoded) | | md5(s) | MD5 hash | md5(input.body.email) | | sha256(s) | SHA-256 hash | sha256(input.body.payload) |


Common Patterns

Check if a field exists

input.query.id

Returns the value if present, None if missing. None is falsy, so this works directly in If conditions.

Dynamic URL

https://api.example.com/users/{"{{ input.body.user_id }}"}?format={"{{ input.body.format }}"}

Build a payload from multiple nodes

{"{{ merge(pick(webhook.body, \"email\", \"name\"), {\"processed_at\": now()}) }}"}

Filter a list

{"{{ [item for item in input.body.users if item.active] }}"}

Aggregate values

{"{{ sum(pluck(input.body.line_items, \"amount\")) }}"}

Conditional value

{"{{ \"Premium\" if input.body.plan == \"pro\" else \"Free\" }}"}

Troubleshooting

"name 'input' is not defined"

input is only available when data is wired to the node's input handle. If no edge is connected, use an upstream alias directly:

webhook.body.email

Type mismatch with ==

If input.body.status == 200 returns False unexpectedly, the status is likely a string "200". Use eq():

eq(input.body.status, 200)

Missing nested field

Use safe_get() or .get() with a default:

safe_get(input, "body.user.address.city", "Unknown")
input.body.get("optional", "default")

See Also

On this page