Automette docs
Templates

Typst fields and data

This page covers how to declare fields in Typst templates, annotate their types, and how data flows in from each trigger.

Declaring fields in Typst

Fields are declared as a #let dictionary at the top of your Typst source. The values you provide are used as defaults and shown as pre-fills on the Generate form.

#let data = (
  recipient: "Alice Johnson",
  issue_date: "15 Jan 2025",
  amount: "5000",
)

Hello, #data.recipient — your invoice for #data.amount is due on #data.issue_date.

Annotating field types

By default, Automette infers the field type from the field name (e.g. _date suffix → date, _qty → number). You can override this with inline // @type: comments:

#let data = (
  recipient: "Alice Johnson",          // @type: text
  issue_date: "15 Jan 2025",           // @type: date @format: DD MMM YYYY
  amount: "5000",                      // @type: number
  notes: "Payment due on receipt",     // @type: textarea
  paid: "false",                       // @type: boolean
  logo: "logo.png",                    // @type: image
  brand_color: "#1a56db",              // @type: color
  portal_url: "https://example.com",   // @type: url
  label: "Issue Date",                 // @label: Custom Label
)

Supported types

AnnotationInput shownInjected value
textSingle-line text inputString as-is
textareaMultiline textString as-is
numberNumber inputString (use float() in template)
date @format: ...Date pickerDate formatted per @format
booleanCheckbox"true" or "false"
imageImage upload + URLS3 key or URL string
colorColor picker + hex inputHex string e.g. "#1a56db"
urlText inputURL string
arrayRepeating row editorNative Typst tuple — iterate directly
jsonMonospace JSON textareaJSON string — call json.decode() in template

Date fields and formatting

Date fields require a @format to be useful. Without it, @type: date is treated as plain text — no date picker is shown and no conversion happens.

// ✅ Date picker shown; value injected as "15 Jan 2025"
issue_date: "15 Jan 2025",   // @type: date @format: DD MMM YYYY

// ✅ Different format
expiry: "Jan 2025",          // @type: date @format: MMM YYYY

// ⚠️ No @format — treated as plain text input
start_date: "2025-01-15",    // @type: date  ← no picker, no conversion

Format tokens

TokenOutput
DDDay with leading zero (01–31)
DDay without leading zero (1–31)
MMMShort month name (Jan, Feb, …)
MMMMFull month name (January, …)
MMMonth number with zero (01–12)
MMonth number without zero (1–12)
YYYY4-digit year (2025)
YY2-digit year (25)

Color fields

Color values are injected as hex strings. Use rgb() in your Typst template to convert them:

#let data = (
  brand_color: "#1a56db",  // @type: color
)

#rect(fill: rgb(data.brand_color), width: 100%)
#text(fill: rgb(data.brand_color))[Colored text]

Image fields

Image fields accept a file upload or a URL. The value is injected as a filename (for uploaded assets) or a URL string:

#let data = (
  logo: "logo.png",  // @type: image
)

#image(data.logo, width: 80pt)

Upload the image in the Assets tab before generating.

Array fields (repeating rows)

Use @type: array for fields that hold a list of items — line items on an invoice, subjects in a report card, features in a product sheet. The value is a native Typst tuple, so you iterate it directly without any decoding.

#let data = (
  items: (  // @type: array @columns: desc:text,qty:number,amount:number
    (desc: "Widget A", qty: "2", amount: "1200"),
    (desc: "Service fee", qty: "1", amount: "500"),
  ),
)

The @columns: key:type,... annotation lists sub-field names and their input types. Supported sub-types: text, number, date, boolean, image, color, url. Date sub-fields accept a format: delivery_date:date:DD MMM YYYY.

Iterate with a for loop:

#for item in data.items [
  #item.desc#item.qty × #item.amount
]

Or spread into a table:

#table(
  columns: (1fr, auto, auto),
  table.header([Description], [Qty], [Amount]),
  ..data.items.map(item => (
    [#item.desc], [#item.qty], [#item.amount],
  )).flatten()
)

Always include at least one representative row as the default value — it makes the live preview look correct.

@type: json (legacy)

Some older templates store repeating data as a JSON-encoded string and call json.decode() to parse it:

#let data = (
  items: "[{\"desc\":\"Widget\",\"qty\":2}]",  // @type: json
)
#let rows = json.decode(data.items)

This still works and is supported, but @type: array is cleaner for new templates — no escaping, sub-field types are shown correctly in forms.

How data flows in

SourceHow values are provided
Generate pageForm fields filled manually
CSV bulkOne row per render — column headers map to field keys
Public FormsShareable form with the same field inputs
APIfields object in the POST body
Signed URLsQuery parameters in the URL

Scalar values are always injected as strings — use int() or float() in the template to convert them. Array fields (@type: array) are injected as native Typst tuples and can be iterated directly.

Last updated on

On this page