Templates
Store reusable Handlebars templates. Render them with variables. AI-generate them from a prompt with optional voice-and-tone matching.
Why templates?
- One source of truth — your team edits one template; every send picks up the change.
- Smaller request bodies — your send call carries variables, not 50 KB of HTML.
- Compiled once — Mailgrid parses the template once on save and caches the AST in memory; renders are O(variables).
- AI generation — describe what you want; get back a polished template you can persist.
Handlebars syntax supported
Mailgrid uses a hand-written safe interpreter (Cloudflare Workers blocks new Function). The supported subset:
Variable interpolation
Hello {{name}}!
Your order total is ${{order.total}}.
Triple-stash for raw HTML: {{{rawHtml}}}.Double-stash {{x}} HTML-escapes. Triple-stash {{{x}}} emits raw.
Conditionals
{{#if isPremium}}
<p>Premium support available 24/7.</p>
{{else}}
<p>Upgrade for 24/7 support.</p>
{{/if}}
{{#unless verified}}
<p>Please verify your email.</p>
{{/unless}}Loops
<ul>
{{#each items}}
<li>{{this.name}} — ${{this.price}} (#{{@index}})</li>
{{/each}}
</ul>Inside {{#each}}, the special variables are this (current item), @index (0-based), @first, @last.
Object scoping
{{#with user.address}}
<p>{{street}}, {{city}}, {{country}}</p>
{{/with}}Built-in helpers
| Helper | Effect |
|---|---|
{{uppercase x}} | Returns x.toUpperCase() |
{{lowercase x}} | Returns x.toLowerCase() |
{{capitalize x}} | First letter of each word capitalized |
{{date x}} | Format ISO date as YYYY-MM-DD |
{{currency x}} | Format number as USD (override locale on send) |
{{default x "fallback"}} | Use x if truthy, else fallback |
{{eq a b}} | Equality (for use inside #if) |
Not supported (intentionally)
{{#registerHelper}}— no runtime helper registration (security).- Partials (
{{> partial}}) — usetemplateIdcomposition instead. - Inline JavaScript — no
new Function, noeval.
Create a template
Scopes: templates:write
| Field | Type | Required | Description |
|---|---|---|---|
name | string (64 chars, [A-Za-z0-9_-]) | Yes | Unique per tenant. Used as alternative to UUID for templateId. |
subject | string (998) | Yes | Handlebars supported. |
html | string (≤10 MB) | Yes | Handlebars supported. |
text | string (≤10 MB) | No | Plain-text body; auto-sent as multipart/alternative. |
description | string (500) | No | Internal label for your team. |
Example
curl -X POST https://api.mailgrid.space/api/templates \ -H "Authorization: Bearer $KEY" \ -d '{ "name": "welcome", "subject": "Welcome to {{company}}, {{name}}!", "html": "<h1>Hi {{name}}</h1><p>Glad you joined {{company}}.</p>{{#if isPro}}<p>Pro tools unlocked.</p>{{/if}}", "text": "Hi {{name}}. Glad you joined {{company}}." }'
Get a template
Where :id is either the UUID or the unique name.
List templates
Render pipeline
When you send via a template, here's what happens internally:
- D1 lookup by
tenant_id + id|name. - Template AST parsed (cached if already in this Worker's memory).
- Variables interpolated into
subject,html,text. - If
personalize: true, AI rewrites with contact metadata + tone. - Tracking pixel + click rewrites injected (if enabled).
- Unsubscribe headers stamped.
- Sent to SES.
AI-generate a template
Scopes: ai:use
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string (8-2000) | Yes | What you want the email to do. |
audience | string | No | Who's reading it (e.g. "developers, technical"). |
tone | enum | No | formal · friendly · urgent · celebratory |
voiceExamples | string[] (1-3) | No | Paste sample emails for style-matched generation. |
Voice & Tone Match
Provide 1-3 example emails (max 3000 chars each) and the AI rewrites your prompt in matching style:
{
"prompt": "Notify customer their invoice is overdue.",
"tone": "friendly",
"voiceExamples": [
"Hey there! Just a heads up that we shipped 3.2 this morning...",
"Quick note: we'll be doing maintenance from 2-3am UTC..."
]
}The AI picks up: contractions, lowercase greetings, "Quick note", "Just a heads up". Without voice examples, you get a generic professional tone.
AI provider chain
- Workers AI (default) — Llama 3.1 8B Instruct. Free at the edge. ~800-1500 ms per generation.
- Anthropic Claude Haiku (fallback) — engaged automatically if
ANTHROPIC_API_KEYis set. ~400-900 ms, higher quality. - If both fail, the endpoint returns
UPSTREAM_FAILED.
Auto-injected context
Mailgrid automatically pulls in your knowledge base entries as additional context for generation. Add entries via POST /api/knowledge like:
- "Our company is called Acme. We sell developer tools."
- "Our customer success email is success@acme.com."
- "Our brand voice is direct, friendly, never uses 'utilize'."
All entries flow into the system prompt automatically.
Response
{
"success": true,
"data": {
"subject": "Quick note about invoice {{invoiceId}}",
"html": "<p>Hey {{name}}, just a heads up...",
"text": "Hey {{name}}, just a heads up...",
"variables": ["name", "invoiceId", "amount", "dueDate"]
}
}The variables array tells you which Handlebars placeholders the AI used — useful for building UI that asks the user to fill them in.
Persisting a generated template
Generation doesn't auto-save. Call POST /api/templates with the returned fields if you want to reuse it.
Using a template on send
{
"from": "hello@yourdomain.com",
"to": "user@example.com",
"templateId": "welcome",
"variables": { "name": "Anna", "company": "Acme", "isPro": true }
}Subject from the template can be overridden by passing subject on the send. Bodies cannot be overridden when templateId is used — either use a template OR provide html/text inline.
Errors specific to templates
| Code | Cause |
|---|---|
NOT_FOUND | Template id/name doesn't exist for this tenant. |
BAD_REQUEST | Template syntax error (e.g. unclosed {{#if}}). |
CONFLICT | Template name already exists. |
UPSTREAM_FAILED | AI generation provider failed. |
Variables passed into {{double}} stash are auto HTML-escaped. Be careful with {{{triple}}} stash — only use when the variable is trusted (e.g. another tenant-owned template render, not user input).