> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eigenpal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# JSON to XLSX

> Convert JSON rows into an XLSX spreadsheet file for download or downstream delivery.

`transform.json-to-xlsx` writes an XLSX workbook from ordered columns and an array of row objects.

Use it when a workflow already has tabular JSON — extract output, a script result, or rows from `transform.xlsx-to-json` — and needs a spreadsheet file. To fill an existing spreadsheet template, use [Fill Template](/steps/transform/template) instead.

## When to use it

| Goal                                                | Step                                                      |
| --------------------------------------------------- | --------------------------------------------------------- |
| Read an uploaded workbook into JSON                 | [`transform.xlsx-to-json`](/steps/transform/xlsx-to-json) |
| Write JSON rows to a new workbook                   | `transform.json-to-xlsx`                                  |
| Fill placeholders in an existing DOCX/XLSX template | [`transform.template`](/steps/transform/template)         |

The step always produces a run output file (`.xlsx`) plus metadata: `fileId`, `filename`, `sheetCount`, and per-sheet row counts.

## Configuration

Configuration goes inside the step's `with:` block.

<ParamField path="filename" type="string">
  Output filename, supports LiquidJS; .xlsx is added if omitted
</ParamField>

<ParamField path="columns" type="array<object>">
  Ordered columns for a single-sheet workbook. Use sheets for multiple sheets.

  <Expandable title="columns properties">
    <ParamField path="key" type="string" required>
      Row object key to read for this column
    </ParamField>

    <ParamField path="header" type="string">
      Header cell text. Defaults to key.
    </ParamField>

    <ParamField path="type" type="&#x22;string&#x22; | &#x22;number&#x22; | &#x22;boolean&#x22; | &#x22;date&#x22;">
      Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="rows" type="string | array<record<string, unknown>>">
  Array of row objects, or a template expression that resolves to one
</ParamField>

<ParamField path="sheets" type="array<object>">
  Multiple sheets. Do not combine with top-level columns/rows.

  <Expandable title="sheets properties">
    <ParamField path="name" type="string">
      Sheet name. Defaults to Sheet1, Sheet2, …. Excel limit: 31 characters. Cannot contain : \ / ? \* \[ ], cannot start or end with a single quote, and History is reserved.
    </ParamField>

    <ParamField path="columns" type="array<object>" required>
      Ordered columns for this sheet

      <Expandable title="columns properties">
        <ParamField path="key" type="string" required>
          Row object key to read for this column
        </ParamField>

        <ParamField path="header" type="string">
          Header cell text. Defaults to key.
        </ParamField>

        <ParamField path="type" type="&#x22;string&#x22; | &#x22;number&#x22; | &#x22;boolean&#x22; | &#x22;date&#x22;">
          Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField path="rows" type="string | array<record<string, unknown>>" required>
      Array of row objects, or a template expression that resolves to one
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="limits" type="object">
  Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults.

  <Expandable title="limits properties">
    <ParamField path="maxSheets" type="integer">
      Maximum worksheets. Omitted uses the server default (50). Cannot exceed it.
    </ParamField>

    <ParamField path="maxRowsPerSheet" type="integer">
      Maximum data rows per sheet. Omitted uses the server default (100,000). Cannot exceed it.
    </ParamField>

    <ParamField path="maxTotalRows" type="integer">
      Maximum data rows summed across all sheets. Omitted uses the server default (250,000). Cannot exceed it.
    </ParamField>

    <ParamField path="maxColumns" type="integer">
      Maximum columns per sheet. Omitted uses the server default (256). Cannot exceed it.
    </ParamField>

    <ParamField path="maxTotalCells" type="integer">
      Maximum populated cells across the workbook. Omitted uses the server default (5,000,000). Cannot exceed it.
    </ParamField>

    <ParamField path="maxOutputBytes" type="integer">
      Maximum serialized .xlsx size in bytes. Omitted uses the server default (52,428,800, 50 MiB). Cannot exceed it.
    </ParamField>
  </Expandable>
