A test that posts the same email address twice fails the second time. A signed request needs an HMAC over a body you only assembled a moment ago. A correlation header wants a fresh id per call.
Every JavaScript scope a flow can write has a set of $-prefixed helper functions for exactly
this. They need no import, no script node, and no dependency:
- id: create_user
type: request
config:
method: POST
url: ${{ variables.baseUrl }}/users
headers:
X-Request-Id: ${{ $uuid() }}
X-Signature: ${{ $hmac(variables.payload, env.SIGNING_KEY) }}
body: |
{
"email": "${{ $randomEmail() }}",
"name": "${{ $randomFullName() }}",
"createdAt": "${{ $isoTimestamp() }}"
}
next: null
Where they work
Everywhere a flow can run JavaScript, which is more places than it first sounds:
${{ }}interpolation in a URL, header, query or path param, or body- a
scriptnode’scode - every hook phase
- an
assertion’scheck, acondition’sexpression, aloop’sover - a
captureexpression
They are ordinary function calls, so the parentheses are not optional: ${{ $uuid }} interpolates
the function itself, not a value. Write ${{ $uuid() }}.
Identity and time
| Helper | Returns |
|---|---|
$uuid() | A v4 UUID |
$timestamp() | Unix epoch milliseconds, a number |
$isoTimestamp() | An ISO 8601 string, for example 2026-08-08T12:34:56.789Z |
Random values
| Helper | Returns |
|---|---|
$randomInt(min, max) | Integer in [min, max], both ends included |
$randomFloat(min, max) | Float in [min, max) |
$randomString(len?) | Alphanumeric string, len characters, 10 by default |
$randomBoolean() | true or false |
$randomEmail() | A plausible-looking email address |
$randomFirstName() | A first name |
$randomLastName() | A last name |
$randomFullName() | First and last, separated by a space |
$randomUserName() | A first.last123-shaped handle |
$randomIp() | An IPv4-shaped string |
$randomUrl() | An https:// URL |
$randomPhoneNumber() | A phone-number-shaped string |
$randomCity() | A city name |
$randomCountryCode() | An ISO-2 country code |
$randomColor() | A colour name |
$randomPrice(min?, max?) | A number with two decimal places, 1 to 1000 by default |
The name, city, country and colour helpers read from small hand-written lists, roughly thirty
entries each. They are there to make a fixture look like real data, not to model a population. If
you need a specific locale or a large unique set, generate it in a script node and capture it.
Encoding and signing
| Helper | Returns |
|---|---|
$base64(str) | Base64 encoding of str |
$base64decode(str) | The inverse |
$hash(str, algo?) | Hex digest, algo defaults to sha256 |
$hmac(str, key, algo?) | Hex HMAC digest keyed by key, algo defaults to sha256 |
$urlencode(str) | encodeURIComponent(str) |
algo is passed straight to Node’s crypto layer, so "sha1", "sha512" and the rest are all
available if an API asks for one.
These exist because the sandbox a flow runs in does not otherwise hand you node:crypto. The
sandbox is a typo boundary rather than a security boundary, so treat a flow file the way you would
treat any code in your repository.
Every call generates a new value
There is no per-run caching. Two references to $uuid() in the same node produce two different
ids, and that is usually what you want in a header. It is usually not what you want when the same
value has to appear in a body and then in a later assertion.
To reuse one value, generate it once in a script node and read it back from variables:
version: "1.0"
name: Idempotent order
nodes:
- id: mint_key
type: script
config:
code: |
variables.idempotencyKey = $uuid();
next: place_order
- id: place_order
type: request
config:
method: POST
url: ${{ variables.baseUrl }}/orders
headers:
Idempotency-Key: ${{ variables.idempotencyKey }}
body: '{ "sku": "ABC-1" }'
next: replay_order
- id: replay_order
type: request
config:
method: POST
url: ${{ variables.baseUrl }}/orders
headers:
# The same key, so a correct API returns the first order rather than making a second.
Idempotency-Key: ${{ variables.idempotencyKey }}
body: '{ "sku": "ABC-1" }'
capture:
replayedId: response.body.id
next: null
A capture does the same job
for a value that comes back from the API rather than one you generated.
Not the flow-level variables: block. Values declared there are seeded as they are written and
never go through ${{ }}, so key: ${{ $uuid() }} at the top of a file leaves variables.key
holding that text verbatim. Interpolation happens where the value is
used, not where it is declared. Use a script node, as above.
Signing a request body
$hmac is most useful in a hook, where
it can sign whatever the request ended up being rather than what you typed:
hooks:
before:
script: |
const ts = String($timestamp());
request.headers["X-Timestamp"] = ts;
request.headers["X-Signature"] = $hmac(ts + (request.body ?? ""), env.SIGNING_SECRET);
Hooks run before auth is resolved, so a signature
written here covers the body that actually ships.
Coming from Postman
Postman’s most-used built-in dynamic variables map onto these helpers at
import time: {{$guid}} becomes ${{ $uuid() }},
{{$randomEmail}} becomes ${{ $randomEmail() }}, and the same for $timestamp,
$isoTimestamp, $randomInt, the four name helpers, $randomIP, $randomUrl,
$randomPhoneNumber, $randomCity, $randomColor and $randomPrice.
They are converted into function calls rather than seeded as ordinary flow variables, which is what preserves the behaviour you had: each reference keeps regenerating, exactly as it did in Postman.
Postman has a much longer list than this. A {{$something}} with no counterpart is treated as an
ordinary variable reference and comes through as ${{ variables.something }} with an empty value,
so it shows up as an unset variable rather than disappearing. Point it at the nearest helper above,
or at a script node.
What to read next
- Variables and environments for the five places a value can
come from, and how
capturemoves one between steps. - Node reference for
scriptnodes, hooks, and thelog()binding. - Authentication for the cases where a typed
authblock beats a hand-signed header.