Skip to content

Spreadsheet templates

Two kinds of template turn your data into an Excel (XLSX) file. Pick one:

  • Spreadsheet template: you list the columns in sheets.json and get a plain table, as XLSX or as a PDF. Start here if a plain table is enough.
  • Workbook template: you design the .xlsx in Excel, with your own styles, merged cells, formulas and charts, and type Handlebars into its cells. It renders XLSX only.
  • Access to Studio.
  • An API key with render:create and template:read (plus render:read for PDFs).
  • curl or a client library, set up as in the quickstart.

In Studio, open Workspace → Editor and choose + → New entity → Template. Enter the Template name SalesReport, set Start with to Spreadsheet (XLSX and PDF tables) and click Create template. Studio creates a SalesReport folder with the template and a sample.json for previews, and opens sheets.json, which describes the table.

Each sheet has sections. A section reads an array from your data (source) and writes one row per item, with one column per value. Replace sheets.json with:

sheets.json
{
"type": "sheets",
"sheets": [
{
"name": "Sales",
"sections": [
{
"title": "Sold goods",
"source": "Data.reportRows",
"columns": [
{ "header": "Service", "value": "serviceCode", "format": "text" },
{ "header": "Sold on", "value": "soldOn", "format": "date" },
{ "header": "Amount", "value": "sum", "format": "decimal", "width": 16 }
]
}
]
}
]
}

Then open sample.json in the SalesReport folder and replace it with matching data:

sample.json
{
"Data": {
"reportRows": [
{ "serviceCode": "SRV-01", "soldOn": "2026-09-28", "sum": 480.5 },
{ "serviceCode": "SRV-02", "soldOn": "2026-09-29", "sum": 99 }
]
}
}

Save both files (Ctrl+S, or Cmd+S on macOS), go back to the SalesReport template and click Update preview. You should see a bold title row, a header row on a light blue fill, then one row per item. Download XLSX above the preview saves the file. Click Publish to make this version the one your app renders.

Terminal window
curl "$PDF_URL/v1/reports/render" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-o sales.xlsx \
-d '{
"format": "xlsx",
"template": { "name": "SalesReport" },
"data": { "Data": { "reportRows": [{ "serviceCode": "SRV-01", "soldOn": "2026-09-28", "sum": 480.5 }] } }
}'

The response is the file, saved here as sales.xlsx.

Send "format": "pdf" instead and save the response as sales.pdf. In Studio, switch Output in the preview toolbar from XLSX to PDF and click Update preview.

The PDF lays out the tables for you:

  • Each sheet starts on a new page, headed by its name. Section titles become subheadings.
  • Numbers are right-aligned and formatted for the request’s locale, without thousands separators: decimal with two decimal places, number with as many as it needs. Dates show the locale’s numeric date, in UTC.
  • Columns share the page width in proportion to their width (15 when not set).
  • The page follows the section with the most columns: A4 portrait, A4 landscape above 5 columns, A3 landscape above 10, with 10 mm margins.
  • The PDF’s title is the template’s name and its language is the locale.

page.json does not apply to a spreadsheet template. To change the page, title, PDF/A or PDF/UA, send pdf options with the request; pdf.metadata.title replaces the template-name title. The fields are in the API reference.

{
"format": "pdf",
"template": { "name": "SalesReport" },
"data": { "Data": { "reportRows": [] } },
"locale": "de-CH",
"pdf": {
"pageSize": "A4",
"orientation": "landscape",
"metadata": { "title": "Sales, September" }
}
}
Object Field Value
root type "sheets".
root sheets 1 to 10 sheets, each a worksheet in order.
sheet name The tab name. 1 to 31 characters, none of [ ] * ? : / \, unique ignoring case.
sheet sections 1 to 20 sections, written one below another with a blank row between them.
section title Optional. A bold title row above the section.
section source Data path of the section’s array, such as Data.reportRows.
section columns 1 to 50 columns.
column header The header text.
column value Data path inside each array item, such as sum or customer.name.
column format text, number, decimal or date. See cell formats.
column width Optional. 5 to 100 (Excel character units). Without it the column fits its header (12 to 60).
  • title and header are text of up to 200 characters, or a label per language picked by the request’s locale; see translations.
  • A column’s width is the widest set for it across the sheet’s sections.
  • A section with no items still writes its title and header.
  • Unknown fields are refused when you save.
  • A data path is property names joined by dots, up to 200 characters. Names start with a letter, _ or $, then letters, digits, _ or $. A number after the first name reads an array element, such as lines.0.amount. __proto__, constructor and prototype are not allowed.
  • A value that does not exist gives an empty cell. A source that is not an array fails the render with 400 BAD_REQUEST, such as Section Data.reportRows must resolve to an array.
