Typst Documents skill for Codex

Typst templates that compile fast and warning-free. Codex structures Typst work as a single template function applied with a show rule, keeps content in clean markup, and pulls numbers from JSON or CSV instead of pasting them. It builds with pinned fonts and packages so compiles work offline, renders from Python when a service needs one PDF per customer, and treats font and reference warnings as failures.

  • Documents & Office
  • $typst-documents
  • 207 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- typst-documents

Installs to ~/.agents/skills/typst-documents/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $typst-documents, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes Typst documents and reusable templates - set and show rules, page setup, headings, figures, tables with repeating headers, bibliographies, data-driven content from JSON or CSV, and builds with the typst CLI or the Python typst package, including offline package handling and PDF/A output. Use when the user wants a Typst report, paper, letter or template, asks to migrate from LaTeX to Typst, needs PDFs generated from data with Typst, or has a Typst file that fails to compile.

Use it when

  • Writing a report, paper, CV or letter in Typst
  • Building a reusable Typst template for a team
  • Generating PDFs from data with Typst in a script or service
  • Migrating a LaTeX document to Typst

Use something else when

How to check the result

Compile with no warnings, render pages to PNG to check headers, tables and figure numbering, and confirm fonts with pdffonts.

The mistake it prevents

Ignoring the "unknown font family" warning, which lets Typst silently fall back to a default font in the delivered PDF.

~/.agents/skills/typst-documents/SKILL.md

--- name: typst-documents description: Writes Typst documents and reusable templates - set and show rules, page setup, headings, figures, tables with repeating headers, bibliographies,… license: MIT ---

Done well means: styling lives in one template function applied with #show:, content files stay readable markup, data comes from files rather than being pasted in, the build is one command that works offline, and the compile finishes with no warnings.

Before writing

  1. Check the toolchain: typst --version (CLI) or python -c "import typst" (Python binding). Syntax changes between minor versions; match the version the project pins (look in AGENTS.md, CI config or a typst.toml).
  2. Packages from @preview/... download on first compile, which needs network. In a sandbox without network, use only built-ins or vendored packages (see Offline below).
  3. Fonts: list what Typst can see with typst fonts --font-path fonts. Commit the fonts the template needs into fonts/ so the output is identical on every machine.

Layout

report/
├── main.typ          # imports the template, holds or includes content
├── template.typ      # the look: page, text, headings, figures
├── chapters/         # 01-intro.typ, 02-results.typ ...
├── data/             # metrics.json, table.csv
├── figures/          # SVG preferred
├── fonts/
└── refs.bib

Template function

// template.typ
#let report(title: none, authors: (), date: none, body) = {
  set document(title: title, author: authors)
  set page(
    paper: "a4",
    margin: (x: 2.2cm, top: 2.5cm, bottom: 2.2cm),
    header: context {
      if counter(page).get().first() > 1 [
        #set text(8pt, fill: luma(110))
        #title #h(1fr) #date
      ]
    },
    numbering: "1 / 1",
    number-align: right,
  )
  set text(font: "Source Serif 4", size: 10.5pt, lang: "en")
  set par(justify: true, leading: 0.65em)
  set heading(numbering: "1.1")
  show heading.where(level: 1): it => {
    pagebreak(weak: true)
    set text(16pt, weight: "bold")
    block(above: 1.2em, below: 0.8em, it)
  }
  show figure.where(kind: table): set figure.caption(position: top)
  show link: underline

  align(center)[
    #text(22pt, weight: "bold", title) \
    #v(0.4em)
    #authors.join(", ") #h(1em) #date
  ]
  v(1.5em)
  body
}
// main.typ
#import "template.typ": report
#show: report.with(
  title: "Q3 Platform Review",
  authors: ("Ops Analytics",),          // one-element array needs the trailing comma
  date: datetime.today().display("[day] [month repr:long] [year]"),
)

#outline(depth: 2)

= Summary
Throughput rose 12% while cost per order fell (@fig:throughput, @tab:regions).

#include "chapters/02-results.typ"

#bibliography("refs.bib", style: "ieee")

Content patterns

// chapters/02-results.typ - a leading "/" resolves from the project root
#figure(
  image("/figures/throughput.svg", width: 90%),
  caption: [Weekly throughput, Q3.],
) <fig:throughput>