</ParamField>

## Output

<ResponseField path="fileId" type="string" required>
  File ID from the files table
</ResponseField>

<ResponseField path="filename" type="string" required>
  Sanitized output filename including .xlsx
</ResponseField>

<ResponseField path="sheetCount" type="integer" required>
  Number of sheets in the workbook
</ResponseField>

<ResponseField path="sheets" type="array<object>" required>
  Per-sheet name and row count

  <Expandable title="sheets properties">
    <ResponseField path="name" type="string" required>
      Written sheet name
    </ResponseField>

    <ResponseField path="rowCount" type="integer" required>
      Number of data rows written (excludes header)
    </ResponseField>
  </Expandable>
</ResponseField>

## Single sheet

Declare `columns` in display order. `rows` is usually a template expression that resolves to an array of objects. Extra keys on a row are ignored. Missing keys become blank cells.

```yaml theme={null}
- name: export-xlsx
  type: transform.json-to-xlsx
  with:
    filename: "invoices-{{ steps.extract.output.vendor }}"
    columns:
      - key: vendor
        header: Vendor
      - key: total
        header: Total
        type: number
      - key: paid
        header: Paid
        type: boolean
    rows: "{{ steps.extract.output.lineItems }}"
```

## Multiple sheets

Use `sheets` instead of top-level `columns` / `rows`. Do not mix the two forms.

```yaml theme={null}
- name: export-workbook
  type: transform.json-to-xlsx
  with:
    filename: report.xlsx
    sheets:
      - name: Line items
        columns:
          - key: sku
          - key: qty
            type: number
        rows: "{{ steps.extract.output.lineItems }}"
      - name: Totals
        columns:
          - key: label
          - key: amount
            type: number
        rows: "{{ steps.summarize.output.rows }}"
```

## Cell types

* Numbers and booleans are written as spreadsheet numbers and booleans.
* `null` and missing fields are blank.
* YYYY-MM-DD strings stay **text** unless the column sets `type: date`. Date columns require a valid civil date (overflow values like `2026-02-31` are rejected) and are written as timezone-independent Excel date serials formatted `yyyy-mm-dd`.
* Strings that look like formulas (`=`, `+`, `-`, `@` prefixes) are written as text. This step does not author Excel formulas.
* Nested objects and arrays are rejected. Serialize them in a prior `transform.script` if you need them as cell text.

Sheet names follow Excel rules: 1–31 characters, no `: \ / ? * [ ]`, unique within the workbook, cannot start or end with a single quote, and `History` is reserved.

## Workload limits

To keep worker memory and output size bounded, `transform.json-to-xlsx` enforces hard caps before building cell data and again after serializing the workbook. Override any cap with an optional `limits` object in the step config.

| Limit             | Default             | What it counts                                      |
| ----------------- | ------------------- | --------------------------------------------------- |
| `maxSheets`       | 50                  | Worksheets in the workbook                          |
| `maxRowsPerSheet` | 100,000             | Data rows on one sheet (header excluded)            |
| `maxTotalRows`    | 250,000             | Data rows summed across all sheets                  |
| `maxColumns`      | 256                 | Columns on one sheet                                |
| `maxTotalCells`   | 5,000,000           | Populated cells across the workbook (header + data) |
| `maxOutputBytes`  | 52,428,800 (50 MiB) | Serialized `.xlsx` file size                        |

When a limit is exceeded, the step fails with an error that names the limit and the configured maximum. Raise only the caps you need for large exports.

```yaml theme={null}
- name: export-large-report
  type: transform.json-to-xlsx
  with:
    filename: report.xlsx
    limits:
      maxTotalRows: 500000
      maxOutputBytes: 104857600
    columns:
      - key: id
      - key: amount
        type: number
    rows: "{{ steps.extract.output.rows }}"
```
