Template Variables
Keelstone uses the xlsx-template library to merge data into Excel files. This page explains how to structure your Excel templates and your mergeData JSON to produce the output you want.
How merging works
- Your Excel template contains placeholder cells like
{account.Name} excelTemplatesends the template (as base64) and your data toPOST /api/docs/generate- The server substitutes all placeholders with real values
- The merged workbook is returned as base64 and sent to Excel
Basic placeholders
Use curly braces to insert a single value into a cell:
| Template cell | mergeData | Output |
|---|---|---|
{account.Name} | { account: { Name: "Acme Corp" } } | Acme Corp |
{account.BillingCity} | { account: { BillingCity: "San Francisco" } } | San Francisco |
Placeholders can appear anywhere in a cell — mixed with static text:
Generated for: {account.Name} on {generatedDate}
Array iteration (table rows)
To repeat rows for a list of records, use the {#list}...{/list} syntax in adjacent rows:
| First Name | Last Name | Email | Phone |
|------------------|-----------------|--------------|--------------|
| {#contacts} | | | |
| {FirstName} | {LastName} | {Email} | {Phone} |
| {/contacts} | | | |
When merged with:
{
"contacts": [
{ "FirstName": "John", "LastName": "Doe", "Email": "john@acme.com", "Phone": "555-0101" },
{ "FirstName": "Jane", "LastName": "Smith", "Email": "jane@acme.com", "Phone": "555-0102" }
]
}
The table expands to two rows automatically, and surrounding rows shift down.
Standard mergeData shape
The accountSearch component produces this structure, which maps directly to the sample template:
{
"account": {
"Id": "001...",
"Name": "Acme Corp",
"Phone": "415-555-0100",
"Website": "https://acme.com",
"BillingCity": "San Francisco",
"BillingState": "CA",
"Owner": {
"Name": "Jane Smith"
}
},
"contacts": [
{
"Id": "003...",
"FirstName": "John",
"LastName": "Doe",
"Title": "CEO",
"Email": "john@acme.com",
"Phone": "555-0101"
}
]
}
Corresponding template placeholders:
{account.Name}
{account.Phone}
{account.BillingCity}, {account.BillingState}
{account.Owner.Name}
Custom mergeData shapes
You are not limited to the account/contacts structure. Build any JSON shape in your flow, then match the placeholders in your template.
Example: an opportunity pipeline report
// In your LWC or flow formula
const mergeData = JSON.stringify({
reportTitle: 'Q4 Pipeline Review',
generatedDate: new Date().toLocaleDateString(),
summary: {
totalAmount: 4250000,
openCount: 23,
closedWonCount: 7
},
opportunities: opportunities.map(o => ({
Name: o.Name,
Amount: o.Amount,
Stage: o.StageName,
CloseDate: o.CloseDate,
OwnerName: o.Owner?.Name
}))
});
Template placeholders:
{reportTitle}
Generated: {generatedDate}
Total Pipeline: {summary.totalAmount}
Open Deals: {summary.openCount}
{#opportunities}
{Name} | {Stage} | {Amount} | {CloseDate}
{/opportunities}
Date formatting
xlsx-template passes values as-is. Salesforce date fields return ISO strings ("2024-01-15"). If you need a formatted date, convert it before building mergeData:
const formatDate = (isoDate) =>
isoDate ? new Date(isoDate).toLocaleDateString('en-US') : '';
const mergeData = JSON.stringify({
opportunities: opps.map(o => ({
Name: o.Name,
CloseDate: formatDate(o.CloseDate) // "1/15/2024"
}))
});
Numeric formatting
Numbers are passed as raw values. Cell formatting in the Excel template controls display — set currency, decimal places, or percentage format directly in the template cell.
Uploading a new template
Templates are standard .xlsx files stored in Salesforce Files:
- Build your template in Excel with the placeholders above
- Upload it to Salesforce Files
- Move it into the Keelstone Templates content library
- Copy its
ContentDocumentIdand use it astemplateIdin your flow
To retrieve the ID:
ContentDocument doc = [
SELECT Id FROM ContentDocument
WHERE Title = 'My Template Name' LIMIT 1
];
For team-maintained templates, store the ID in a Custom Metadata record so non-developers can update it without code changes.