Template Syntax
QuickFlo has two template languages, used in different places:
- LiquidJS is the template engine for step configuration — use it to reference step outputs, environment variables, connections, and utility values anywhere in a step’s input fields.
- JSONLogic is the expression language for control-flow conditions and data-pipeline predicates. It’s used by
core.if,core.switch,data.filter,data.reduce, anddata.classify. JSONLogic operands accept Liquid{{ template }}form for data access, so the two languages compose naturally — see Conditionals & Data-Pipeline Logic below.
Referencing Step Outputs
Section titled “Referencing Step Outputs”Step outputs are referenced by their step ID directly — not nested under a steps prefix:
{{ my-step-id.someOutputField }}For example, if you have a step with ID fetch-users that returns a list of users:
{{ fetch-users.data }}{{ fetch-users.data[0].name }}{{ fetch-users.totalCount }}Step Metadata
Section titled “Step Metadata”Every step exposes a $meta object with execution metadata:
{{ fetch-users.$meta.success }} // boolean — true if the step ran without throwing AND wasn't an error-severity operational failure{{ fetch-users.$meta.skipped }} // boolean — true if the step's skipCondition matched{{ fetch-users.$meta.durationMilliseconds }} // number — execution time in ms{{ fetch-users.$meta.stepType }} // string — step type name{{ fetch-users.$meta.operationalStatus }} // 'ok' | 'warning' | 'error' (see Operational Errors){{ fetch-users.$meta.error }} // object — present when the step *threw* (execution error){{ fetch-users.$meta.operationalErrors }} // array — present when classifyOutput() flagged the result (operational error)$meta.error vs $meta.operationalErrors vs $errors — three different surfaces, three different jobs:
$meta.error(per-step, singular object) — only set when the step’s code threw. Usestep.$meta.error.messageto read the thrown message.$meta.operationalErrors(per-step, array) — only set when the step ran successfully but the result was a failure (HTTP 4xx, SMTP rejection, file not found, LLM rate limit). The original output is still readable. Usestep.$meta.operationalErrors[0].codeand.message.$errors(workflow-global, array) — every error from every step in the run, aggregated. Use it to ask “did anything fail anywhere?” — see Operational Errors below.
For “did this specific step succeed?” — step.$meta.success is the simplest check. It’s false for both thrown and operational failures. See the Error Handling cheat sheet for the full mapping.
$meta.error is an object, not a string. When present, it has these fields:
| Field | Description |
|---|---|
.message | Human-readable error message |
.name | Error class name (e.g., WorkflowError, Five9Error) |
.code | Optional machine-readable code (e.g., HTTP_CLIENT_ERROR) |
.severity | Optional severity level |
.isRetryable | Optional boolean — true if the engine considers the error transient |
{{ fetch-users.$meta.error.message }}{{ fetch-users.$meta.error.code }}For operational errors specifically, use $meta.operationalErrors (an array) or the global $errors variable described below.
Template Variables
Section titled “Template Variables”These global variables are available in every template expression:
| Variable | Description |
|---|---|
{{ initial.* }} | Data passed when the workflow execution started |
{{ step-id.* }} | Output from a completed step, referenced by its step ID |
{{ $env.* }} | Environment variables |
{{ $connections.* }} | Connection credential objects |
{{ $vars.* }} | Workflow variables set by set-variable steps |
{{ $util.* }} | Utility generators (UUIDs, timestamps, etc.) |
{{ $errors.* }} | Array of operational and execution errors that have accumulated during the run — see Operational Errors below |
Initial Data
Section titled “Initial Data”The initial object contains the data that triggered the workflow execution. Its structure depends on the trigger type.
Webhook Triggers
Section titled “Webhook Triggers”When a workflow is triggered via webhook, the request body fields are spread to the root of initial, and a webhook context object provides access to the full request:
{{ initial.someFieldFromBody }}{{ initial.webhook.query.page }}{{ initial.webhook.query.filter }}{{ initial.webhook.headers['content-type'] }}{{ initial.webhook.body }}{{ initial.webhook.params }}The webhook object contains:
| Field | Description |
|---|---|
webhook.body | Raw request body |
webhook.query | URL query parameters (?key=value) |
webhook.params | URL path parameters |
webhook.headers | Request headers (lowercase keys) |
webhook.files | Uploaded files (multipart/form-data) with base64 buffers |
For JSON request bodies, all top-level fields are also accessible directly:
// POST body: { "userId": 123, "action": "sync" }{{ initial.userId }} // 123{{ initial.action }} // "sync"{{ initial.webhook.query.debug }} // query param ?debug=trueForm Triggers
Section titled “Form Triggers”Form submissions spread the submitted field values to the root and include a form context:
{{ initial.name }}{{ initial.email }}{{ initial.form.formName }}{{ initial.form.submittedAt }}{{ initial.form.authenticatedUser.username }}The form object contains:
| Field | Description |
|---|---|
form.triggerId | The trigger ID |
form.formName | Name of the form |
form.submittedAt | ISO 8601 timestamp of submission |
form.authenticatedUser | Present if the form requires authentication |
form.authenticatedUser.username | The authenticated user’s username |
form.authenticatedUser.connectionName | The form-auth connection used |
form.authenticatedUser.metadata | Custom metadata from the form-auth connection |
Schedule Triggers
Section titled “Schedule Triggers”Scheduled workflows receive the initialData configured in the trigger settings, spread directly to the root:
// initialData config: { "reportType": "daily", "emailTo": "admin@example.com" }{{ initial.reportType }} // "daily"{{ initial.emailTo }} // "admin@example.com"Event Triggers
Section titled “Event Triggers”Event payloads from connected services are spread directly to the root of initial. The structure depends on the event provider:
{{ initial.eventType }}{{ initial.data.agentId }}{{ initial.data.newState }}Manual Execution
Section titled “Manual Execution”When executing a workflow manually or via API, the provided initial data is spread to the root:
// API call: POST /workflow-templates/:id/execute { "initial": { "name": "John" } }{{ initial.name }} // "John"Workflow Variables ($vars)
Section titled “Workflow Variables ($vars)”The set-variable step writes values into $vars. Each set-variable step merges its output into the existing $vars object, so values persist and accumulate across the workflow:
{{ $vars.customerName }}{{ $vars.retryCount | default: 0 }}Connections ($connections)
Section titled “Connections ($connections)”Reference connection credential objects by their connection name:
{{ $connections.my-salesforce }}{{ $connections.postgres.host }}See Connections for setup guides.
Environment Variables ($env)
Section titled “Environment Variables ($env)”Reference encrypted environment variables. Variables from the workflow’s default environment are available at the root level:
{{ $env.API_KEY }}{{ $env.DATABASE_URL }}Variables from other environments are scoped by environment name:
{{ $env.staging.API_KEY }}See Environments for details on setup and connection redirection.
Operational Errors ($errors)
Section titled “Operational Errors ($errors)”$errors is a running array of every operational and execution error that has occurred in the workflow up to the current step. Use it in Continue on Error recovery steps to inspect what went wrong without having to remember which step failed. See the Error Handling guide for the full model.
Each entry has this shape:
{{ $errors | size }} // total entries so far{{ $errors[0].stepId }} // step that produced the error{{ $errors[0].type }} // 'operational' or 'execution'{{ $errors[0].message }} // human-readable error message{{ $errors[0].code }} // machine code (e.g., HTTP_CLIENT_ERROR){{ $errors[0].severity }} // 'error' or 'warning'$errors is populated even when Continue on Error isn’t set — but in that case the workflow halts on the first error, so only the failing step’s entry is visible. With Continue on Error enabled on a step (or globally), downstream steps can branch on $errors to react to upstream failures.
Utilities ($util)
Section titled “Utilities ($util)”$util provides zero-input generators — each call produces a fresh value:
| Utility | Type | Description |
|---|---|---|
$util.uuid | string | Random UUID v4 |
$util.now | string | Current ISO 8601 timestamp |
$util.timestamp | number | Unix timestamp (milliseconds) |
$util.timestampSeconds | number | Unix timestamp (seconds) |
$util.today | string | Today’s date (YYYY-MM-DD) |
$util.fileTimestamp | string | File-safe date/time (YYYY-MM-DD_HHmmss) |
$util.random | number | Random integer 0–100 |
$util.randomFloat | number | Random float 0–1 |
$util.password | string | Secure random 16-character password |
$util.randomHex | string | 32 random hex characters |
$util.randomBase64 | string | 32 random bytes as base64 |
$util.emptyObject | object | Empty {} |
$util.emptyArray | array | Empty [] |
$util.true | boolean | Boolean true |
$util.false | boolean | Boolean false |
$util.null | null | Null value |
Example:
{{ $util.uuid }}// → "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
{{ $util.now }}// → "2026-02-18T14:30:00.000Z"Filters
Section titled “Filters”Filters transform values using the pipe (|) syntax:
{{ fetch-users.name | upcase }}{{ fetch-users.items | size }}{{ some-step.createdAt | date: "%Y-%m-%d" }}QuickFlo supports all standard LiquidJS filters (string, math, array, date, etc.) plus the custom filters listed below.
Encoding
Section titled “Encoding”| Filter | Description | Example |
|---|---|---|
base64Encode | Encode string to Base64 | {{ "hello" | base64Encode }} → aGVsbG8= |
base64Decode | Decode Base64 string | {{ "aGVsbG8=" | base64Decode }} → hello |
fromJson | Parse JSON string to object | {{ jsonString | fromJson }} |
toJson | Convert value to JSON string | {{ obj | toJson }} |
| Filter | Description | Example |
|---|---|---|
basicAuth | Base64-encode username:password | {{ "user" | basicAuth: "pass" }} |
basicAuthHeader | Full Basic ... header value | {{ "user" | basicAuthHeader: "pass" }} |
basicAuthHeaderFromObject | Basic auth header from object with username/password fields | {{ creds | basicAuthHeaderFromObject }} |
Type Conversion
Section titled “Type Conversion”| Filter | Description | Example |
|---|---|---|
toString | Convert to string (objects become JSON) | {{ 123 | toString }} → "123" |
toNumber | Convert to number (0 if invalid) | {{ "42" | toNumber }} → 42 |
toInt | Convert to integer (truncates decimal) | {{ "12.9" | toInt }} → 12 |
toFloat | Convert to float | {{ "12.5" | toFloat }} → 12.5 |
toBoolean | Convert to boolean ("true", "1", "yes" → true) | {{ "true" | toBoolean }} → true |
toArray | Wrap value in array (parses JSON strings) | {{ "hello" | toArray }} → ["hello"] |
| Filter | Description | Example |
|---|---|---|
array | Create array from arguments | {{ "" | array: 1, 2, 3 }} → [1, 2, 3] |
range | Create number range | {{ 0 | range: 5 }} → [0, 1, 2, 3, 4] |
find | Find first item where field equals value | {{ users | find: "id", 42 }} |
distinctBy | Remove duplicates by field | {{ users | distinctBy: "email" }} |
distinctByFields | Remove duplicates by multiple fields | {{ items | distinctByFields: "name", "email" }} |
arrayConcat | Concatenate arrays | {{ arr1 | arrayConcat: arr2 }} |
push | Add item to end of array | {{ items | push: "new" }} |
Object
Section titled “Object”| Filter | Description | Example |
|---|---|---|
pick | Keep only specified keys | {{ user | pick: "name", "email" }} |
omit | Remove specified keys | {{ user | omit: "password" }} |
keys | Get array of object keys | {{ user | keys }} |
values | Get array of object values | {{ user | values }} |
objectMerge | Merge objects together | {{ defaults | objectMerge: overrides }} |
String
Section titled “String”| Filter | Description | Example |
|---|---|---|
padStart | Pad string at start to target length | {{ "5" | padStart: 3, "0" }} → "005" |
padEnd | Pad string at end to target length | {{ "hi" | padEnd: 5, "." }} → "hi..." |
substring | Extract substring by index | {{ "hello" | substring: 1, 4 }} → "ell" |
includes | Check if string contains substring | {{ "hello" | includes: "ell" }} → true |
startsWith | Check if string starts with prefix | {{ "hello" | startsWith: "he" }} → true |
endsWith | Check if string ends with suffix | {{ "hello" | endsWith: "lo" }} → true |
strConcat | Concatenate multiple values | {{ "Hello" | strConcat: " ", "World" }} → "Hello World" |
stripDiacritics | Remove accents and diacritical marks (é→e, ñ→n, ü→u) | {{ "José García" | stripDiacritics }} → "Jose Garcia" |
Comparison
Section titled “Comparison”| Filter | Description | Example |
|---|---|---|
eq | Equal | {{ status | eq: "active" }} |
ne | Not equal | {{ status | ne: "deleted" }} |
gt | Greater than | {{ age | gt: 18 }} |
gte | Greater than or equal | {{ score | gte: 100 }} |
lt | Less than | {{ price | lt: 50 }} |
lte | Less than or equal | {{ count | lte: 10 }} |
| Filter | Description | Example |
|---|---|---|
coalesce | First non-null value | {{ value | coalesce: "fallback" }} |
ternary | Conditional value selection | {{ isAdmin | ternary: "Admin", "User" }} |
and | Logical AND | {{ hasAccess | and: isActive }} |
or | Logical OR | {{ isAdmin | or: isManager }} |
not | Logical NOT | {{ isDeleted | not }} |
| Filter | Description | Example |
|---|---|---|
min | Minimum of values | {{ 5 | min: 3, 8 }} → 3 |
max | Maximum of values | {{ 5 | max: 3, 8 }} → 8 |
sum | Sum all numbers in array | {{ amounts | sum }} |
avg | Average of numbers in array | {{ scores | avg }} |
Date & Time
Section titled “Date & Time”| Filter | Description | Example |
|---|---|---|
now | Current ISO 8601 timestamp | {{ "" | now }} |
timestamp | Current Unix timestamp (ms) | {{ "" | timestamp }} |
formatCurrentDateTime | Format current time | {{ "" | formatCurrentDateTime: "YYYY-MM-DD" }} |
Cryptography
Section titled “Cryptography”| Filter | Description | Example |
|---|---|---|
hash | Cryptographic hash (sha256, sha512, md5) | {{ "data" | hash }} or {{ "data" | hash: "md5" }} |
hmac | HMAC signature | {{ "message" | hmac: "secret" }} |
password | Generate secure random password | {{ "" | password }} or {{ "" | password: 24, false }} |
randomHex | Random hex string | {{ "" | randomHex }} → 32 hex characters |
randomBase64 | Random base64 string | {{ "" | randomBase64 }} |
Telephony
Section titled “Telephony”All telephony filters use libphonenumber-js for parsing and validation.
| Filter | Description | Example |
|---|---|---|
toE164 | Convert to E.164 international format | {{ "202-555-1234" | toE164: "US" }} → +12025551234 |
e164ToNanpa | Convert E.164 to NANPA national format | {{ "+12025551234" | e164ToNanpa }} → (202) 555-1234 |
isValidPhone | Check if phone number is valid | {{ "202-555-1234" | isValidPhone: "US" }} → true |
isValidE164 | Check if strictly valid E.164 format | {{ "+12025551234" | isValidE164 }} → true |
formatPhone | Format to NATIONAL, INTERNATIONAL, or E.164 | {{ "+12025551234" | formatPhone: "INTERNATIONAL" }} |
phoneCountry | Get ISO country code | {{ "+12025551234" | phoneCountry }} → US |
phoneCallingCode | Get country calling code | {{ "+12025551234" | phoneCallingCode }} → 1 |
phoneNationalNumber | Get national number without country code | {{ "+12025551234" | phoneNationalNumber }} → 2025551234 |
The toE164, isValidPhone, and formatPhone filters accept an optional default country code (e.g., "US") for parsing numbers without a country prefix.
Row-Level Variables
Section titled “Row-Level Variables”When using data transformation steps like map, filter, or reduce, these variables are available within each iteration:
| Variable | Description |
|---|---|
{{ $item }} | Current item being processed |
{{ $original }} | Original item before transformation |
{{ $this }} | Alias for $item (useful for primitives) |
{{ $index }} | Zero-based iteration index |
{{ $isFirst }} | true for the first item |
{{ $isLast }} | true for the last item |
Example in a map step expression:
{{ $item.firstName }} {{ $item.lastName }} ({{ $index }})Liquid Control Flow
Section titled “Liquid Control Flow”These tags work inside Liquid template strings (step config fields, return values, etc.). For if/switch steps and data-pipeline predicates, see Conditionals & Data-Pipeline Logic below.
Templates support LiquidJS control flow tags for conditional content:
{% if fetch-users.$meta.success %} Found {{ fetch-users.data | size }} users{% else %} Step failed: {{ fetch-users.$meta.error.message }}{% endif %}See the LiquidJS tags documentation for if/elsif/else, unless, case/when, for loops, and more.
Conditionals & Data-Pipeline Logic (JSONLogic)
Section titled “Conditionals & Data-Pipeline Logic (JSONLogic)”core.if, core.switch, and the predicates inside data.filter, data.reduce, and data.classify evaluate JSONLogic expressions — not Liquid. JSONLogic is a JSON-shaped expression language with operators as keys and operands as arrays.
Template-Form Operands
Section titled “Template-Form Operands”The recommended way to reference data inside a JSONLogic operand is the template form — wrap a Liquid expression in a string:
{ "==": ["{{ $item.status }}", "active"] }QuickFlo’s resolver renders the templates first, then hands the rendered tree to the JSONLogic evaluator. This is the same syntax you use everywhere else in step config, so there’s nothing new to learn.
The legacy form uses a var operator with a path string:
{ "==": [{ "var": "$item.status" }, "active"] }Both forms still work, but the template form is preferred — it’s consistent with the rest of the platform and supports filters, default values, and the full Liquid grammar inside the operand.
Available Operators
Section titled “Available Operators”| Category | Operators |
|---|---|
| Equality | ==, !=, ===, !== |
| Comparison | >, <, >=, <= |
| Logic | and, or, !, !! |
| Branching | if (ternary-style) |
| Membership | in (substring or array contains) |
| Existence | var, missing, missing_some |
| Arithmetic | +, -, *, /, %, min, max |
| String | cat (concat), substr, strlen |
| Regex | matches — case-sensitive regex match (QuickFlo-specific, with an internal LRU cache) |
| Debug | log |
Examples
Section titled “Examples”core.if — branch on a step output:
{ "stepId": "is-paid", "stepType": "core.if", "input": { "condition": { ">": ["{{ fetch-customer.body.balance }}", 0] }, "then": { "steps": [ /* charge them */ ] }, "else": { "steps": [ /* skip */ ] } }}core.switch — multiple cases on the same value:
{ "stepId": "route", "stepType": "core.switch", "input": { "cases": [ { "id": "north-america", "when": { "in": ["{{ initial.country }}", ["US", "CA", "MX"]] }, "steps": [ /* ... */ ] }, { "id": "europe", "when": { "in": ["{{ initial.country }}", ["UK", "DE", "FR"]] }, "steps": [ /* ... */ ] } ], "default": { "steps": [ /* ... */ ] } }}data.filter — keep items matching a predicate:
{ "stepId": "active-only", "stepType": "data.filter", "input": { "items": "{{ fetch-users.body.data }}", "filter": { "and": [ { "==": ["{{ $item.status }}", "active"] }, { ">": ["{{ $item.lastLoginDays }}", 0] } ] } }}data.reduce — conditional aggregation:
{ "stepId": "summarize", "stepType": "data.reduce", "input": { "items": "{{ fetch-orders.body.data }}", "reduce": { "highValueCount": { "operation": "count", "condition": { ">": ["{{ $item.amount }}", 1000] } }, "totalRevenue": { "operation": "sum", "field": "{{ $item.amount }}" } } }}matches — regex predicate:
{ "matches": ["{{ $item.email }}", "@(gmail|yahoo)\\.com$"] }Literal Blocks
Section titled “Literal Blocks”Use {% literal %} to preserve template syntax that should not be resolved — useful when passing templates to external APIs:
{% literal %}Hello {{name}}, your order {{orderId}} is ready{% endliteral %}This outputs the raw string Hello {{name}}, your order {{orderId}} is ready without resolving the {{ }} expressions.
Recursive Resolution
Section titled “Recursive Resolution”Templates support up to 5 levels of recursive resolution. This means a template can resolve to another template, which resolves again:
{{ $env.MY_CONNECTION }}If $env.MY_CONNECTION contains the string {{$connections.production-db}}, the engine resolves it further to the actual connection object. This is the foundation of environment connection redirection.