Recipes

Last updated 23 May 2026

A collection of Flexie Scripting snippets, an overdue invoice line, a tag toggle, a QR pay block

1. A personalised greeting that never looks broken

Where: email or SMS templates, or any message body.

Hi {{ coalesce(first_name, "there") }},

(Current record = the contact.) coalesce returns the first value that is not empty, so a missing first name falls back to there. You can chain as many fallbacks as you need, coalesce(a, b, c, "default").

2. An overdue-invoice line

Where: an invoice reminder email, or a workflow email action.

Current record = the invoice.

{% if status != "paid" and daysBetween(due_date, now()) > 0 %}
  Invoice {{ number }} is {{ daysBetween(due_date, now()) }} days overdue.
  Amount due: {{ formatCurrency(total_incl_tax, "€", 2, ".|,", "after") }}.
{% else %}
  Thank you, your account is up to date.
{% endif %}

3. A customer account summary block

Where: an account-overview email, a PDF, or a workflow note.

Current record = the account.

{% set dealVal = findDealsValue("account", "won", "*", id) %}
Account: {{ account_name }}
Won deals: {{ getDealStats("account", id).won }} ({{ formatCurrency(dealVal, "€") }})
Open cases: {{ getCaseStats("account", id).open }}
Outstanding invoices: {{ getInvoiceStats("account", id).pending }}

4. List a contact's invoices, grouped and subtotalled

Where: a statement email or PDF.

Current record = the contact.

{% set invoices = findMany("invoice", "contact_id", id, "due_date", "ASC", 200) %}

{% if invoices | length == 0 %}
  No invoices on file.
{% else %}
  {% for inv in invoices | sort_by("due_date") %}
    {{ inv.number }}, due {{ date(inv.due_date, "M j, Y") }},
    {{ inv.total_incl_tax | number_format(2) }} ({{ inv.status }})
  {% endfor %}

  Total billed: {{ invoices | sum_by("total_incl_tax") | number_format(2) }}
{% endif %}

Note the empty-list guard and the 200 cap, both keep the output clean and fast.

5. Generate a gap-free document number

Where: a workflow action that sets a field (for example stamping a reference on a new record).

{% set seq = incrementAndGetSequenceNumber(
     "invoice", "sequence_no", "year", now("Y")
   )
%}
REF-{{ now("Y") }}-{{ padString(seq, 5, "0") }}

Produces values like REF-2026-00042. incrementAndGetSequenceNumber bumps the counter safely even if two records are created at the same instant, so numbers never collide or skip; padString pads it to a fixed width.

6. Build the body of an outgoing webhook

Where: a workflow Webhook action's request-body field.

Current record = the contact.

{
  "external_id": "{{ id }}",
  "name": {{ (first_name ~ " " ~ last_name) | json_encode }},
  "email": {{ email | json_encode }},
  "lifetime_value": {{ findDealsValue("contact", "won", "*", id) }},
  "tags": {{ (tags | default("")) | split("|") | json_encode }}
}

Wrapping text values in | json_encode quotes and escapes them correctly, so a name with a quote or comma cannot break the JSON.

7. Turn an email body into a clean SMS

Where: a workflow that receives an email and sends an SMS summary.

{{ htmlToText(__data.incoming_email.html) | truncate(140, "…") }}

htmlToText strips the formatting; truncate keeps it within one message.

8. Add or remove a tag without losing the others

Where: a workflow Update action setting the tags field.

(Current record = the contact.)

