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

# Document Templates

> Turn flow data into PDFs: HTML templates written in Liquid, and fillable PDF forms

A **Document Template** is a document layout that a [flow](https://docs.twine.se/flow-engine/concepts/flows-and-runs.md)
fills with data and turns into a PDF: a payslip, an employment certificate, an onboarding form.
The flow produces the file, and an upload step after it delivers the file wherever it should go.

There are two kinds:

| Kind | You upload | Filled by the flow action |
| - | - | - |
| **HTML** | an `.html` file using [Liquid](https://shopify.github.io/liquid/) placeholders | **Render HTML to PDF** |
| **AcroForm** | a `.pdf` with fillable form fields | **Fill PDF form** |

Templates are managed under **Organization settings → Document templates**.

## Uploading a template

Drop an `.html` or `.pdf` file (at most 10 MB) on the upload area. The template is named after
the file, without its extension, and the flow action picks it by that name. Names are unique
within an organization: to replace a template, delete it and upload the new version.

Each template is checked when it is uploaded, so a broken one is refused there and then instead
of failing a flow run later:

* An **HTML** template must be valid Liquid. A syntax error is reported with its line number.
* An **AcroForm** must be a standard PDF form with at least one fillable field. XFA forms and PDFs
  without fields are refused.

## What a template needs

Click a template in the list to see what a flow has to give it.

For an **AcroForm**, that is the list of its form fields. The flow passes a map with one entry
per field name.

For an **HTML** template, the dialog has three parts:

* **Data to supply.** The keys the map passed to the template must have, and what each holds:
  a single value, a map with certain fields, or a list of rows with certain fields.
* **Placeholders.** Every value the template reads, written as a path into that map. `[]` marks a
  list: `rows[].Amount` is the `Amount` of each row. When the template reads a value under
  another name, that name is shown next to it.
* **Set by the template.** Names the template creates itself with `{% assign %}` or
  `{% capture %}`. The flow does not supply these.

Take a payslip template that starts like this:

```liquid theme={null}
{%- assign summary = rows[0] -%}
{%- assign currency = employee.salary_currency -%}
<h1>Payslip for {{ summary.Employee_Name }}</h1>
{% for row in rows %}
  <tr><td>{{ row.Description }}</td><td>{{ row.Amount | money }} {{ currency }}</td></tr>
{% endfor %}
```

Its placeholders are `rows[].Employee_Name` (read as `summary.Employee_Name`),
`rows[].Description` and `rows[].Amount` (read as `row.Description` and `row.Amount`), and
`employee.salary_currency` (read as `currency`). `summary` and `currency` are set by the
template. The flow supplies two keys: `employee` and `rows`.

> [!NOTE]
>
> The placeholder list is read from the template's text, so it is a strong hint rather than a
> guarantee. A value read in an unusual way, for example through a name built at run time, may
> not appear in it.

## Rendering a template in a flow

Add a [Flow Action](https://docs.twine.se/flow-engine/flow/flow_action.md) and pick the **Render HTML to PDF** step:

* **Template**: the HTML template to render.
* **Data assign**: the assign holding the map the template reads. Build it with
  [Build Map](https://docs.twine.se/flow-engine/transformation/flow_map_build.md), one row per key from **Data to
  supply**.
* **File name**, or **File name assign** to name each file from the data
  (`payslip-1042.pdf`).
* **Fail on missing values**: stop when the template reads a value the data does not have, instead
  of printing it blank. The error names each missing value and its line in the template.
* **Output assign**: where the PDF is written, ready for an upload step.

For the payslip above, the Build Map has two rows: `employee`, taken from the assign holding the
employee, and `rows`, taken from the assign holding the payslip lines (for example the rows read
from a CSV file).

**Fill PDF form** works the same way, with a **Fields assign** holding one entry per form field
instead of a data assign. Its **Flatten** option, on by default, bakes the values into the page so
the result can no longer be edited.

## Employees and dated properties

An employee from Twine's [System Data](https://docs.twine.se/platform/data-model/employee.md) can be passed to the
template as it is. The template reads its properties by their Twine names, and each one prints
**the value valid today**:

```liquid theme={null}
{{ employee.first_name }} {{ employee.last_name }}, {{ employee.bank_iban }}
```

Properties have a history, and a moment after the property name reads another point in it.
These are the same moments as `@` in [expressions](https://docs.twine.se/platform/expressions.md#dated-properties):

| Template | Reads |
| - | - |
| `employee.bank_iban` | the value valid today |
| `employee.bank_iban.today` | the same, spelled out |
| `employee.bank_iban.latest` | the newest value, even one dated in the future |
| `employee.bank_iban.latest_defined` | the newest value that is not blank |
| `employee.bank_iban.oldest` | the first value |
| `employee.bank_iban.all` | every value, newest first, to loop over |
| `employee.bank_iban["2026-01-01"]` | the value valid on that date |
| `employee.bank_iban[summary.Payment_Date]` | the value valid on a date the data holds |

A payslip printed after a change of bank account can show the account valid on the payment date
rather than today's:

```liquid theme={null}
{%- assign summary = rows[0] -%}
Paid to {{ employee.bank_iban[summary.Payment_Date] }}
```

Some properties are kept per employment or per salary, such as `salary_currency`. These are read
across all of the employee's employments, so `employee.salary_currency` is the currency valid
today whichever employment it belongs to.

A property the employee has no value for prints blank, or stops the flow when **Fail on missing
values** is on. Use Twine's property names (`bank_iban`), not the source system's: the
[employee data model](https://docs.twine.se/platform/data-model/employee.md#properties) lists them.

## Writing HTML templates

Templates use [Liquid](https://shopify.github.io/liquid/): `{{ ... }}` prints a value and
`{% ... %}` holds logic such as `if` and `for`. Liquid's standard filters are available, plus one
of Twine's own:

* `money` formats a number Swedish style: `{{ row.Amount | money }}` prints `5838.42` as
  `5 838,42`. Pass the number of decimals to change them: `{{ row.Amount | money: 0 }}`.

Some things to know:

* **Make the file self-contained.** For security, rendering cannot fetch anything from the
  internet. Put styles in a `<style>` block and images in the file as `data:` URIs. An external
  stylesheet, font or image is left out of the PDF, and the flow run log names each one.
* **Read the first row with `rows[0]`.** `rows.first` and `rows.last` are not supported on lists.
* **An empty text value counts as true.** `{% if row.Unit %}` is true for an empty cell. Reading a
  CSV file turns blank cells into nothing by default, which counts as false.
* **Keep Liquid out of HTML comments.** Liquid reads `{{` and `{%` inside `<!-- -->` too, so a
  comment containing them is a syntax error.
* **Use print CSS for page layout.** `@page { size: A4; margin: 20mm; }` sets the paper size and
  margins, and `page-break-before: always` starts a new page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.