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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- typst-documents Installs to ~/.agents/skills/typst-documents/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install typst-documents The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project typst-documents Installs to .agents/skills/typst-documents/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/typst-documents.zipunzip -o typst-documents.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) typst-documents PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- A journal requires its LaTeX class: use latex-documents
- You need to pick between PDF engines for an app first: use pdf-generation
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.
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
- Check the toolchain:
typst --version(CLI) orpython -c "import typst"(Python binding). Syntax changes between minor versions; match the version the project pins (look inAGENTS.md, CI config or atypst.toml). - 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). - Fonts: list what Typst can see with
typst fonts --font-path fonts. Commit the fonts the template needs intofonts/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.headerrepeats on every page when the table breaks.- CSV values are strings; format numbers before display, or convert with
float(x)/int(x)for arithmetic. @keycites from the bibliography (.bibor 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
| LaTeX | Typst |
|---|---|
\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 likeupperaccept 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.
setrules apply to the rest of the current scope only; put global ones in the template beforebody.datetime.today()makes builds non-reproducible; pass the date as an input for documents that must be regenerated identically.
Verification
typst compileexits 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.pdfshows 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.yaml
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.