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.
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
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.
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.
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")
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.
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.
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.
| 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 |
| 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) |
| 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 |
| 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) |
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.
| 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) |
| 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.
| 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).
| 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") |
| 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) |
input.query.id
Returns the value if present, None if missing. None is falsy, so this works directly in If conditions.
https://api.example.com/users/{"{{ input.body.user_id }}"}?format={"{{ input.body.format }}"}
{"{{ merge(pick(webhook.body, \"email\", \"name\"), {\"processed_at\": now()}) }}"}
{"{{ [item for item in input.body.users if item.active] }}"}
{"{{ sum(pluck(input.body.line_items, \"amount\")) }}"}
{"{{ \"Premium\" if input.body.plan == \"pro\" else \"Free\" }}"}
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
If input.body.status == 200 returns False unexpectedly, the status is likely a string "200". Use eq():
eq(input.body.status, 200)
Use safe_get() or .get() with a default:
safe_get(input, "body.user.address.city", "Unknown")
input.body.get("optional", "default")
- If Plugin — boolean condition branching
- Switch Plugin — multi-case routing
- Transform Plugin — data reshaping with templates
- Environment Variables — managing secrets
- Expressions & Conditions Guide — workflow recipes