Format Send Cell
text Any value. Numbers and booleans become text. Text. A value starting with = stays text.
number A JSON number. A number in Excel’s General format.
decimal A JSON number. A number shown with two decimal places.
date An ISO 8601 date, such as 2026-09-28 or 2026-09-28T14:30:00+02:00. A real Excel date in the short date format.
  • A missing value, null or "" gives an empty cell in every format.
  • number and decimal refuse strings such as "12.50". Send amounts as JSON numbers.
  • A date must be a real calendar date. A time without an offset is read as UTC. The cell keeps the time, but shows the date only.
  • A value that does not fit its format fails the render with 400 BAD_REQUEST naming the cell, such as Cell C3 requires a number. In a PDF the cell is named by its value and row, such as Cell sum in row 3 requires a number.

A workbook template fills a copy of an .xlsx you designed. Styles, merged cells, column widths, formulas and charts are kept.

Lay out the file as you want it and type Handlebars where data goes. To repeat a row once per item, open {{#each}} in its first cell and close it in its last:

A B
1 Service Quantity
2 {{#each Data.rows as |row|}}{{row.code}} {{row.qty}}{{/each}}
3 Total {{Data.total}} =SUM(B2:B2)

Rows below a repeated row move down, and formulas that point at it grow with it. With two items, the total moves to row 4 and its formula becomes =SUM(B2:B3).

In Workspace → Editor, choose + → New entity → Template. Enter a Template name, set Start with to Excel workbook (XLSX), drop your .xlsx into Workbook and click Create template. Studio saves the .xlsx as a file beside the template and sets the template’s template.html to {{{xlsxPrint}}}. Leave it as it is unless you add row commands.

Open the .xlsx in the explorer to see its cells, Handlebars included. Double-click a cell (or press Enter) to change it, then save. To use another workbook, pick it in the Workbook menu of the preview toolbar; it lists the .xlsx files in and below the template’s folder.

Choose + → New entity → Sample data, enter a Name and click Create entity. Replace the {} in the new file with data such as this and save it:

{
"Data": {
"rows": [
{ "code": "A", "qty": 2 },
{ "code": "C", "qty": 3 }
],
"total": 5
}
}

Go back to the template, pick the file under Data in the preview toolbar and click Update preview. You should see the filled workbook with your styles, merged cells and formulas. Save the template, then click Publish. Publishing copies the workbook into the version, so after you change the .xlsx, publish again.

Use the call from step 4 above with your workbook template’s name and data.

In a cell Result
{{customer.name}} The value. Text around it is kept, such as Total {{total}}.
{{#each rows}} in one cell, {{/each}} in a later cell of the same row The row repeats once per item.
{{#each rows}} in one row, {{/each}} in a later row The block of rows repeats once per item.
{{#each columns vertical=true}} and {{/each}} in the same column The column repeats once per item, to the right.
{{#each}} and {{/each}} in the same cell The text repeats inside that one cell.
{{xlsxCType t="n"}} anywhere in the cell The cell is a number. t="b" makes a boolean, t="s" text.
  • A cell whose whole text is one expression takes the value’s type: a number or boolean writes a numeric or boolean cell, not text.
  • Formulas, merged cells, conditional formatting and data validation follow the cells they point at. Excel recalculates formulas when it opens the file.
  • To fit a column’s width to its content, add a note to its cell in row 1 containing {{xlsxColAutofit}}. {{xlsxColAutofit all=true}} fits every column of the sheet. The note is removed from the output.
  • Handlebars in a chart title renders too, and {{xlsxChart}} there fills the chart from your data; see Excel charts.
  • Your helpers.js helpers, the built-in helpers and translations work in cells; see Handlebars templates and translations.

For changes cells cannot express, the template’s template.html can patch the workbook with commands:

Command Effect
{{#xlsxAdd "xl/worksheets/sheet1.xml" "worksheet.sheetData[0].row"}}<row ...>...</row>{{/xlsxAdd}} Adds one <row> to the sheet.
{{xlsxRemove "xl/worksheets/sheet1.xml" "worksheet.sheetData[0].row" 0}} Removes the row at that position in the sheet’s list of rows.
{{#xlsxReplace "xl/worksheets/sheet1.xml" "worksheet.mergeCells"}}<mergeCells>...</mergeCells>{{/xlsxReplace}} Replaces the sheet’s merged cells, or adds them.
{{#xlsxReplace "xl/styles.xml"}}<styleSheet>...</styleSheet>{{/xlsxReplace}} Replaces the workbook’s styles.
{{{xlsxPrint}}} Marks the template as a command template. Prints nothing.

For example, with a helpers.js helper that turns the item’s position into a row number:

helpers.js
function rowNumber(index) {
return index + 2;
}
template.html
{{#each Data.reportRows}}
{{#xlsxAdd "xl/worksheets/sheet1.xml" "worksheet.sheetData[0].row"}}
<row r="{{rowNumber @index}}">
<c r="A{{rowNumber @index}}" t="inlineStr"><is><t>{{serviceCode}}</t></is></c>
<c r="B{{rowNumber @index}}"><v>{{sum}}</v></c>
</row>
{{/xlsxAdd}}
{{/each}}
{{{xlsxPrint}}}
  • Cells render first; commands then change the result.
  • Commands can target only xl/styles.xml and the workbook’s existing xl/worksheets/sheetN.xml parts, with the targets in the table.
  • xlsxRemove counts from 0 in the rows the sheet holds (with rows added before it), not Excel row numbers.
  • Each added <row> sets its Excel row number in r (1 to 1,048,576). Rows are sorted by it, and a row with the same number as another replaces it.
  • Write values with {{ }}, not {{{ }}}, so text such as A&B stays valid XML.
  • A replacement keeps only its root element; text around it is dropped.
  • When the cells hold no Handlebars, template.html must call xlsxPrint and produce at least one command, unless it is exactly {{{xlsxPrint}}}, which returns the workbook unchanged.
  • A spreadsheet template’s content.composition.root is the sheets.json object. content.settings is still required, though a spreadsheet does not use it.
  • A workbook template’s .xlsx is a workspace file (uploading needs the asset:manage scope), named by its path in the draft’s workbook field.
  • GET /v1/reports/data-schema returns the JSON Schema of the data a template reads; for a spreadsheet template it is built from its sections and columns.

Endpoints and fields are in the API reference.

Limit Value When exceeded
Spreadsheet XLSX cells per render 500,000, counting titles and headers 400 LIMIT_EXCEEDED: Excel report exceeds the row or cell limit.
Spreadsheet XLSX rows per sheet 1,048,576 (Excel’s limit) 400 LIMIT_EXCEEDED: Excel report exceeds the row or cell limit.
Spreadsheet PDF data cells per render 50,000 400 LIMIT_EXCEEDED: PDF report is too large.
Spreadsheet PDF table size 8 MiB 400 LIMIT_EXCEEDED: PDF report is too large.
Files inside a workbook .xlsx 1,000 400 LIMIT_EXCEEDED
One file inside a workbook .xlsx 50 MiB 400 LIMIT_EXCEEDED
Workbook .xlsx uncompressed 100 MiB 400 LIMIT_EXCEEDED
Row commands per render 20,000 400 UNSUPPORTED_FEATURE

Workbook templates also fail with:

  • 400 BAD_REQUEST when the .xlsx is not a valid workbook, or a command is malformed (XLSX template produced an invalid command.).
  • 400 UNSUPPORTED_FEATURE when template.html has no xlsxPrint or produced no commands while the cells hold no Handlebars, or a command targets an unsupported part, has an invalid row number or removal index, or does not hold exactly one <row> or <mergeCells>.

Other limits are in limits.