Language Basics

Last updated 23 May 2026

A short Flexie Scripting template with placeholders, an if block, and a for loop

Flexie Scripting is small on purpose: values, record fields, conditions, loops, and operators. Once you know that handful of shapes, you can read or write any script you'll encounter in templates, workflow actions, and dynamic endpoints.

Printing a value

Use {{ ... }} to print something:

{{ "Hello" }}            → Hello
{{ 42 }}                 → 42
{{ first_name }}         → the first_name of the current record

Anything between the braces is worked out first, then printed. You can combine text and values by stringing pieces together, but the usual way is to write plain text and only drop into {{ }} where a value belongs:

Dear {{ first_name }} {{ last_name }},

Reading record fields

When a script runs, it is given the record it is working on, a contact, a deal, an invoice, and so on. You read that record's fields directly, with no prefix:

{{ first_name }}
{{ email }}
{{ total_incl_tax }}
{{ stage }}

The name in the braces is the field's alias (its system name), which you can find next to the field itself in the admin field setup (Custom Fields).

There is no built-in contact., deal., or invoice. global. Writing {{ contact.first_name }} does not work, the current record's fields live at the top level. A prefixed path like {{ X.field }} is only valid when X is a name you have created yourself (see below).

Variables for any other record

To work with a record that is not the current one, fetch it with a look-up function (such as findOne) and assign the result to your own variable. From then on you read its fields under that variable name:

{% set primary = findOne("contact", "email", email) %}
{{ primary.first_name }}, {{ primary.phone }}

Here primary is a name you chose, it could be contact, match, c, or anything else. The dot-path is valid because primary is now a defined variable holding an associative result.

Data from the trigger and earlier steps (in a workflow)

Inside a workflow, anything the trigger carried or earlier steps stored is under the __data namespace:

{{ __data.incoming_email.subject }}
{{ __data.last_task.due_date }}

See Where it runs & its limits for what is available in each context.

Tip: square brackets for awkward names. If a key has spaces or special characters, or is held in a variable, use brackets: {{ row['field name'] }} or {{ row[chosenField] }}.

Setting your own variables

Use {% set %} to name a value once and reuse it:

{% set fullName = first_name ~ " " ~ last_name %}
{% set vatRate = 0.20 %}

Hello {{ fullName }}, VAT is {{ vatRate * 100 }}%.

The ~ operator joins (concatenates) text together.

Making decisions, if

Show or compute something only when a condition holds:

{% if amount > 10000 %}
  Priority deal, assign to a senior rep.
{% elseif amount > 1000 %}
  Standard deal.
{% else %}
  Small deal, handle via self-service.
{% endif %}

The short, inline form (a "ternary") is handy inside {{ }}:

{{ amount > 1000 ? "Large" : "Small" }}

And to supply a fallback when something is empty, use ??:

{{ phone ?? "no phone on file" }}

Repeating, for

Loop over a list of records or values:

{% set list = findMany("invoice", "contact_id", id, "due_date", "ASC", 50) %}
{% for inv in list %}
  Invoice {{ inv.number }}, {{ inv.total_incl_tax | number_format(2) }}
{% endfor %}

Inside the loop, inv is the loop variable (a name you chose) holding the current row, so inv.number is valid even though invoice.number directly would not be.

Inside a loop you get a loop helper with useful counters:

{% for item in items %}
  {{ loop.index }}. {{ item.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
Helper Meaning
loop.index Current pass, starting at 1
loop.index0 Current pass, starting at 0
loop.first True on the first pass
loop.last True on the last pass
loop.length Total number of passes

Operators you can use

Maths

+ - * / % (remainder) // (whole-number division) ** (power)

{{ (price * quantity) * (1 + vatRate) }}

Comparisons

== (equal) != (not equal) < > <= >= and membership tests in / not in:

{% if stage_id in [4, 5] %}...{% endif %}

Logic

and or not

{% if email and not unsubscribed %}...{% endif %}

Joining text

~ joins values into one string:

{{ "Hi " ~ first_name ~ " " ~ last_name ~ "!" }}

Tests, checking the shape of a value

{% if phone is defined %}...{% endif %}
{% if notes is empty %}...{% endif %}
{% if items is iterable %}...{% endif %}

Common tests: is defined, is null, is empty, is iterable, is even, is odd.

Comments

Anything between {# ... #} is ignored, never printed, never run. Use it to leave notes for whoever edits the template next:

{# Discount only applies to renewals, confirmed with finance #}

Inserting a field with the merge-field picker

Some editors (for example the email or template builder) offer a merge-field picker, a menu of available fields you can click to drop a value into your content without typing. That is the click-driven way to insert a single field value.

When you need anything beyond a plain value, formatting, a fallback, a condition, a calculation, a function, write Flexie Scripting with {{ }}:

{{ first_name | capitalize }}
{{ phone ?? "no phone on file" }}

The {{ }} form is the powerful one and is the focus of this guide.

Global values always available

A few values are present in every script:

Value What it holds
LOGGED_IN_USER The signed-in user running the action (their id, name, email, phone, role, timezone). Empty when no one is signed in (e.g. a background job).
NULL / EMPTY Convenient names for "nothing" or an empty string.
Prepared by {{ LOGGED_IN_USER.full_name }} on {{ now("Y-m-d") }}.

Administrators can also define custom global variables (a company name, a support address, a default VAT rate) that then become available by name in every template. Ask your administrator what globals are defined for your account.

Next steps