#let rows = csv("/data/regions.csv", row-type: dictionary)
#figure(
  table(
    columns: (1fr, auto, auto),
    align: (left, right, right),
    stroke: none,
    table.hline(),
    table.header[*Region*][*Orders*][*Defect rate*],
    table.hline(stroke: 0.5pt),
    ..rows.map(r => (r.region, r.orders, r.defect_rate + "%")).flatten(),
    table.hline(),
  ),
  caption: [Orders by region.],
) <tab:regions>

#let m = json("/data/metrics.json")
The p95 latency was #m.latency_p95 ms, down from #m.latency_p95_prev ms.

As shown by Dean and Barroso @dean2013tail, tail latency dominates at scale.
  • table.header repeats on every page when the table breaks.
  • CSV values are strings; format numbers before display, or convert with float(x) / int(x) for arithmetic.
  • @key cites from the bibliography (.bib or Hayagriva .yml) or references a label.
  • Math: $ E = m c^2 $ with spaces inside the dollars renders as a display block.

Build

typst compile main.typ out/report.pdf --root . --font-path fonts
typst watch main.typ out/report.pdf --font-path fonts          # rebuild on save
typst compile main.typ "out/page-{p}.png" --ppi 110            # one PNG per page, to inspect
typst compile --input customer=ACME --input month=2025-09 main.typ out.pdf
typst compile --pdf-standard a-2b main.typ archive.pdf          # PDF/A for archiving
typst query main.typ "heading.where(level: 1)" --field body     # list chapter titles

Read --input values in the document with sys.inputs.at("customer", default: "Demo"); they always arrive as strings.

From Python (for example a service that renders one PDF per customer):

import json, pathlib, typst

pathlib.Path("data/metrics.json").write_text(json.dumps(metrics))
pdf_bytes = typst.compile("main.typ", root=".", font_paths=["fonts"],
                          sys_inputs={"customer": customer_name})
pathlib.Path(f"out/{customer_id}.pdf").write_bytes(pdf_bytes)

Omitting output= returns the PDF as bytes. Write each job’s data to its own directory when rendering in parallel, or pass everything through sys_inputs instead of shared files.

Offline packages

#import "@preview/cetz:0.4.2": canvas, draw    // pin exact versions, never omit them

The version above is an example; look up the current one on Typst Universe when network is available. Packages are cached after first download (~/.cache/typst/packages on Linux, ~/Library/Caches/typst/packages on macOS). For CI or sandboxes without network, vendor them and pass --package-cache-path vendor/typst-cache, or copy a package into the local namespace and import it as @local/name:version.

LaTeX to Typst quick map

LaTeXTypst
\section{X} / \subsection{X}= X / == X
\textbf{x} / \emph{x}*x* / _x_
\label{fig:a} + \ref{fig:a}<fig:a> + @fig:a
\cite{key}@key or #cite(<key>)
\newcommand{\x}[1]{...}#let x(arg) = ...
\usepackage{geometry}#set page(margin: ...)
\input{file}#include "file.typ"

Port the structure and template first, then the content; do not try to convert macros one for one.

Gotchas

  • Markup vs code: inside markup, call functions with #; inside a code block ({ }) or function arguments, no #. Content in code is written in [ ].
  • Strings and content differ: "text" is a string, [text] is content. Functions like upper accept both; concatenating them needs content ([#s]).
  • Paths resolve relative to the file that contains them (a leading / means the project root) and may not leave --root. “Access denied” errors usually mean the root is too narrow.
  • Unknown fonts are a warning, not an error: Typst falls back silently and the PDF looks wrong. Treat “unknown font family” warnings as failures.
  • set rules apply to the rest of the current scope only; put global ones in the template before body.
  • datetime.today() makes builds non-reproducible; pass the date as an input for documents that must be regenerated identically.

Verification

  • typst compile exits 0 and prints no warnings (fonts, unresolved references, missing bibliography entries all appear as warnings or errors).
  • Render pages to PNG and look at them: header hidden on page 1, tables breaking with repeated headers, figures numbered.
  • pdffonts out/report.pdf shows only the intended, embedded fonts.
  • For data-driven documents, compile with an empty dataset and a very large one.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Typst Documents"
  short_description: "Typst templates that compile fast and warning-free"
  default_prompt: "$typst-documents Create a Typst report template with our fonts, numbered headings and a cover block"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem