FluxBilling
Plugins

Variables & Expressions

Use {{ }} variables and expressions to pass data between nodes and your configuration.

Updated · 2026-09-03

Almost every field in a flow can hold a dynamic value instead of a fixed one. You write these with double curly braces — {{ ... }} — and inside the braces you put an expression the flow evaluates when it runs. This is how you take a customer's hostname from the flow's input, drop an API key from configuration into a request, reshape a response from an earlier node, or build a message out of live data.

This article explains what you can reference, the difference between a placeholder that is the whole value and one embedded in a longer string, and exactly which expression features evaluate — including where the evaluator stops, and what to use instead. For where these fields live see the Node Reference; for assembling them into a workflow see Building Flows.

What you can reference

Values available inside {{ }}
ReferenceWhat it is
inputThe data the flow was started with. For a provision flow that's the order and service details; for a webhook flow it's the incoming request.
config.<key>Your plugin's configuration values — the fields filled in when the plugin is set up (for example config.api_url, config.region).
a node's idThe output of an earlier node, by that node's id. If a node has the id get_status, then {{ get_status.data.state }} reads from its output.
item / indexInside a Loop, the current item and its zero-based position. Both names are configurable on the Loop node; these are the defaults.
a variable you setAny value stored earlier with a Set Variable node, by the name you gave it.
helpersIn a notification flow only: formatting helpers plus the panel URLs. See Notification Plugins.

Top-level fields of the input are also reachable directly — if the input has a serviceId, both {{ input.serviceId }} and {{ serviceId }} resolve to it.

{{ input.hostname }}
{{ config.api_url }}
{{ get_status.data.state }}
{{ item.id }}
{{ index }}
{{ chosen_plan }}

Whole-value vs. embedded placeholders

  • Whole value. If the field is exactly one placeholder and nothing else — {{ get_list.data.items }} — the raw value is returned and its type is preserved. Objects stay objects, arrays stay arrays, numbers stay numbers. Use this to pass a structured value through, such as handing an array to a Loop node's source or putting an object into a request body field.

  • Embedded. If a placeholder sits inside other text — Bearer {{ config.token }} or Hello {{ input.name }} — the resolved value is converted to text and spliced in. Objects and arrays become their JSON text, so reserve embedding for values meant to read as strings or numbers.

{{ input.items }}                  → the actual array (whole value)
Order #{{ input.orderId }} ready   → "Order #1234 ready" (embedded, text)

If a placeholder can't be resolved it is left exactly as written rather than becoming empty, which makes typos easy to spot when you inspect a run in History.

How expressions are evaluated

Placeholders are resolved once. The result is not re-scanned, so if a resolved value happens to contain {{ }} itself, that inner text is left alone. This keeps data values from being mistaken for template instructions.

One naming detail: identifiers use underscores, not hyphens. A node whose id contains a hyphen is still reachable — write it either way, {{ get-status.data }} or {{ get_status.data }}, and both resolve. Underscores are the simpler habit when you choose names yourself.

Expression features

Expressions run in a restricted evaluator with no access to anything outside the flow's own data — enough to compute, compare and reshape values, and nothing more.

Operators

Supported operators
FeatureExample
Comparisons{{ input.qty > 0 }}, {{ config.region == "eu" }}, {{ a !== b }}
Logical AND / OR{{ input.active && input.paid }}, {{ a || b }}
Ternary (if / else){{ input.qty > 1 ? "many" : "one" }}
Optional chaining{{ input.meta?.label }} (safe when meta is missing)
Arithmetic{{ input.price * input.qty }}, {{ total + tax }}, {{ n % 2 }}, {{ n ^ 2 }} (power)
Membership{{ "eu" in input.regions }}
Array and object literals{{ [input.a, input.b] }}, {{ { id: input.id, name: input.name } }}

|| is a logical OR, and doubles as a fallback: {{ config.name || "default" }}. It is not string concatenation — use + for that, or simply embed placeholders in literal text. && returns the first falsy operand, so {{ input.meta && input.meta.label }} is a safe guard against a missing object; add || "" when the field needs a definite value either way.

Note: the nullish operator ?? and the negation operator ! are not supported. Write {{ input.x || "fallback" }} instead of ??, and express a negation as a comparison, such as {{ input.active == false }}, or use a Condition node with the isFalse or notExists operator.

Helper functions

