Skip to main content

Templating Reference: Filling in Values with {{ }} and Helpers

Many fields in Vince Platform let you write a piece of text with placeholders in it. When the workflow runs, each placeholder is replaced with real data, for example an order number from an earlier step. This is the same idea as a mail merge in Word: you write the letter once, and the names and addresses are filled in for each recipient.

The placeholders are written with double curly braces, {{ and }}. The language behind them is called Handlebars, so you may see that name in other places. You don't need to know anything about it to use this guide.

What you'll learn​

  • How to insert a value from an earlier step
  • How to reach into nested data and pick items from lists
  • When to use two braces and when to use three
  • How to change a value with helpers: upper/lower case, rounding, date formats and much more
  • How to show or hide text depending on a value, and how to repeat text for every item in a list
  • How to find out why a placeholder isn't working

Where you can use it​

Anywhere a field says it supports values from earlier steps. Typical places:

WhereExample
Endpoint URL in a REST API stephttps://someurl.com/orders/{{{ body.orderId }}}
Headers and Body in a REST API step{ "orderNumber": "{{ body.orderNumber }}" }
Subject and body of an Email stepOrder {{ body.orderNumber }} is on its way
Configuration values that take a JSON object{ "endpoint": "https://someurl.com/orders/{{ body.orderId }}" }

The placeholders are filled in when the workflow runs, using the data from the step before. The examples in this guide all use body, which is the name of that data. See Using data from earlier steps for the other names available, such as header and $context.

The basics​

Insert a value​

Put the path to a value between double braces:

You writeThe previous step returnedYou get
Order {{ body.orderId }}{ "orderId": 42 }Order 42
Hello {{ body.customer.name }}!{ "customer": { "name": "Anna" } }Hello Anna!

Things to know:

  • Dots go deeper. body.customer.name means "the name inside customer inside body".
  • Capital letters matter. body.orderId and body.orderid are different things.
  • Spaces inside the braces don't matter. {{body.orderId}} and {{ body.orderId }} are the same.
  • A value that doesn't exist becomes empty text. There is no error. If body.nickname isn't there, Hi {{ body.nickname }}! becomes Hi !. See Fallback values to handle this.
  • The result is always text. "qty": "{{ body.qty }}" gives you "qty": "3" with quotes, not the number 3.

Pick an item from a list​

If a value is a list, count from 0. The first item is 0, the second is 1, and so on:

You writeThe previous step returnedYou get
{{ body.items.[0].name }}{ "items": [ { "name": "Bolt" }, { "name": "Nut" } ] }Bolt
{{ body.items.[1].name }}the sameNut

To work with all items in a list rather than one, see Repeating for every item.

Double braces or triple braces?​

The templating language was built for web pages, where characters like < and & have a special meaning. With two braces it protects you by replacing them with safe codes. With three braces, the value is inserted exactly as it is.

You writeValue is Smith & SonsYou get
{{ body.company }}Smith &amp; Sons
{{{ body.company }}}Smith & Sons

The characters that are replaced with two braces are & < > " ' ` and =. A name like O'Brien would turn into O&#x27;Brien.

UseWhen
{{{ three braces }}}The result is not an HTML page: web addresses, plain-text emails, identifiers, anything a computer reads. In Endpoint URL, always use three.
{{ two braces }}The result is HTML, such as an HTML email, and the value is plain text that might contain < or &.

Also use three braces if the value contains HTML that you built yourself and want to be shown as formatting.

Helpers: changing a value​

A helper is a small built-in tool that does something with a value: makes it upper case, rounds a number, formats a date. You write the helper's name first, then the value(s) it should work on, separated by spaces:

{{ uppercase body.customer.name }}

If the name is Anna Svensson, this gives ANNA SVENSSON.

Some helpers need more than one value. They always go in the order shown in the tables below. Text goes in quotes:

{{ replace body.status "SHIPPED" "Sent" }}
Single or double quotes

Both work, but if you are writing the placeholder inside a JSON object (like a configuration value), use single quotes: {{ replace body.status 'SHIPPED' 'Sent' }}. Double quotes there can make the whole template fail silently. The examples below use double quotes for readability, so switch to single ones in that situation.

Combining helpers​

To use the result of one helper as the input of another, put the inner one in parentheses. The inner one is worked out first:

{{ uppercase (trim body.customer.name) }}
{{ dateFormat "yyyy-MM-dd" (humanDate "in 3 days") }}

Text​

