Automette docs
Templates

Typst template reference

How data flows in

When a document is generated, field values are passed as a dictionary called dict. Reference any field with dict.fieldname.

// Field values passed at render time
Hello, #dict.name

// In expressions
#let total = float(dict.amount) * 1.2

Field values are always strings. Use int(), float(), or json.decode() to convert them.

Field type annotations

Add // @type: comments to control the input shown on the Generate form and in public Forms. Without an annotation, field types are inferred from the field name.

#let data = (
  name: "Alice Johnson",          // @type: text
  dob: "15 Jan 1990",             // @type: date @format: DD MMM YYYY
  amount: "5000",                 // @type: number
  notes: "Top performer",         // @type: textarea
  active: "true",                 // @type: boolean
  logo: "logo.png",               // @type: image
  brand_color: "#1a56db",         // @type: color
  website: "https://example.com", // @type: url
  label: "Ship Date",             // @label: Custom Label
)

Date fields require @format. Writing @type: date without @format keeps the field as plain text — the format tells the system how to convert the date picker value before injecting it.

Date format tokens

DD01–31 (with zero)
D1–31 (no zero)
MMMJan, Feb, …
MMMMJanuary, February, …
MM01–12
M1–12
YYYY2025
YY25

Using a color field

#let data = (
  brand_color: "#1a56db",  // @type: color
)
// Use rgb() to convert the hex value
#rect(fill: rgb(data.brand_color), width: 100%)

Array fields (line items, lists)

Pass arrays as a JSON string. Decode with json.decode() then loop with for.

// dict.items = '[{"name":"Widget","qty":2,"price":9.99}]'
#let items = json.decode(dict.items)

#for item in items [
  #item.name — #item.qty × #item.price
]

Page setup

// A4 portrait
#set page(paper: "a4", margin: (top: 2cm, bottom: 2cm, left: 2.5cm, right: 2.5cm))

// A4 landscape
#set page(paper: "a4", flipped: true, margin: 1.5cm)

// US Letter
#set page(paper: "us-letter", margin: 1in)

// Custom size
#set page(width: 10cm, height: 10cm, margin: 8mm)

Text and fonts

// Document-wide default
#set text(font: "Inter", size: 11pt, fill: rgb("#1a1a1a"))

// Inline formatting
*bold*   _italic_   #underline[underlined]

// Apply font to a single span
#text(font: "Playfair Display", size: 18pt, weight: "600")[Certificate of Completion]

// Apply font to multiple lines — use a scoped block
#[
  #set text(font: "Playfair Display", size: 14pt)
  This line uses Playfair Display.
  So does this one.
  And this one.
]
// Back to the document default here

// Style headings globally
#show heading.where(level: 1): it => text(font: "Playfair Display", size: 22pt, weight: "bold")[#it.body]

Available fonts

Use any of these in font: "Name". Custom fonts uploaded to your Brand Kit are also available.

InterRobotoOpen SansMontserratNunito SansDM SansRalewayWork SansPlus Jakarta SansOutfitNunitoLatoPoppinsOswaldJosefin SansBebas NeuePlayfair DisplayEB GaramondCrimson ProSource Serif 4MerriweatherLibre BaskervilleSource Sans 3Noto SansPT SansAbril FatfaceGreat Vibes

Colors

rgb("#e63946")          // hex
rgb(230, 57, 70)        // r, g, b  (0–255)
luma(80%)               // grayscale
color.mix(red, blue)    // mix

// Usage
#set text(fill: rgb("#333333"))
#rect(fill: rgb("#f0f0f0"), stroke: rgb("#cccccc"))

Images (assets)

Images must be uploaded in the Assets tab before they can be used.

// Full width
#image("logo.png", width: 100%)

// Fixed size
#image("logo.png", width: 60pt, height: 60pt, fit: "contain")

// Fit modes: "contain" | "cover" | "stretch"

// Conditionally show
#if dict.show_logo == "true" {
  image("logo.png", width: 80pt)
}

Layout

// Two columns
#columns(2, gutter: 1cm)[
  Left column content
  #colbreak()
  Right column content
]

// Box / container
#block(
  width: 100%,
  fill: rgb("#f8f8f8"),
  radius: 4pt,
  inset: (x: 12pt, y: 10pt),
)[
  Boxed content here
]

// Horizontal stack
#grid(
  columns: (1fr, auto),
  align: horizon,
)[
  Left content
][
  Right content
]

Tables

#table(
  columns: (2fr, 1fr, 1fr),
  stroke: 0.5pt + rgb("#dddddd"),
  fill: (_, row) => if row == 0 { rgb("#f0f0f0") } else { white },
  inset: (x: 8pt, y: 6pt),

  // Header row
  [*Description*], [*Qty*], [*Price*],

  // Data rows from array field
  ..{
    let items = json.decode(dict.items)
    items.map(item => (
      [#item.description],
      [#item.qty],
      [$#item.price],
    )).flatten()
  }
)

Conditionals

// Show/hide a section
#if dict.show_badge == "true" [
  #text(fill: green)[Verified]
]

// if / else
#if float(dict.balance) < 0 [
  #text(fill: red)[Overdue]
] else [
  #text(fill: green)[Paid]
]

Common patterns

Centered certificate heading

#set align(center)
#v(3cm)
#text(font: "Playfair Display", size: 28pt, weight: "bold")[
  Certificate of Achievement
]
#v(0.5cm)
#text(size: 14pt)[Awarded to]
#v(0.3cm)
#text(font: "Playfair Display", size: 22pt)[#dict.recipient_name]

Invoice total row

#let items = json.decode(dict.line_items)
#let total = items.fold(0.0, (acc, i) => acc + float(i.price) * float(i.qty))

Total: *$#str(calc.round(total, digits: 2))*

Page header with logo + company name

#grid(
  columns: (auto, 1fr),
  gutter: 12pt,
  align: horizon,
)[
  #image("logo.png", height: 36pt)
][
  #text(font: "Inter", size: 10pt, fill: rgb("#666"))[#dict.company_name]
]
#line(length: 100%, stroke: 0.5pt + rgb("#dddddd"))
#v(0.5cm)

Last updated on