Email templates
CairnCMS sends transactional emails like password resets, user invitations, and any email a flow’s Send Email operation puts through the template path by rendering Liquid templates. You can override the built-in templates and add new ones by dropping .liquid files into the configured extensions folder.
This is not an extension type. There is no SDK, defineEmailTemplate, or cairncms:extension manifest involvement. Email templates are picked up by file convention from the configured extensions folder.
File location
Section titled “File location”Custom templates go in the templates subdirectory of the extensions folder:
<EXTENSIONS_PATH>/templates/<template-name>.liquidThe mail service’s Liquid engine is configured with two roots: the extensions folder first, then the platform’s built-in templates directory. When two templates share a name, the one in the extensions folder wins. That is the mechanism for overriding a built-in template.
The extension is .liquid. Filenames without that extension are not picked up.
Built-in templates
Section titled “Built-in templates”CairnCMS ships three built-in templates:
base— a shared HTML layout. Used by the other built-in templates and rendered directly for share-link emails and notification emails.password-reset— sent when a user requests a password reset. Receivesurl(the reset link) andemail.user-invitation— sent when an admin invites a new user. Receivesurl(the invitation link) andemail.
To override any of these, save a file with the matching name in the templates folder. The custom-templates root is checked before the built-in root for every template name, including base, so dropping in base.liquid, password-reset.liquid, or user-invitation.liquid replaces the corresponding built-in.
For action emails (password-reset, user-invitation), make sure the override still surfaces the url variable somewhere. Without it, the user has no way to complete the action the email is asking them to take.
Default template variables
Section titled “Default template variables”Every template render automatically receives the following variables, derived from the project settings:
projectName— the project name, defaulting to'CairnCMS'projectColor— the project accent color, defaulting to'#546e7a'projectLogo— the URL of the project logo. Always set: when a logo asset is configured under Project Settings, this is the asset URL; otherwise it falls back to<PUBLIC_URL>/admin/img/cairncms-white.png.projectUrl— the project URL, when one is set; an empty string otherwise
These are merged with whatever data the caller passes, with the caller’s data taking precedence on key conflicts.
Calling a custom template
Section titled “Calling a custom template”A flow’s Send Email operation can call any template by name. Set the Type to template, the Template to the filename without the extension, and the Data to a JSON object of variables for the template.
For example, a template at <EXTENSIONS_PATH>/templates/order-confirmation.liquid:
{% layout "base" %}{% block content %}<p>Hi {{ customerName }},</p><p> Your order <strong>{{ orderNumber }}</strong> for {{ total }} has been received. We will send a follow-up when it ships.</p><p>Thanks,<br>The {{ projectName }} team</p>{% endblock %}A flow that calls this template would set Type to template, Template to order-confirmation, and Data to { "customerName": "{{ $trigger.payload.name }}", "orderNumber": "{{ $trigger.payload.id }}", "total": "{{ $trigger.payload.total }}" }. The data chain variables resolve before the operation runs, so the template receives concrete values.
Using the base layout
Section titled “Using the base layout”The built-in base.liquid template handles the email shell, including doctype, head, branding, and footer styling, and exposes a content block. To match the styling of the built-in emails, extend it:
{% layout "base" %}{% block content %}<p>Your custom message here, with project styling already applied.</p>{% endblock %}You are not required to use base. A template that does not start with {% layout "base" %} renders on its own, which is appropriate for emails with their own complete HTML.
Liquid features
Section titled “Liquid features”Liquid offers conditionals, loops, filters, includes, and template inheritance. The mail service uses LiquidJS, which closely follows the Shopify Liquid spec. The most common features:
{{ variable }}— output a value{% if condition %}...{% endif %}— conditional blocks{% for item in collection %}...{% endfor %}— iteration{{ value | filter }}— filter pipelines (upcase,downcase,default,date,escape, and others){% layout "name" %}+{% block name %}...{% endblock %}— template inheritance{% include "partial" %}— include another template
See the LiquidJS documentation for the full reference.
A complete minimal example
Section titled “A complete minimal example”A custom welcome email that includes branding and a tailored message.
<EXTENSIONS_PATH>/templates/welcome.liquid:
{% layout "base" %}{% block content %}<p>Welcome to {{ projectName }}, {{ name }}!</p>
<p> Your account has been created with the email <strong>{{ email }}</strong>. Sign in any time at <a href="{{ projectUrl }}">{{ projectUrl }}</a>.</p>
{% if showOnboardingLink %}<p> <a href="{{ onboardingUrl }}">Open your onboarding checklist</a> to get started.</p>{% endif %}
<p>Thanks for joining,<br>The {{ projectName }} team</p>{% endblock %}A flow that sends this template would set:
- Type:
template - Template:
welcome - Data:
{ "name": "{{ $trigger.payload.first_name }}", "email": "{{ $trigger.payload.email }}", "showOnboardingLink": true, "onboardingUrl": "{{ $trigger.payload.onboarding_url }}" }
Cautions
Section titled “Cautions”- Liquid is rendered server-side, but the resulting HTML reaches an email client. Email clients are notoriously inconsistent. Test any styling decisions across the clients your audience actually uses; do not assume modern CSS will work.
- Treat untrusted data carefully. When a template renders values that originated from user input or an external service, apply Liquid’s
escapefilter ({{ value | escape }}) explicitly rather than relying on engine defaults. Reserve raw HTML output for content you fully control. - Templates run with whatever data the caller provides. They are not sandboxed against malicious input, but they are not a code-execution surface either; treat them like any other shared template.
Where to go next
Section titled “Where to go next”- Configuration covers
EXTENSIONS_PATH,EMAIL_FROM, and the broader email transport configuration. - Operations covers custom flow operations if you want a richer API for sending email than the built-in Send Email operation provides.
- Custom migrations is the other convention-based developer customization path documented here.