---
title: "Flexie Scripting: Recipes"
url: https://flexie.io/resources/flexie-scripting/recipes
description: "Complete, ready-to-adapt examples for the things people actually build. Every recipe uses only documented functions and notes where you would use it."
---

# Recipes

Last updated 23 May 2026

![A collection of Flexie Scripting snippets, an overdue invoice line, a tag toggle, a QR pay block](https://flexie.io/image/resources/flexie-scripting-recipes.png)

## 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](https://flexie.io/resources/dynamic-endpoints/overview) 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

* **Always provide a fallback** for fields that might be empty (`default`, `coalesce`, or an `{% if %}`).
* **Cap and sort** any `findMany` you loop over.
* **Total in the database** with `findSum` and `findCount` when you only need a figure.
* **Encode before embedding:** `json_encode` for JSON, `url_encode` for URLs, `escape` for HTML, so special characters cannot break your output.
* **Preview against a real record** before switching anything on.

## Next steps

* [Function reference](https://flexie.io/resources/flexie-scripting/function-reference): the full toolkit these recipes draw on.
* [Where it runs & its limits](https://flexie.io/resources/flexie-scripting/where-it-runs-and-limits): the rules these habits come from.