{# Add "renewal-2026", keeping existing tags #}
{{ addTag(tags | default(""), "renewal-2026") }}

{# Remove "trial", keeping the rest #}
{{ removeTag(tags | default(""), "trial") }}

Tags are stored as a pipe-separated list; these helpers edit that list safely.

9. Branch a message by what a list contains

Where: any template or workflow field.

{% if matchAnyTag(tags | default(""), "vip|gold") %}
  You are a priority customer, your dedicated line is +1 555 0100.
{% else %}
  Reach us any time at support@example.com.
{% endif %}

10. A pay-by-QR block

Where: an invoice PDF or email.

Scan to pay:
{{ qrCode("https://pay.example.com/invoice/" ~ id, 2, 200, 200) }}

11. Read JSON that arrived from another system

Where: a workflow triggered by a dynamic endpoint or webhook, see the Dynamic Endpoints section.

{% set order = __data.webhook | json_decode %}
Order {{ order.id }}, {{ order.items | length }} items,
total {{ jsonPath(order, "$.totals.grand") | number_format(2) }}

12. A friendly "days since" or "days until" line

Where: any message body, adds a human tone instead of raw dates.

{% set days = daysBetween(date_added, now()) %}
{% if days == 0 %}
  joined us today
{% elseif days == 1 %}
  joined us yesterday
{% elseif days < 30 %}
  joined us {{ days }} days ago
{% else %}
  joined us {{ (days / 30) | round(0) }} months ago
{% endif %}

daysBetween is always positive, so this works both for past and future dates.

13. Decide what to send by what arrived

Where: a workflow on an incoming case or message.

{% set subjectLower = subject | lower %}
{% if contains(subjectLower, "refund") or contains(subjectLower, "money back") %}
  {% snippet "support_refund_reply" %}
{% elseif contains(subjectLower, "demo") %}
  {% snippet "sales_demo_reply" %}
{% else %}
  {% snippet "support_generic_reply" %}
{% endif %}

Snippets keep the long bodies out of the workflow and let support edit them in one place without re-touching the automation.

14. Total a contact's child records and use it as a threshold

Where: a workflow deciding whether a contact qualifies as VIP.

{% set deals = findDeals("contact", id) %}
{% set wonDeals = deals | filter(d => d.is_won) %}
{% set lifetime = wonDeals | sum_by("amount") %}

{% if lifetime >= 50000 %}
  VIP (lifetime value {{ lifetime | number_format(2) }})
{% endif %}

filter narrows a list to the items matching a condition, then sum_by totals one field across what is left.

15. Find-or-skip: only continue if a matching record exists

Where: a workflow that needs a pre-existing record to act on.

{% set match = findOne("contact", "email", customer_email) %}
{% if match and match.id %}
  Acting on contact #{{ match.id }} ({{ match.first_name }} {{ match.last_name }})
{% else %}
  No contact for {{ customer_email }}, skipping.
{% endif %}

findOne returns an empty result rather than throwing when there is no match, so the {% if match and match.id %} guard is what you check.

16. Turn pipe-delimited tags into a bulleted list

Where: an HTML email or PDF showing a contact's tags clearly.

{% set tagList = (tags | default("")) | split("|") | filter(t => t | trim) %}
{% if tagList | length > 0 %}
  <ul>
  {% for t in tagList | sort %}
    <li>{{ t | trim }}</li>
  {% endfor %}
  </ul>
{% endif %}

Splitting on | and filtering out empties handles records that happen to have adjacent delimiters or no tags at all.

17. Build a signed, time-limited public link

Where: an email that links to a public resource you only want valid for a day.

{% set link = signUrl("https://example.com/orders/" ~ order.id, 24) %}
View your order: {{ link }}

signUrl returns a tamper-proof URL that expires in the given number of hours, so even if a recipient forwards the link, it will not keep working forever.

18. Build a JWT for an outbound integration

Where: a workflow webhook calling a system that uses JWT auth.

{% set payload = {
    "sub": id | cast_string,
    "tenant": "acme",
    "exp": dateAdd(now(), 5, "minutes")
} %}
{% set token = jwtEncode(payload, "your-shared-secret", "HS256") %}

Authorization: Bearer {{ token }}

Put that into the Webhook action's headers field to authenticate the outgoing call.

19. Read a value out of a deeply-nested webhook reply

Where: a workflow step that branches on something specific in a JSON response stored as {{ __data.webhook }}.

{% set reply = __data.webhook | json_decode %}
{% set firstItemSku = jsonPath(reply, "$.order.items[0].sku") %}
{% set grandTotal = jsonPath(reply, "$.totals.grand") | cast_float(2) %}

First item: {{ firstItemSku ?? "(none)" }}, total: {{ grandTotal | number_format(2) }}

json_path (and the jsonPath function) lets you reach into nested data by path rather than chaining lots of […] lookups.

20. A safe "numbered list" loop, with last-item handling

Where: any body that needs a clean numbered list.

{% set items = findMany("invoice", "contact_id", id, "due_date", "ASC", 50) %}
{% for inv in items %}
  {{ loop.index }}. {{ inv.number }},
  {{ inv.total_incl_tax | number_format(2) }}{% if not loop.last %},{% endif %}
{% endfor %}
{% if items | length == 0 %}No invoices on file.{% endif %}

loop.index numbers the items; loop.last lets you handle the final separator properly so you do not end with a trailing comma.

Habits that keep recipes reliable

Next steps