Spreadsheet templates
Two kinds of template turn your data into an Excel (XLSX) file. Pick one:
- Spreadsheet template: you list the columns in
sheets.jsonand get a plain table, as XLSX or as a PDF. Start here if a plain table is enough. - Workbook template: you design the
.xlsxin Excel, with your own styles, merged cells, formulas and charts, and type Handlebars into its cells. It renders XLSX only.
Before you start
Section titled “Before you start”- Access to Studio.
- An API key with
render:createandtemplate:read(plusrender:readfor PDFs). - curl or a client library, set up as in the quickstart.
Spreadsheet templates
Section titled “Spreadsheet templates”1. Create the template
Section titled “1. Create the template”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.
2. Describe the columns
Section titled “2. Describe the columns”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:
{ "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:
{ "Data": { "reportRows": [ { "serviceCode": "SRV-01", "soldOn": "2026-09-28", "sum": 480.5 }, { "serviceCode": "SRV-02", "soldOn": "2026-09-29", "sum": 99 } ] }}3. Preview and publish
Section titled “3. Preview and publish”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.
4. Render it from your app
Section titled “4. Render it from your app”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 }] } } }'using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
byte[] xlsx = await client.RenderReportAsync(new RenderReportRequest{ Format = ReportFormat.Xlsx, Template = TemplateRef.ByName("SalesReport"), Data = new { Data = new { reportRows = new[] { new { serviceCode = "SRV-01", soldOn = "2026-09-28", sum = 480.5 }, }, }, },});
await File.WriteAllBytesAsync("sales.xlsx", xlsx);import { writeFile } from "node:fs/promises";import { TypefoldClient, ReportFormat, TemplateRef } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const xlsx = await client.renderReport({ format: ReportFormat.Xlsx, template: TemplateRef.byName("SalesReport"), data: { Data: { reportRows: [{ serviceCode: "SRV-01", soldOn: "2026-09-28", sum: 480.5 }] }, },});
await writeFile("sales.xlsx", xlsx);import { writeFile } from "node:fs/promises";import { TypefoldClient, ReportFormat, TemplateRef } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const xlsx = await client.renderReport({ format: ReportFormat.Xlsx, template: TemplateRef.byName("SalesReport"), data: { Data: { reportRows: [{ serviceCode: "SRV-01", soldOn: "2026-09-28", sum: 480.5 }] }, },});
await writeFile("sales.xlsx", xlsx);import osfrom pathlib import Path
from typefold import ( TypefoldClient, RenderReportRequest, ReportFormat, TemplateRef,)
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
xlsx = client.render_report( RenderReportRequest( template=TemplateRef.by_name("SalesReport"), data={ "Data": { "reportRows": [ {"serviceCode": "SRV-01", "soldOn": "2026-09-28", "sum": 480.5}, ], }, }, format=ReportFormat.XLSX, ))
Path("sales.xlsx").write_bytes(xlsx)The response is the file, saved here as sales.xlsx.
5. Get the same table as a PDF
Section titled “5. Get the same table as a PDF”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:decimalwith two decimal places,numberwith 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" } }}sheets.json fields
Section titled “sheets.json fields”| 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). |
titleandheaderare text of up to 200 characters, or a label per language picked by the request’slocale; 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 aslines.0.amount.__proto__,constructorandprototypeare not allowed. - A
valuethat does not exist gives an empty cell. Asourcethat is not an array fails the render with400 BAD_REQUEST, such asSection Data.reportRows must resolve to an array.
Cell formats
Section titled “Cell formats”| 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,
nullor""gives an empty cell in every format. numberanddecimalrefuse 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_REQUESTnaming the cell, such asCell C3 requires a number.In a PDF the cell is named by itsvalueand row, such asCell sum in row 3 requires a number.
Workbook templates
Section titled “Workbook templates”A workbook template fills a copy of an .xlsx you designed. Styles, merged cells, column widths,
formulas and charts are kept.
1. Design the workbook in Excel
Section titled “1. Design the workbook in Excel”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).
2. Upload it as a template
Section titled “2. Upload it as a template”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.
3. Preview with sample data, then publish
Section titled “3. Preview with sample data, then publish”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.
4. Render it
Section titled “4. Render it”Use the call from step 4 above with your workbook template’s name and data.
Handlebars in cells
Section titled “Handlebars in cells”| 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.jshelpers, the built-in helpers and translations work in cells; see Handlebars templates and translations.
Row commands
Section titled “Row commands”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:
function rowNumber(index) { return index + 2;}{{#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.xmland the workbook’s existingxl/worksheets/sheetN.xmlparts, with the targets in the table. xlsxRemovecounts 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 inr(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 asA&Bstays valid XML. - A replacement keeps only its root element; text around it is dropped.
- When the cells hold no Handlebars,
template.htmlmust callxlsxPrintand produce at least one command, unless it is exactly{{{xlsxPrint}}}, which returns the workbook unchanged.
Create templates through the API
Section titled “Create templates through the API”- A spreadsheet template’s
content.composition.rootis thesheets.jsonobject.content.settingsis still required, though a spreadsheet does not use it. - A workbook template’s
.xlsxis a workspace file (uploading needs theasset:managescope), named by its path in the draft’sworkbookfield. GET /v1/reports/data-schemareturns 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.
Limits and errors
Section titled “Limits and errors”| 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_REQUESTwhen the.xlsxis not a valid workbook, or a command is malformed (XLSX template produced an invalid command.).400 UNSUPPORTED_FEATUREwhentemplate.htmlhas noxlsxPrintor 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.
Next steps
Section titled “Next steps”- Render a report by name: more on the Reports API.
- Excel charts: fill workbook charts from your data.
- Translations: labels per language and number and date formatting.