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.
typescript// 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
handlebarsYour 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
handlebarsOrder placed: Delivery window:
Output:
Order placed: November 27, 2025
Delivery window: November 30, 2025, 2:30 PM
Format 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:
handlebarsYour 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
handlebars<h2>Order Summary ( items)</h2> <ul> <li> x - (First item!) </li> </ul>
Array helpers: length, first, last, join
Math when you need it
handlebarsSubtotal: 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:
handlebars<!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:
typescriptexport 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:
typescript// 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.
typescriptapi.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:
typescriptapi.renderTemplate(template, data, { strict: true }); // Throws if any variable in the template is missing from data
Custom helpers and partials
Need functionality we didn't include? Register your own:
typescriptconst 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:
typescriptconst 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.