When you're building an application, you inevitably need to generate dynamic content. Order confirmations. Password reset emails. Invoice PDFs. Webhook payloads. The list goes on.
Most developers end up doing one of two things:
- String concatenation - Messy, error-prone, and impossible to maintain
- Spin up a templating service - Now you have another service to deploy, monitor, and pay for
Neither option feels good. So we added a third: Handlebars templating, built directly into Centrali compute functions.
What we shipped
The new api.renderTemplate() method lets you render Handlebars templates with your data in a single function call. No external services, no string concatenation, no setup.
// In your compute function
const html = api.renderTemplate(
'Hello {{customer.name}}, your order #{{order.id}} ships on {{formatDate order.shipDate "date"}}.',
{
customer: { name: 'Sarah Chen' },
order: { id: 'ORD-2024-1847', shipDate: '2025-12-01' }
}
);
// Output: "Hello Sarah Chen, your order #ORD-2024-1847 ships on December 1, 2025."That's it. One line of code, and your dynamic content is ready.
Why Handlebars?
We considered several templating engines before settling on Handlebars:
Mustache is simple but too limited—no helpers, no conditionals beyond truthy/falsy checks.
EJS and Pug are powerful but require escaping user content carefully to avoid XSS. One mistake and you've introduced a security vulnerability.
Handlebars hits the sweet spot:
- Logic-less by design (keeps templates clean)
- Built-in HTML escaping (secure by default)
- Extensible through helpers (powerful when you need it)
- Battle-tested in production at scale
Built-in helpers that actually matter
We didn't just add Handlebars—we added 30+ helpers that solve real problems. Here are the ones you'll use most:
Currency formatting
Your total: Output: Your total: ₦125,000
We support 20+ currency symbols out of the box: USD, EUR, GBP, NGN, JPY, CNY, INR, KES, GHS, ZAR, and more. Unknown currencies fall back gracefully to the currency code.
Date formatting
Order placed:
Delivery window: Output:
Order placed: November 27, 2025
Delivery window: November 30, 2025, 2:30 PMFormat options: short, long, date, time, datetime, iso
Conditionals that work
Standard Handlebars gives you #if blocks, but comparing values requires custom helpers. We included them:
Your order is on its way!
We're preparing your order.
Your order is confirmed.
You qualify for free shipping!
Available comparisons: eq, ne, gt, gte, lt, lte, and, or, not
Working with arrays
<h2>Order Summary ( items)</h2>
<ul>
<li>
x -
(First item!)
</li>
</ul>Array helpers: length, first, last, join
Math when you need it
Subtotal:
Tax (%):
Total: Math helpers: add, subtract, multiply, divide, modulo, round
Real example: Order confirmation email
Here's what a complete order confirmation template looks like:
<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Thanks for your order, !</h1>
<p>We've received order <strong>#</strong> and will start preparing it shortly.</p>
<h2>Order Summary</h2>
<table style="width: 100%; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #eee;">
<th style="text-align: left; padding: 8px 0;">Item</th>
<th style="text-align: right; padding: 8px 0;">Price</th>
</tr>
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px 0;">
()
<span style="color: #666;"> × </span>
</td>
<td style="text-align: right; padding: 12px 0;">
</td>
</tr>
</table>
<div style="margin-top: 16px; text-align: right;">
<p>Discount: <strong>-</strong></p>
<p>Shipping: </p>
<p style="color: #16a34a;">Free Shipping!</p>
<p style="font-size: 1.25em;">
<strong>Total: </strong>
</p>
</div>
<div style="margin-top: 24px; padding: 16px; background: #f9fafb; border-radius: 8px;">
<h3 style="margin: 0 0 8px 0;">Tracking Information</h3>
<p style="margin: 0;">
: <strong></strong>
</p>
</div>
<p style="margin-top: 24px; color: #666; font-size: 0.875em;">
Order placed on
</p>
</body>
</html>Call it from your compute function:
export default async function sendOrderConfirmation({ event, api }) {
const order = event.record.data;
// Fetch related data
const customer = await api.fetchRecord(order.customerId);
const items = await api.queryRecords('order_items', {
filter: { orderId: order.id }
});
// Render the email
const html = api.renderTemplate(ORDER_CONFIRMATION_TEMPLATE, {
customer: customer.data,
order: order,
items: items.items.map(i => i.data),
tracking: order.trackingNumber ? {
carrier: order.carrier,
number: order.trackingNumber
} : null
});
// Send it (see the Resend section below!)
await api.httpPost('https://api.resend.com/emails', {
from: 'orders@yourstore.com',
to: customer.data.email,
subject: `Order #${order.orderNumber} confirmed`,
html: html
}, {
headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` }
});
return { sent: true };
}Sending emails: Try Resend
Now that you can generate beautiful HTML emails, you need a way to send them. We recommend Resend.
Why Resend?
- Free tier: 3,000 emails/month, 100 emails/day—plenty for testing and small projects
- Developer-first API: Send an email with one HTTP request
- Great deliverability: Built by the team behind React Email
- Generous pricing: $20/month gets you 50,000 emails
Here's how simple it is to integrate with Centrali:
// Add api.resend.com to your workspace's allowed domains in Settings
const response = await api.httpPost(
'https://api.resend.com/emails',
{
from: 'Your App <hello@yourapp.com>',
to: customer.email,
subject: 'Your order is confirmed',
html: renderedHtml
},
{
headers: {
Authorization: 'Bearer re_your_api_key_here',
'Content-Type': 'application/json'
}
}
);That's it. Combine api.renderTemplate() with Resend's API, and you have production-ready transactional emails in under 50 lines of code.
Sign up at resend.com to get your API key.
Security by default
Templates are secure out of the box:
HTML escaping: All variables are escaped by default. {{userInput}} becomes safe HTML.
api.renderTemplate(
'Hello {{name}}',
{ name: '<script>alert("xss")</script>' }
);
// Output: "Hello <script>alert("xss")</script>"Triple braces for trusted content: Use {{{html}}} only when you explicitly trust the content.
Strict mode: Enable strict mode to catch missing variables during development:
api.renderTemplate(template, data, { strict: true });
// Throws if any variable in the template is missing from dataCustom helpers and partials
Need functionality we didn't include? Register your own:
const html = api.renderTemplate(
'Price: {{formatPrice amount}}',
{ amount: 4999 },
{
helpers: {
formatPrice: (cents) => `$${(cents / 100).toFixed(2)}`
}
}
);
// Output: "Price: $49.99"Partials let you reuse template fragments:
const html = api.renderTemplate(
'{{> header}} Main content {{> footer}}',
{ companyName: 'Acme Inc' },
{
partials: {
header: '<header>{{companyName}}</header>',
footer: '<footer>© 2025 {{companyName}}</footer>'
}
}
);What you can build
This isn't just for emails. Use api.renderTemplate() for:
- Invoices and receipts - Generate HTML, convert to PDF with a service like PDFShift
- Webhook payloads - Dynamically construct JSON or XML payloads for third-party integrations
- Slack/Discord messages - Build rich notifications with conditional sections
- SMS templates - Generate personalized text messages
- Export templates - Create custom CSV or report formats
Get started
The api.renderTemplate() method is available now in all Centrali compute functions. No configuration needed—just start using it.
Check the compute functions documentation for the complete helper reference and more examples.
Building something cool with templating? We'd love to hear about it—drop us a line at feedback@centrali.io.