Skip to content

Code inputs, imports, and outputs

The Code step receives the same workflow context available elsewhere in QuickFlo. Return an object with named fields when downstream steps need to use the result.

These values are available directly. You do not need to import them:

BindingWhat it is
$envYour environment variables, such as $env.STRIPE_SECRET
$connectionsYour connection credentials by name. e.g. $connections['my-salesforce'].accessToken
$stepsOutputs from every step that ran before this one in the same workflow, keyed by step ID. e.g. $steps['fetch-customer'].body.email or $steps['parse-csv'].items
$varsWorkflow variables set by Set Variable steps. e.g. $vars.customerName
$errorsOperational errors emitted by prior steps in this run
$executionThe current run’s ID, workflow, environment, and start time
initialThe data that started the workflow, such as initial.city or initial.form.email—see Initial data

When the Code step runs inside a for-each, extra iteration bindings are also in scope — the same ones Liquid templates see as {{ $item }}, {{ $index }}, etc.:

BindingWhat it is
$itemThe current item from the for-each list
$indexThe current iteration’s zero-based index
$isFirsttrue on the first iteration
$isLasttrue on the last iteration
$indexPathIteration indexes across nested loops, outermost first, such as [0, 2]

Outside a for-each, the iteration bindings are undefined.

Field references that work in Liquid templates work the same way in code. {{ fetch-customer.body.email }} in a Liquid string is $steps['fetch-customer'].body.email in a code step.

You can import packages and modules directly:

SourceSyntax
npmimport phoneNumber from 'npm:libphonenumber-js'
jsrimport { parseCSV } from 'jsr:@std/csv'
HTTPS URLimport { z } from 'https://esm.sh/zod'
Standard packagesPackages published under @std

Keep imports at the top of the script:

import parsePhoneNumber from 'npm:libphonenumber-js'
import { DateTime } from 'npm:luxon'
const phone = parsePhoneNumber('+1 415 555 0100')
const reportedAt = DateTime.now().setZone('America/Los_Angeles')
return {
phone: phone.formatInternational(),
country: phone.country,
reportedAt: reportedAt.toFormat('EEEE, MMM d, yyyy h:mm a ZZZZ'),
}

Anything you console.log, console.info, console.warn, console.error, etc. is captured and shown in the Console Output panel below the editor when you Run the step, and in the execution trace when the workflow runs in production.

for (let i = 0; i < 100; i++) {
console.log({ message: `hello ${i}` })
}
return { processed: 100 }
The Code step editor after clicking Run, with the Console Output panel below the editor showing captured console.log lines and the final return value

Console output is capped at 1 MB. For larger results, return a concise summary and inspect it in the execution trace.

Whatever you return becomes the step’s output. The shape is flexible:

You returnDownstream sees
return { foo: 'bar', count: 42 }{ foo: 'bar', count: 42 } — referenced as {{ my-code.foo }}, {{ my-code.count }}
return [1, 2, 3]{ data: [1, 2, 3] } — referenced as {{ my-code.data }}
return 'hello'{ data: 'hello' } — primitives are wrapped in data
return null or no return at all{} — empty object

For predictable downstream access, return an object with named fields. The auto-wrap for primitives and arrays is a safety net; explicit objects are clearer.

A downstream Set Variable step referencing a code step's return value via the template autocomplete — the autocomplete dropdown shows the code step's output fields (`found: bool`, `userId: str`) inferred from the most recent run, and a preview pane on the right resolves `code-rxn4.found` to `true`

The screenshot above shows the round trip: a code step that returned { found: true, userId: '...' } exposes those exact field names to downstream steps. The Set Variable step’s autocomplete picks them up automatically — including their inferred types (bool, str) — and the preview pane on the right resolves the reference to its actual value. No type stubs to write, no schema to maintain.

By default, the way to fail a Code step is to throw:

if (!$env.STRIPE_SECRET) {
throw new Error('STRIPE_SECRET environment variable is not set')
}

A throw stops the workflow like any other failed step. Turn on Continue on Error only when the later steps can still produce a useful result. See Error Handling.

There are also two opt-in error keys you can include in your return value. The rest of the return object is preserved and inspectable downstream — just like an HTTP step’s 4xx response leaves the body readable.

$error marks the step as failed. The workflow stops unless Continue on Error is enabled:

const result = await fetchSomething()
if (result.status >= 400) {
return {
statusCode: result.status,
body: result.body,
$error: 'Upstream returned ' + result.status,
}
}
return { ok: true, data: result.body }

$warning records a warning and lets the workflow continue:

return {
partialData: rows.slice(0, 50),
$warning: {
code: 'PARTIAL_SYNC',
message: 'Only the first 50 rows were processed',
},
}

Both keys accept a string or { code, message }. If both are present, $error takes precedence.

The key is stripped from the visible output before downstream steps see it. The error or warning remains available in the step metadata and the global $errors array.

The execution trace showing a Code step with an operational error — the failure badge is visible alongside the error code and message, while the rest of the return object is still inspectable below

A returned $error is permanent by default. To retry a specific case, give it a code and add that code to Retryable Error Codes in the step’s error settings:

try {
const result = await callPartnerApi(input)
return { result }
} catch (err) {
if (err.status === 429) {
// Temporary: use a code that the step is configured to retry
return { $error: { code: 'RATE_LIMITED', message: 'Partner rate limit hit' } }
}
// Permanent: stop here unless Continue on Error is enabled
return { $error: { code: 'PARTNER_ERROR', message: err.message } }
}

With RATE_LIMITED listed as a retryable code, QuickFlo retries that result up to the configured limit. PARTNER_ERROR remains permanent. See Customize retries when the default is wrong.