Built-in functions
HelperPurpose
Number(x), String(x), Boolean(x)Convert between types.
parseInt(x), parseFloat(x)Parse numbers out of strings.
Object.keys(o), Object.values(o), Object.entries(o), Object.fromEntries(a)Read an object's keys, values or pairs, or build one from pairs.
JSON.stringify(x), JSON.parse(s)Serialise to / parse from JSON text.
Array.isArray(x)Test whether a value is an array.
Math.round(x), Math.floor(x), Math.ceil(x), Math.abs(x)The four supported maths functions. Others, including Math.max and Math.min, are not available.
encodeURIComponent(s), decodeURIComponent(s)URL-encode / decode.
btoa(s), atob(s)Base64 encode / decode.
Date.now()The current time in milliseconds. No other date functions are available.

A call to one of these must make up the whole expression. You may index its result — {{ Object.values(input.map)[0] }} works — but you cannot read a property off it, so {{ Object.keys(input.meta).length }} does not resolve. Compute the list in one field and count it with a Transform node instead (see below).

String methods

Called directly on a value: toLowerCase, toUpperCase, trim, split, replace, replaceAll, slice, substring, startsWith, endsWith, includes, indexOf, lastIndexOf, toFixed, toString, join. Their arguments may be string or number literals or further expressions, and negative numbers work — {{ input.card.slice(-4) }} returns the last four characters.

Warning: apart from join, these methods treat their subject as text. Applied to an array they operate on its comma-joined text form, so {{ input.tags.includes("pri") }} is true for a tag list containing priority, and {{ input.tags.slice(0,1) }} returns the first character, not the first element. For real element tests use the array methods below, a Condition node with the in operator, or a Loop.

Array methods that take a callback

Expressions do support the five higher-order array methods — map, filter, find, some and every — each with a callback:

{{ input.servers.filter(s => s.online) }}          keep only the online servers
{{ input.servers.map(s => s.id) }}                 pull out just the ids
{{ input.links.find(l => l.rel === "approve") }}   the first matching entry
{{ input.servers.some(s => s.online) }}            true if any is online
{{ input.servers.map(s => s.id).join(",") }}       chain, then join to a string

Three callback shapes are accepted, and they behave identically:

s => s.online
(s) => s.online
function(s) { return s.online; }

Passing a bare converter also works for the common idioms: {{ input.slots.filter(Boolean) }} drops empty entries, and {{ input.ids.map(String) }} and {{ input.ids.map(Number) }} convert each element.

Within those five methods the limits are worth knowing up front:

  • The callback takes one parameter. There is no index or whole-array parameter.
  • The body is a single expression, using the operators and helpers on this page. A callback with several statements, or one that declares a variable, is not accepted.
  • Only those five methods take a callback. reduce, sort, forEach and flatMap are not available.
  • The call must end the expression. You can chain another array method or join onto it, but you cannot read a property off the result — so {{ input.servers.filter(s => s.online).length }} does not resolve.

Working alternative: filter in one place, count in another

Where you want a count, split the work across a Transform node mapping: put the filter in the mapping's Expression and pick length as its Transform.

Counting online servers with a Transform mapping
Mapping fieldValue
Target FieldonlineCount
Expressioninput.servers.filter(s => s.online)
Transformlength

The mapped value is then available downstream as {{ <transform-node-id>.onlineCount }}. The same trick covers the other cases: pair an expression with join to build a string, with first or last to pick an element, or with unique, sort or flatten to reshape the list. The Transform node also offers callback-free map, filter and find transforms driven by a field name and a value, which cover simple cases without any callback at all. When the per-item work is bigger than one expression — a request per item, say — use a Loop node and put the work in its body. See the Node Reference.

Small examples

{{ input.region || "us-east" }}                       fallback when region is empty
{{ Number(input.amount) * 100 }}                      currency units → minor units
{{ Math.round(input.amount * 100) }}                  round before sending
{{ input.status === "active" ? "Active" : "Off" }}    map a flag to a label
{{ Number(input.amount).toFixed(2) }}                 format to two decimals
{{ input.servers.length }}                            how many servers
{{ Object.keys(input.meta) }}                         the metadata field names
{{ input.name.trim().toLowerCase() }}                 normalise a name
{{ encodeURIComponent(input.query) }}                 make a value URL-safe
{{ input.meta?.label || "none" }}                     safe read with a fallback

When an expression returns nothing

An expression that resolves to nothing leaves the placeholder in place, which is usually enough to spot the problem in the run trace. Two frequent causes: a node id that doesn't match (check the id shown on the node, not its label), and a path into a response that is one level off (expand the node's output in the Execution Trace and read the real shape). A find that matched nothing also resolves to nothing — that is a normal result, not an error.


Related: Building Flows · Node Reference · Connections & Authentication · Configuration Settings · Notification Plugins