HelperWhat it doesExampleResult
uppercaseCapitals{{ uppercase "hello" }}HELLO
lowercaseSmall letters{{ lowercase "Hello" }}hello
capitalizeCapital first letter{{ capitalize "anna svensson" }}Anna svensson
capitalizeAll / titleizeCapital first letter of every word{{ titleize "anna svensson" }}Anna Svensson
sentenceCapital first letter of each sentence{{ sentence "hello world" }}Hello world
trimRemove spaces at the start and end{{ trim " Anna " }}Anna
replaceReplace every occurrence of some text{{ replace "a-b-c" "-" "/" }}a/b/c
appendAdd text after{{ append "Order" ":" }}Order:
prependAdd text before{{ prepend "123" "#" }}#123
truncateKeep only the first N characters{{ truncate "Hello world" 5 }}Hello
ellipsisSame, but add … if text was cut{{ ellipsis "Hello world" 5 }}Hello…
reverseReverse the text{{ reverse "abc" }}cba
splitSplit text into a list{{ split "a b c" " " }}a,b,c
dashcaseWords joined with -{{ dashcase "Anna Svensson" }}anna-svensson
snakecaseWords joined with _{{ snakecase "Anna Svensson" }}anna_svensson
camelcaseWords joined, capital letters inside{{ camelcase "hello world" }}helloWorld
url_encodeMake text safe inside a web address{{ url_encode "a b&c" }}a%20b%26c
stripQuerystringRemove everything from ? on in a web address{{ stripQuerystring "https://x.com/a?b=1" }}https://x.com/a
sanitizeRemove HTML tags{{ sanitize "<b>Hi</b>" }}Hi

Numbers​

Numbers stored as text (like "12") work too.

HelperWhat it doesExampleResult
addAdd{{ add body.price 5 }}price + 5
subtractSubtract (first minus second){{ subtract 10 3 }}7
multiplyMultiply{{ multiply body.qty body.unitPrice }}qty × unit price
divideDivide (first by second){{ divide 10 4 }}2.5
moduloRemainder after division{{ modulo 7 3 }}1
absRemove a minus sign{{ abs -4 }}4
roundRound to nearest whole number{{ round 2.567 }}3
ceil / floorRound up / round down{{ ceil 1.2 }} {{ floor 1.8 }}2 1
toFixedFixed number of decimals (also pads with zeros){{ toFixed 1234.5 2 }}1234.50
addCommasThousands separators{{ addCommas 1234567.891 }}1,234,567.891
toIntDrop the decimals{{ toInt "42.9" }}42
avgAverage{{ avg 1 2 3 }}2
ordinalizeAdd st, nd, rd, th{{ ordinalize 3 }}3rd

Calculations on prices can produce long decimals, for example {{ divide 7 3 }} gives 2.3333333333333335. Wrap the result in toFixed to tidy it up:

{{ toFixed (divide body.total body.qty) 2 }}

Dates and times​

HelperWhat it doesExampleResult
isoDateTimeThe current date and time in standard format{{ isoDateTime }}2026-10-02T11:24:08.696Z
dateFormatShow a date the way you want it. Without a date, uses today.{{ dateFormat "yyyy-MM-dd" body.orderDate }}2026-03-05
humanDateTurn everyday words into a date and time{{ humanDate "tomorrow" }}tomorrow's date and time
uuidv4A random unique ID{{ uuidv4 }}6610059c-a34c-483f-ad2c-1fc975b610d8

Note that dateFormat takes the format first, then the date.

Common format letters. Letters are case sensitive: MM is month, mm is minutes.

LettersMeaningExample
yyyyYear2026
MM / MMM / MMMMMonth as number / short name / full name03 / Mar / March
dd / dDay of month05 / 5
EEEE / EEEWeekday full / shortThursday / Thu
HHHour, 24-hour clock15
mmMinutes30
ssSeconds00

Some complete examples with a date of 2026-03-05T14:30:00Z:

You writeYou get
{{ dateFormat "yyyy-MM-dd" body.date }}2026-03-05
{{ dateFormat "dd.MM.yyyy" body.date }}05.03.2026
{{ dateFormat "EEEE d MMMM yyyy" body.date }}Thursday 5 March 2026
{{ dateFormat "yyyyMMdd" }}today's date, for example 20261002

humanDate understands phrases such as tomorrow, yesterday, next friday, in 3 days and 2 days ago. It gives a full date and time, so combine it with dateFormat to show only the date:

{{ dateFormat "yyyy-MM-dd" (humanDate "in 3 days") }}
Time zones

dateFormat shows times in the time zone of the server that runs your workflow, which is usually UTC, not your local time. A time of 14:30 in the data may therefore show as a different hour than you expect. Check one example before relying on a time of day in a message to customers.

