# Template DSL

> For AI agents: a documentation index is available at https://makespdf.com/llms.txt — every docs page also has a markdown twin (append .md to the page URL), including this one: https://makespdf.com/docs/dsl.md

The builder DSL is a compact, JavaScript-like language for describing PDF
templates. This guide walks through the concepts so you can read and hand-write
DSL yourself. For the exhaustive function-by-function reference, see the
[template author skill file](https://makespdf.com/skills/pdf-template-author.md).

## What the DSL is

A template is a layout with `{{variable}}` placeholders. You write it once, then
render it any number of times with different data. Instead of hand-writing the
verbose JSON document tree, you call small builder functions — `doc()`, `page()`,
`col()`, `table()` — that compose into the same tree. The DSL output is roughly
75–95% smaller than the equivalent JSON, which is why it is also the format AI
agents generate.

A DSL "script" is plain JavaScript evaluated in a sandbox. It must define two
top-level constants:

| Constant | Purpose |
| --- | --- |
| `template` | The document, built with `doc(...)`. Required. |
| `sampleData` | Example data used to fill the placeholders when no `data` is supplied to the render call. Recommended. |

```js
const template = doc(
  { size: "A4" },
  page(col(s("Hello {{name}}")))
);

const sampleData = { name: "World" };
```

## Core building blocks

Everything is built from a handful of primitives. Layout flows top-to-bottom by
default; rows lay their children out left-to-right.

| Function | What it does |
| --- | --- |
| `doc(opts, ...sections)` | Creates the document. Options: size, title, padding, currency, styles. |
| `page(...kids)` | A page. doc() auto-wraps loose content in one, so it is often optional. |
| `col(width?, ...kids)` | A vertical stack. First arg can be a width ("50%"), a class (".body"), or a style object. |
| `r(...kids)` | A row — children flow horizontally. |
| `s(text)` | A span — a run of text. Accepts a class or style object: `s(".label", "Total")`. |
| `text(...spans)` | An inline text block — children flow on one wrapped line. Use for mixed styles. |
| `hdr(...) / ftr(...)` | Header / footer, repeated at the top / bottom of every page. |
| `img(src, w, h)` or `img({ src, alt?, width?, height? })` | An image. The positional form needs a URL and explicit width and height in points; the object form carries alt text and allows the dimensions to be omitted when the intrinsic size is unknown. |

Units are **points** (1pt = 1/72 inch). An A4 page is 595 × 842pt.

## Variables, loops, and conditionals

Placeholders are written `{{ ... }}` and resolved against your render data.

| Syntax | Meaning |
| --- | --- |
| `{{fieldName}}` | Simple value from your data. |
| `{{customer.name}}` | Nested value via dot notation. |
| `{{qty * price}}` | An expression — arithmetic, comparisons, and ternaries are supported. |
| `{{total \| currency}}` | A filter. `currency` and `number` format values; `currency:AUD` overrides the code. |
| `each("item in items", ...)` | Loop — repeats its children once per array element. |
| `when("discount > 0", ...)` | Conditional. Pair with `elseWhen(...)` / `otherwise(...)`. Negate with `!`. |

Inside a loop you also get `{{@index}}` (0-based), `{{@first}}`, and
`{{@last}}`. ISO date strings in date-like fields are auto-formatted — send
`"2026-04-10"` and it renders as `10 April 2026`.

```js
text(each("a in authors",
  s("{{a.name}}"),
  when("!@last", s(", "))
))
// → "Marie Curie, Alan Turing, Ada Lovelace"
```

## Helpers: atoms, molecules, organisms

On top of the primitives, the DSL ships a library of higher-level helpers so you
rarely assemble common patterns by hand. They come in three tiers:

**Atoms** — inline text shortcuts: `bold()`, `italic()`, `underline()`,
`mono()`, `colored()`, `link()`, `muted()`, plus `hr()` and `gap()` for
spacing.

**Molecules** — small composite pieces: `lv()` (label-value row), `slv()`
(stacked label-value), `addr()` (address block), `th()` / `td()` (table cells),
`bullet()`.

**Organisms** — full sections: `minHdr()` (header), `lvGrid()` (label-value
grid), `addrs()` (two-column from/to), `table()` (looped data table),
`totals()` (totals section), `terms()` (notes block), `ftrPages()` (footer with
page numbers).

> Every text-bearing parameter of a molecule or organism accepts a plain string,
> a single inline element (`bold("Paid")`), or an array mixing both — so you
> never need to drop to raw `s()` just to add emphasis.

## Styling elements

Most builders accept an inline style object as their first argument, e.g.
`col({ "font-size": 12, width: "50%" }, ...)`. You can also define reusable
classes in `doc({ styles: { ... } })` and reference them by name (`".label"`).

> **Style property names are kebab-case, not camelCase.** Even though the DSL is
> JavaScript-like, properties follow CSS conventions and must be quoted:
> `{ "font-size": 12 }`, not `{ fontSize: 12 }`. camelCase keys are silently
> ignored and the element renders with the default.

Common properties: `font-family`, `font-size`, `font-weight`, `color`,
`background-color`, `align`, `width`, `margin`, `padding`, `border`,
`border-color`, `border-radius`. Spacing properties take either a single number
or a `[top, right, bottom, left]` array. The full property table — plus absolute
positioning, watermarks, and the standard style kit — lives in the
[skill file](https://makespdf.com/skills/pdf-template-author.md).

To change something document-wide (e.g. the default font), edit the `"."`
selector — the cascade root every element inherits from — rather than
enumerating each class:

```js
doc(
  { size: "A4", styles: { ".": { "font-family": "NotoSans", "font-size": 10 } } },
  page(col(s("Inherits NotoSans 10pt")))
);
```

## A complete example

This is a full invoice template. `cols` defines the table column widths once and
is shared by `table()` and `totals()` so the totals rows line up with the table
columns.

```js
const cols = [
  ["Description", "1fr"],
  ["Qty", "auto", "center"],
  ["Price", "auto", "right"],
  ["Amount", "auto", "right"],
];

const template = doc(
  { size: "A4", title: "Invoice {{invoiceNumber}}" },
  minHdr("Invoice", "{{company.name}}"),
  lvGrid([
    ["Invoice #:", "{{invoiceNumber}}"],
    ["Date:", "{{date}}"],
    ["Due:", "{{dueDate}}"],
  ]),
  gap(8),
  addrs(
    { label: "From", lines: ["{{company.name}}", "{{company.address}}"] },
    { label: "Bill to", lines: ["{{customer.name}}", "{{customer.address}}"] }
  ),
  gap(8),
  table(cols, "item in items", [
    ["{{item.description}}", "1fr"],
    ["{{item.qty}}", "auto", "center"],
    ["{{item.price | currency}}", "auto", "right"],
    ["{{item.amount | currency}}", "auto", "right"],
  ]),
  totals(
    [
      ["Subtotal", "{{subtotal | currency}}"],
      ["Tax (10%)", "{{tax | currency}}"],
      ["Total Due", "{{total | currency}}", true],
    ],
    cols
  ),
  ftrPages("{{company.name}}")
);

const sampleData = {
  invoiceNumber: "INV-2026-001",
  date: "2026-03-30",
  dueDate: "2026-04-29",
  company: { name: "Acme Corp", address: "123 Main St" },
  customer: { name: "Jane Smith", address: "456 Oak Ave" },
  items: [
    { description: "Consulting", qty: 40, price: 150, amount: 6000 },
    { description: "Travel expenses", qty: 1, price: 450, amount: 450 },
  ],
  subtotal: 6450,
  tax: 645,
  total: 7095,
};
```

## Rendering your DSL

Send the script as the `dsl` field to `POST /api/v1/preview` while you author —
it is free, deterministic, and about a second. When the value of `data` is
omitted, the script's `sampleData` is used.

```bash
curl -X POST https://makespdf.com/api/v1/preview \
  -H "Authorization: Bearer mpdf_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"dsl": "const template = doc(page(col(s(\"Hi {{name}}\"))));\nconst sampleData = { name: \"World\" };", "data": {"name": "Ada"}}' \
  -o output.pdf
```

Once a template is finalized, save it with `POST /api/v1/templates` and render it
in production with `POST /api/v1/render` using `{ templateId, data }`.
`/preview` is the free authoring endpoint; `/render` is the billed production
endpoint. See the [API Reference](https://makespdf.com/docs/api) for both.

## Going deeper

This page covers the concepts. The
[template author skill file](https://makespdf.com/skills/pdf-template-author.md) is the
canonical, exhaustive reference — every function, the full style-property table,
fillable form fields, barcodes and QR codes, vector drawing, watermarks, and
per-document recipes. It is the same material formatted for AI coding
assistants; you can also hand it to Claude, Cursor, or Codex as context (see
[AI & Agent Setup](https://makespdf.com/docs/ai-setup)).

- [pdf-template-author.md](https://makespdf.com/skills/pdf-template-author.md) — full DSL
  reference
- [API Reference](https://makespdf.com/docs/api) — `/preview`, `/render`, `/templates`
  endpoints
- [AI & Agent Setup](https://makespdf.com/docs/ai-setup) — generate DSL templates with an AI
  assistant
