Template DSL
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.
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. |
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) | An image. Needs a URL and explicit width and height in points. |
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.
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).
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").
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.
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:
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.
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.
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.pdfOnce 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 for both.
Going deeper
This page covers the concepts. The template author skill file 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).
- pdf-template-author.md — full DSL reference
- API Reference — /preview, /render, /templates endpoints
- AI & Agent Setup — generate DSL templates with an AI assistant