Template Syntax
QuickFlo uses LiquidJS as its template engine. Templates let you reference step outputs, environment variables, connections, and utility values anywhere in your workflow step configuration.
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{{ fetch-users.$meta.error }} // error message (if failed){{ fetch-users.$meta.skipped }} // boolean{{ fetch-users.$meta.durationMilliseconds }} // execution time in ms{{ fetch-users.$meta.stepType }} // step type nameTemplate 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.) |
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.
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" |
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 }})Conditional Logic
Section titled “Conditional Logic”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 }}{% endif %}See the LiquidJS tags documentation for if/elsif/else, unless, case/when, for loops, and more.
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.