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:
| Where | Example |
|---|---|
| Endpoint URL in a REST API step | https://someurl.com/orders/{{{ body.orderId }}} |
| Headers and Body in a REST API step | { "orderNumber": "{{ body.orderNumber }}" } |
| Subject and body of an Email step | Order {{ 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 write | The previous step returned | You 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.namemeans "thenameinsidecustomerinsidebody". - Capital letters matter.
body.orderIdandbody.orderidare 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.nicknameisn't there,Hi {{ body.nickname }}!becomesHi !. 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 write | The previous step returned | You get |
|---|---|---|
{{ body.items.[0].name }} | { "items": [ { "name": "Bolt" }, { "name": "Nut" } ] } | Bolt |
{{ body.items.[1].name }} | the same | Nut |
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 write | Value is Smith & Sons | You get |
|---|---|---|
{{ body.company }} | Smith & Sons | |
{{{ body.company }}} | Smith & Sons |
The characters that are replaced with two braces are & < > " ' ` and =. A name like O'Brien would turn into O'Brien.
| Use | When |
|---|---|
{{{ 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" }}
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
| Helper | What it does | Example | Result |
|---|---|---|---|
uppercase | Capitals | {{ uppercase "hello" }} | HELLO |
lowercase | Small letters | {{ lowercase "Hello" }} | hello |
capitalize | Capital first letter | {{ capitalize "anna svensson" }} | Anna svensson |
capitalizeAll / titleize | Capital first letter of every word | {{ titleize "anna svensson" }} | Anna Svensson |
sentence | Capital first letter of each sentence | {{ sentence "hello world" }} | Hello world |
trim | Remove spaces at the start and end | {{ trim " Anna " }} | Anna |
replace | Replace every occurrence of some text | {{ replace "a-b-c" "-" "/" }} | a/b/c |
append | Add text after | {{ append "Order" ":" }} | Order: |
prepend | Add text before | {{ prepend "123" "#" }} | #123 |
truncate | Keep only the first N characters | {{ truncate "Hello world" 5 }} | Hello |
ellipsis | Same, but add … if text was cut | {{ ellipsis "Hello world" 5 }} | Hello… |
reverse | Reverse the text | {{ reverse "abc" }} | cba |
split | Split text into a list | {{ split "a b c" " " }} | a,b,c |
dashcase | Words joined with - | {{ dashcase "Anna Svensson" }} | anna-svensson |
snakecase | Words joined with _ | {{ snakecase "Anna Svensson" }} | anna_svensson |
camelcase | Words joined, capital letters inside | {{ camelcase "hello world" }} | helloWorld |
url_encode | Make text safe inside a web address | {{ url_encode "a b&c" }} | a%20b%26c |
stripQuerystring | Remove everything from ? on in a web address | {{ stripQuerystring "https://x.com/a?b=1" }} | https://x.com/a |
sanitize | Remove HTML tags | {{ sanitize "<b>Hi</b>" }} | Hi |
Numbers
Numbers stored as text (like "12") work too.
| Helper | What it does | Example | Result |
|---|---|---|---|
add | Add | {{ add body.price 5 }} | price + 5 |
subtract | Subtract (first minus second) | {{ subtract 10 3 }} | 7 |
multiply | Multiply | {{ multiply body.qty body.unitPrice }} | qty × unit price |
divide | Divide (first by second) | {{ divide 10 4 }} | 2.5 |
modulo | Remainder after division | {{ modulo 7 3 }} | 1 |
abs | Remove a minus sign | {{ abs -4 }} | 4 |
round | Round to nearest whole number | {{ round 2.567 }} | 3 |
ceil / floor | Round up / round down | {{ ceil 1.2 }} {{ floor 1.8 }} | 2 1 |
toFixed | Fixed number of decimals (also pads with zeros) | {{ toFixed 1234.5 2 }} | 1234.50 |
addCommas | Thousands separators | {{ addCommas 1234567.891 }} | 1,234,567.891 |
toInt | Drop the decimals | {{ toInt "42.9" }} | 42 |
avg | Average | {{ avg 1 2 3 }} | 2 |
ordinalize | Add 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
| Helper | What it does | Example | Result |
|---|---|---|---|
isoDateTime | The current date and time in standard format | {{ isoDateTime }} | 2026-10-02T11:24:08.696Z |
dateFormat | Show a date the way you want it. Without a date, uses today. | {{ dateFormat "yyyy-MM-dd" body.orderDate }} | 2026-03-05 |
humanDate | Turn everyday words into a date and time | {{ humanDate "tomorrow" }} | tomorrow's date and time |
uuidv4 | A 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.
| Letters | Meaning | Example |
|---|---|---|
yyyy | Year | 2026 |
MM / MMM / MMMM | Month as number / short name / full name | 03 / Mar / March |
dd / d | Day of month | 05 / 5 |
EEEE / EEE | Weekday full / short | Thursday / Thu |
HH | Hour, 24-hour clock | 15 |
mm | Minutes | 30 |
ss | Seconds | 00 |
Some complete examples with a date of 2026-03-05T14:30:00Z:
| You write | You 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") }}
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
| Helper | What it does | Example | Result |
|---|---|---|---|
length | How many items | {{ length body.items }} | 2 |
join | Put items in one line with a separator | {{ join body.tags ", " }} | red, green, blue |
first / last | The first / last item | {{ first body.tags }} | red |
itemAt | The item at a position (starts at 0) | {{ itemAt body.tags 1 }} | green |
pluck | One field from every item | {{ join (pluck body.items "name") ", " }} | Bolt, Nut |
unique | Remove duplicates | {{ join (unique body.tags) ", " }} | each tag once |
inArray | Is a value in the list? Used in conditions. | {{#if (inArray body.tags "red")}}…{{/if}} |
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 write | True 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 write | Meaning |
|---|---|
{{ @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 see | Likely cause and fix |
|---|---|
The result still contains {{ … }}, exactly as you wrote it | The 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 blank | The 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 & Sons, O'Brien or " in the result | You 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 result | One 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 right | Double 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 hours | Time zone, see the note under Dates and times. |
| A condition is never true | Check 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 text | or 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,humanDateanddateFormat. - 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.