Recipes
Last updated 23 May 2026

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
- Always provide a fallback for fields that might be empty (
default,coalesce, or an{% if %}). - Cap and sort any
findManyyou loop over. - Total in the database with
findSumandfindCountwhen you only need a figure. - Encode before embedding:
json_encodefor JSON,url_encodefor URLs,escapefor HTML, so special characters cannot break your output. - Preview against a real record before switching anything on.
Next steps
- Function reference: the full toolkit these recipes draw on.
- Where it runs & its limits: the rules these habits come from.