Lists​

HelperWhat it doesExampleResult
lengthHow many items{{ length body.items }}2
joinPut items in one line with a separator{{ join body.tags ", " }}red, green, blue
first / lastThe first / last item{{ first body.tags }}red
itemAtThe item at a position (starts at 0){{ itemAt body.tags 1 }}green
pluckOne field from every item{{ join (pluck body.items "name") ", " }}Bolt, Nut
uniqueRemove duplicates{{ join (unique body.tags) ", " }}each tag once
inArrayIs a value in the list? Used in conditions.{{#if (inArray body.tags "red")}}…{{/if}}
Sorting changes the data

sort, sortBy and reverse rearrange the list itself, not just a copy. Everything else in the same template then sees the rearranged list. If that is a surprise, do the sorting in a Transform step instead.

Fallback values​

default supplies text when a value is missing:

Hello {{ default body.nickname "customer" }}

This gives Hello customer when there is no nickname, and Hello Anna when there is. It only helps when the value is missing. A value that is present but empty ("") stays empty. To cover that case too, use {{#if}}, see below.

Showing text only sometimes​

Use {{#if}} … {{/if}}. The text in between is shown only when the condition is true. Add {{else}} for what to show otherwise.

{{#if body.express}}Express delivery{{else}}Standard delivery{{/if}}

Always close what you open: every {{#if}} needs an {{/if}}.

What counts as "true"​

On its own, {{#if body.something}} is true when the value is:

  • a non-empty text,
  • a number other than 0,
  • true,
  • a list with at least one item,
  • an object.

It is false when the value is missing, empty text, 0, false, or an empty list. This makes {{#if body.items}} a handy way to ask "are there any items?".

Comparing values​

Put a comparison in parentheses after if:

You writeTrue when
{{#if (eq body.status "SHIPPED")}}the status is exactly SHIPPED (capitals matter)
{{#if (isnt body.status "SHIPPED")}}the status is anything but SHIPPED
{{#if (gt body.qty 10)}}quantity is greater than 10
{{#if (gte body.qty 10)}}greater than or equal to 10
{{#if (lt body.qty 10)}}less than 10
{{#if (lte body.qty 10)}}less than or equal to 10
{{#if (contains body.name "Anna")}}the text contains Anna

Note that eq is strict about types: the number 1 and the text "1" are not equal. Values from some systems arrive as text even when they look like numbers.

Combine conditions with and, or and not:

{{#if (and body.express (gt body.qty 10))}}Large express order{{/if}}
{{#if (or (eq body.status "SHIPPED") (eq body.status "DELIVERED"))}}On its way or delivered{{/if}}
{{#if (not body.paid)}}Payment missing{{/if}}

{{#unless x}} … {{/unless}} is the opposite of if: shown only when the condition is not true.

Repeating for every item​

Use {{#each}} … {{/each}} to repeat text once for every item in a list. Inside, you write the item's fields directly, without body.items in front.

Given:

{ "items": [ { "name": "Bolt", "price": 10 }, { "name": "Nut", "price": 20 } ] }

This template:

{{#each body.items}}
- {{ name }}: {{ price }} kr
{{/each}}

gives:

- Bolt: 10 kr
- Nut: 20 kr

Useful extras inside a loop:

You writeMeaning
{{ @index }}Position of the current item, starting at 0
{{ add @index 1 }}Position starting at 1, for numbered lists
{{#if @first}} … {{/if}}Only for the first item
{{#if @last}} … {{/if}}Only for the last item
{{ this }}The item itself, for lists of plain values such as ["red", "green"]
{{ ../body.orderId }}A value from outside the loop

Inside the loop, body no longer means the whole previous step, it is looked for within the current item. To reach back out to the original data, add ../ in front, as in the last row above.

A comma-separated list with no comma after the last item:

{{#each body.items}}{{ name }}{{#unless @last}}, {{/unless}}{{/each}}

gives Bolt, Nut. (For plain lists, {{ join body.tags ", " }} is shorter.)

Add {{else}} to show something when the list is empty:

{{#each body.items}}{{ name }}{{else}}No items{{/each}}

You can combine loops and conditions:

{{#each body.items}}{{#if (gt price 15)}}{{ name }} is expensive{{/if}}{{/each}}

Ready-to-use examples​

A web address with an order number​

https://someurl.com/orders/{{{ body.orderId }}}

Use three braces in addresses so that special characters are not replaced. If the value could contain spaces or &, add url_encode: https://someurl.com/search?q={{{ url_encode body.searchText }}}.

Configuration with a value filled in​

{ "endpoint": "https://someurl.com/orders/{{ body.orderId }}" }

Placeholders can be put in any text value inside JSON. Only the placeholder itself is replaced, and the rest of the JSON stays as it is.

A request body with a formatted date and fallback​

{
"orderNumber": "{{ body.orderNumber }}",
"customer": "{{ uppercase (trim body.customerName) }}",
"deliveryDate": "{{ dateFormat 'yyyy-MM-dd' (humanDate 'in 3 days') }}",
"comment": "{{ default body.comment 'No comment' }}"
}

An email subject that changes with the result​

{{#if body.hasFailures}}⚠ {{ body.failedCount }} of {{ body.total }} orders failed{{else}}✅ All {{ body.total }} orders sent{{/if}}

An email with a table of rows​

<p>Hello {{ default body.customerName "customer" }},</p>
<p>Your order <b>{{ body.orderNumber }}</b> contains {{ length body.lines }} items:</p>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
{{#each body.lines}}
<tr>
<td>{{ name }}</td>
<td>{{ qty }}</td>
<td>{{ toFixed price 2 }} kr</td>
</tr>
{{/each}}
</table>
<p>Expected delivery: {{ dateFormat "EEEE d MMMM" body.deliveryDate }}.</p>

See Email a Run Summary for a complete walk-through.

When something doesn't work​

What you seeLikely cause and fix
The result still contains {{ … }}, exactly as you wrote itThe template has a mistake somewhere, and the whole text is then left untouched, with no error message. Look for: a helper name that doesn't exist or is misspelled, a missing closing {{/if}} or {{/each}}, a missing quote or parenthesis, or a helper given the wrong kind of value. Test the pieces one at a time.
A part is blankThe path doesn't exist in the data, often because of capital letters, a typo, or being inside a loop (see ../). Check the previous step's actual output in the execution log.
Smith &amp; Sons, O&#x27;Brien or &quot; in the resultYou used two braces where the result isn't HTML. Use three: {{{ … }}}.
[object Object]The value is a whole group of fields, not a single value. Go one level deeper, such as body.customer.name, or use {{#each}}.
A calculation gives a blank resultOne of the values isn't a number (for example missing, empty, or text like n/a). Check the input in the execution log.
The whole JSON configuration is left untouched although the placeholder looks rightDouble quotes inside a placeholder, such as {{ default body.x "none" }}, can break when the template is a JSON object. Use single quotes inside placeholders instead: {{ default body.x 'none' }}.
A time is off by one or two hoursTime zone, see the note under Dates and times.
A condition is never trueCheck capital letters, and whether a number is really text. eq body.qty 3 is false when qty is "3".
{{ or a "text" }} shows true instead of the textor answers yes or no, it doesn't pick a value. Use default instead.

Quick reference​

I want to…Write
Insert a value{{ body.name }}
Insert a value into a URL or plain text{{{ body.name }}}
Pick the first list item{{ body.items.[0].name }}
Make text upper case{{ uppercase body.name }}
Use a fallback{{ default body.name "unknown" }}
Round to 2 decimals{{ toFixed body.price 2 }}
Format a date{{ dateFormat "yyyy-MM-dd" body.date }}
Today's date{{ dateFormat "yyyy-MM-dd" }}
A date a few days ahead{{ dateFormat "yyyy-MM-dd" (humanDate "in 3 days") }}
Count items{{ length body.items }}
List items on one line{{ join (pluck body.items "name") ", " }}
Show text if something is true{{#if body.paid}}Paid{{else}}Unpaid{{/if}}
Compare to a value{{#if (eq body.status "SHIPPED")}}…{{/if}}
Repeat for every item{{#each body.items}}{{ name }}{{/each}}
A unique ID{{ uuidv4 }}
Add a comment that isn't shown{{!-- my note --}}

Going further​

The helpers in this guide are the most useful of around 150 available. They come in two groups:

  • Vince's own helpers: isoDateTime, uuidv4, humanDate and dateFormat.
  • A well-known open-source helper library, organised in these categories: array, collection, comparison, html, inflection, match, math, number, object, regex, string and url.

The full list of the open-source helpers, with more examples, is in the Handlebars helpers documentation. Helpers that read files or reach into the system's internals are switched off for security reasons, and the documentation is written for developers. Verify an example with a small test before building on it, as a few helpers described there may not behave as written or may not be available.

For complex calculations or reshaping data, a Transform step with JSONata is usually a better tool than a long template.