Template variables and syntax
The template language opendesk fills in replies, signatures, auto-replies, layouts and the satisfaction email, and every variable it offers.
Replies, saved replies, automation replies, signatures, auto-replies, email layouts and the satisfaction email all use the same template language and the same variables. This page is the reference. For where each template is set, see Email templates and signatures.
Writing templates in the app
Section titled “Writing templates in the app”You don’t need to remember the syntax. In every template editor (signatures, auto-replies, saved replies, footers, layouts and the satisfaction email):
- Type
{{to pick a variable. The list searches as you type. - Type
|after a variable, inside the braces, to pick a helper such asdefaultortruncate. - Type
{%to pick ready-made logic: show a part only when a value is set, if and else, pick by value, list each label, and more. It goes in as working code with the part to change selected, so you can type over it straight away. - Or click Insert for the same three lists: Variables, Logic and Helpers.
Tags show as coloured pills: blue for variables, violet for logic, red for something that isn’t right. When you save, opendesk names each problem and suggests a fix when it looks like a typo (“There is no variable called contact.frist_name. Did you mean contact.first_name?”). The preview next to the editor shows the email filled in, with sample data or a real ticket.
In the reply box, {{ inserts this ticket’s actual values, and {% adds logic that is filled in when you send.
Syntax
Section titled “Syntax”A template is ordinary text or HTML with two kinds of tags: {{ ... }} writes a value, {% ... %} adds logic. Everything else is copied as it is. The language follows Liquid, so most Liquid you already know works.
Variables
Section titled “Variables”Write a variable between double braces. Names are lower case, with dots between the parts. The spaces inside the braces are optional, so {{contact.name}} and {{ contact.name }} are the same.
Hi {{ contact.first_name }},Some variables are lists, like {{ ticket.labels }} or a multi-select field. Written on their own they read as the items joined with , (Billing, Refund); loop over them with for.
Filters
Section titled “Filters”Add a filter after a | to change a value. Filters run left to right, and a filter’s values go after a colon, separated by commas.
{{ contact.first_name | default: "there" | capitalize }}{{ ticket.subject | remove: "Re: " | truncate: 40 }}Text
| Filter | What it does | Example |
|---|---|---|
default |
The fallback when the value is empty. | {{ contact.first_name | default: "there" }} |
upcase |
Upper case. | {{ team.name | upcase }} reads BILLING |
downcase |
Lower case. | {{ contact.email | downcase }} |
capitalize |
The first letter upper case. | {{ ticket.status | capitalize }} |
titlecase |
Every word starting upper case. | {{ contact.name | titlecase }} |
strip |
Spaces at both ends removed. | {{ contact.name | strip }} |
truncate |
Cut to a number of characters, the ending (default …) included. |
{{ ticket.subject | truncate: 40 }} |
truncatewords |
Cut to a number of words. | {{ ticket.subject | truncatewords: 5, "..." }} |
append, prepend |
Text added at the end or the start. | {{ ticket.number | prepend: "#" }} |
replace, replace_first |
Every match, or the first, replaced. | {{ ticket.subject | replace: "Re: ", "" }} |
remove |
Every match removed. | {{ ticket.subject | remove: "[urgent]" }} |
slice |
Characters from a position, with an optional length. | {{ contact.name | slice: 0, 1 }} reads J |
url_encode |
Safe inside a link. | {{ contact.email | url_encode }} |
Lists
| Filter | What it does | Example |
|---|---|---|
join |
A list into text, with the separator (default , ). |
{{ ticket.labels | join: " / " }} |
split |
Text into a list, at each separator. | {{ contact.fields.notes | split: "," }} |
first, last |
The first or last item, or word of text. | {{ ticket.labels | first }} |
size |
How many items, or characters of text. | {{ ticket.labels | size }} |
sort, uniq |
Alphabetical order; repeats removed. | {{ contact.labels | sort | join }} |
Numbers and dates
| Filter | What it does | Example |
|---|---|---|
plus, minus, times, divided_by |
Arithmetic. | {{ ticket.fields.seats | times: 12 }} |
round |
Rounds, to a number of decimals (default 0). | {{ ticket.fields.score | round: 1 }} |
date |
A date in another format, with strftime codes: %Y %m %d %e %B %b %A %a %H %I %M %p. "now" is the current time, in the workspace’s time zone. |
{{ ticket.created_date | date: "%A, %b %e" }} reads Sunday, Oct 18 |
A filter’s value can be a quoted string, a number or another variable: {{ contact.first_name | default: company.name }}.
Conditions
Section titled “Conditions”{% if %} shows its content only when the condition holds. {% elsif %} and {% else %} are optional.
{% if agent.phone %} Call me on {{ agent.phone }}.{% elsif agent.title %} {{ agent.title }}, {{ team.name }}{% else %} Just reply to this email.{% endif %}A variable on its own is true when it has a value (a list, when it has items). Compare with:
| Operator | Means |
|---|---|
==, != |
Equal, not equal. Text ignores capital letters and uses the value as it reads in a message ("on hold" for the status). |
>, <, >=, <= |
Greater or smaller. Numbers compare as numbers. |
contains |
A list has the item, or text has the words. |
== empty, != empty |
Empty or not (blank and nil mean the same). |
Combine conditions with and, or and not, and group them with parentheses. and binds tighter than or.
{% if ticket.priority == "urgent" and ticket.labels contains "VIP" %}…{% endif %}{% if ticket.fields.seats >= 50 or company.labels contains "Enterprise" %}…{% endif %}{% if not (agent.phone or agent.title) %}…{% endif %}{% unless %} is the opposite of if:
{% unless contact.name %}Could you tell us your name?{% endunless %}Blocks can be nested inside each other, up to 24 deep.
{% case %} picks a part by value. A when can list several values.
{% case ticket.priority %} {% when "Urgent", "High" %}We're on it right away. {% when "Low" %}We'll get back to you this week. {% else %}We'll get back to you soon.{% endcase %}{% for %} repeats its content for each item of a list or a range of numbers. {% else %} shows when the list is empty.
{% for label in ticket.labels limit: 5 %} {{ forloop.index }}. {{ label }}{% unless forloop.last %}, {% endunless %}{% else %} No labels yet.{% endfor %}
{% for i in (1..3) %}{{ i }}{% endfor %}After the list, a loop takes limit: n, offset: n and reversed. Inside it, forloop.index (from 1), forloop.index0 (from 0), forloop.rindex, forloop.length, forloop.first and forloop.last tell where you are. Loops can be nested. A loop goes over at most 100 items, and one email runs at most 1,000 loop rounds in total.
Your own variables
Section titled “Your own variables”{% assign %} names a value, filters included; {% capture %} names a whole piece of the template. Both are usable after they are set.
{% assign greeting = contact.first_name | default: "there" %}{% capture signoff %}{{ agent.first_name }} from {{ team.name }}{% endcapture %}Hi {{ greeting }}, … {{ signoff }}Comments and raw text
Section titled “Comments and raw text”{% comment %}Only your team sees this; it never reaches the customer.{% endcomment %}{% raw %}{{ this is written as it is }}{% endraw %}Quotes inside HTML attributes
Section titled “Quotes inside HTML attributes”Inside an HTML attribute, write strings in single quotes so they do not end the attribute:
<a href="{{ brand.help_center_url }}" style="color: {{ brand.color | default: '#0A0A0A' }}">Help center</a>How templates are filled
Section titled “How templates are filled”- One pass. A value is never read again as a template. A customer whose name is literally
{{agent.name}}is greeted with exactly that text. - Escaping. In HTML templates, values are inserted as text: a
<in a customer’s name shows as<, it never becomes markup. In subjects, values are copied as plain text.{{ agent.signature }}and the slots insert formatted HTML, cleaned of anything unsafe. - Empty values. Every template can use every variable. A value that does not apply, such as
{{ company.name }}for a customer without a company, is empty. Usedefaultor{% if %}to cover it. - Unknown variables. When a message is filled, a variable that does not exist, or a tag that does not parse, is left exactly as written, so a code sample with braces in a reply arrives intact. When you save a template, the same mistakes are refused, with each one named, so a typo never reaches a customer.
When each template is filled
Section titled “When each template is filled”| Template | Filled when | Agent |
|---|---|---|
| A reply (reply box, API, or an Ollie draft an agent sends) | The reply is created. The stored message is what the customer got. | The sender |
| A saved reply | It is inserted in the reply box, and again when the reply is sent | The sender |
| An automation’s Send a reply | The action runs | Empty |
| Team, address and personal signatures | Each email is sent | The sender, or empty |
| An address’s auto-reply (subject and message) | It is sent | Empty |
| The satisfaction email (subject and body) | It is sent | Empty |
| The email layout | Each email is sent | The sender, or empty |
Chat replies are filled like email replies. Internal notes are never filled.
Variables
Section titled “Variables”The same list is in the app (Settings, Email design, Variables, or the Insert menu of any editor) and in the API at GET /v1/templates/variables, with your workspace’s custom fields.
Contact
Section titled “Contact”The customer the ticket is with.
| Variable | What it is | Example |
|---|---|---|
contact.name |
The customer’s full name. | Jane Cooper |
contact.first_name |
The first word of the customer’s name. | Jane |
contact.last_name |
The rest of the customer’s name. | Cooper |
contact.email |
The customer’s email address. | [email protected] |
contact.phone |
The customer’s phone number. | +1 555 0100 |
contact.labels |
The customer’s labels, a list. | VIP |
contact.fields.<key> |
A contact custom field. |
Company
Section titled “Company”| Variable | What it is | Example |
|---|---|---|
company.name |
The customer’s company. | Acme Inc |
company.domain |
The company’s web domain. | acme.com |
company.labels |
The company’s labels, a list. | Enterprise |
company.fields.<key> |
A company custom field. |
Ticket
Section titled “Ticket”| Variable | What it is | Example |
|---|---|---|
ticket.number |
The ticket number, without the #. | 1042 |
ticket.subject |
The ticket’s subject. | Can’t sign in |
ticket.status |
Open, Pending, On hold, Solved or Closed. | Open |
ticket.priority |
Low, Normal, High or Urgent. | Normal |
ticket.channel |
Email, Chat, API or Form. | |
ticket.created_date |
The day the ticket was opened, in the workspace’s time zone. | 18 October 2026 |
ticket.labels |
The ticket’s labels, a list. | Billing, Refund |
ticket.fields.<key> |
A ticket custom field. |
The author of the reply. See who the agent is.
| Variable | What it is | Example |
|---|---|---|
agent.name |
Who sends the reply. Empty for automations and auto-replies. | Alex Morgan |
agent.first_name |
The first word of the sender’s name. | Alex |
agent.last_name |
The rest of the sender’s name. | Morgan |
agent.title |
Job title, from the sender’s profile. | Support engineer |
agent.phone |
Phone, from the sender’s profile. | +1 555 0199 |
agent.pronouns |
Pronouns, from the sender’s profile. | they/them |
agent.avatar_url |
The sender’s profile picture, for an image in a signature. | https://api.opendesk.sh/v1/images/… |
agent.signature |
The sender’s own signature (HTML), placed inside a team or address signature. | Alex Morgan, Support engineer |
| Variable | What it is | Example |
|---|---|---|
team.name |
The ticket’s team, or the brand when it has none. | Billing |
team.description |
The team’s description. | Invoices, refunds and plans |
| Variable | What it is | Example |
|---|---|---|
brand.name |
The ticket’s brand. | Acme |
brand.color |
The brand colour, as #rrggbb: the brand’s, else the workspace’s, else #0A0A0A. |
#2563EB |
brand.logo_url |
The brand’s logo, for an image. Empty without one. | https://api.opendesk.sh/v1/images/… |
brand.help_center_url |
The brand’s help center, when it is on. | https://help.acme.com |
Mailbox
Section titled “Mailbox”| Variable | What it is | Example |
|---|---|---|
mailbox.name |
The sending address’s name: for mail to an alias, the alias’s sender name; else the address’s sender name, else its name. | Acme Support |
mailbox.email |
The address the email goes out from. When replies go out as an alias, the alias. | [email protected] |
Workspace and date
Section titled “Workspace and date”| Variable | What it is | Example |
|---|---|---|
workspace.name |
Your workspace’s name. | Acme |
date.today |
Today’s date in the workspace’s time zone. | 18 October 2026 |
Who the agent is
Section titled “Who the agent is”The agent variables describe the author of the message:
- On a reply a person sends, from the reply box or the API with their own sign-in, it is that person, with the details from their profile.
- On a reply Ollie sends by itself,
agent.nameis the name of the bot that answers for the address or widget, and the title, phone and other profile values are empty. - Automation replies, auto-replies, satisfaction emails and replies sent with an API key have no person, so every
agentvariable is empty.
In a preview, you are the agent.
Custom fields
Section titled “Custom fields”Every custom field of your workspace is a variable, by its key:
contact.fields.<key>for a contact field, such as{{ contact.fields.plan }}company.fields.<key>for a company field, such as{{ company.fields.account_manager }}ticket.fields.<key>for a ticket field, such as{{ ticket.fields.order_number }}
A field reads as it shows in the app:
| Type | Reads as |
|---|---|
| Text, Long text, Link, Email | The text as entered. |
| Number | The number, like 42 or 3.5. |
| Select | The chosen option’s label. |
| Multi-select | A list of the chosen options: joined with , when written, or loop over it with for. |
| Date | A date like 18 October 2026. |
| Checkbox | Yes or No. |
| Person | The member’s name. |
A field with no value is empty. Archived fields are not offered in the variable list, but templates that already use them keep filling them.
Slots are places in an email layout or the satisfaction email that opendesk fills with a whole part of the email. They can only be used in those templates.
| Slot | Used in | What goes there |
|---|---|---|
{{ content }} |
A custom layout (required) | The message itself. |
{{ signature }} |
A custom layout | The team’s, address’s or sender’s signature. |
{{ history }} |
A custom layout | The quoted earlier messages, when the layout quotes them. |
{{ survey }} |
The satisfaction email (required) | The five rating buttons, in the accent colour. |
The quote line above each quoted message has three variables of its own, and takes no others:
| Variable | What it is | Example |
|---|---|---|
message.author |
Who wrote the quoted message. | Jane Cooper |
message.email |
The address it was sent from: the customer’s own address for their messages, the address a reply went out from for your team’s. Empty when there is none. | [email protected] |
message.date |
When it was written. | Sat, 17 Oct 2026 at 14:03 |
To show who wrote each quoted message with their address, when it has one:
On {{ message.date }}, {{ message.author }}{% if message.email %} <{{ message.email }}>{% endif %} wrote:Examples
Section titled “Examples”
Examples
Section titled “Examples”A greeting that works with or without a name:
Hi {{ contact.first_name | default: "there" }},A line only for urgent tickets:
{% if ticket.priority == "urgent" %}We've flagged this as urgent and someone is on it now.{% endif %}A reference to the request, for a subject:
Your request #{{ ticket.number }}: {{ ticket.subject }}A plan from a contact custom field, with a fallback:
Your {{ contact.fields.plan | default: "current" }} plan includes priority support.An agent picture in a signature, only when there is one:
{% if agent.avatar_url %}<img src='{{ agent.avatar_url }}' alt='{{ agent.name }}' width='48' height='48' style='border-radius: 24px'>{% endif %}