Setting up automated PDF generation is a daunting task – but it does not need to be. Learn how you can create a scalable generation pipeline for customized PDFs with Typst and how Typst compares against other solutions.
It is not hard to see why PDFs are everywhere: they are portable, consistent, and universally supported. When organizations need to generate thousands of personalized documents, the challenge isn't deciding to use PDFs, but creating a system to generate them efficiently at scale. Hence, setting up a PDF generation pipeline can be a daunting task: Existing solutions are often outdated, complex, or require significant amounts of custom code. But PDF generation does not have to be expensive or difficult to maintain. In this blog post, we'll review the current options for PDF generation and show how Typst offers a modern, streamlined alternative.
So far, the landscape for batch PDF generation has been dominated by these tools, each with its distinct strengths and drawbacks:
-
LaTeX: LaTeX offers beautiful typography but was designed for manually edited manuscripts, not automated generation. Ingesting data requires either generating LaTeX source code with custom, error-prone scripts, or writing Lua scripts executed by the LuaTeX compiler, both of which have a steep learning curve. LaTeX distributions are large (> 1GB), making them ill-suited for deployment in serverless environments, and compilation is slow – often taking multiple seconds per document. LaTeX also needs careful configuration to emit accessible PDFs. It is easy to run into EAA or ADA liabilities when missing this.
-
Browser-based solutions: HTML and CSS skills are widespread, making this approach attractive from a talent perspective. However, the workflow requires two steps: converting data to HTML with careful escaping, then rendering to PDF. Tools like Headless Chrome and the now-unmaintained wkhtmltopdf use full browser engines but lack paged document capabilities. Specialized tools like WeasyPrint and PrinceXML extend CSS with more paged layout features but lack modern CSS and JavaScript support (PrinceXML only supports ES5 from 2009; WeasyPrint does not support JavaScript). You will also need to maintain custom, tool-specific scripts to feed your pipeline.
-
Apache FOP: An XML-centric workflow where data must be formatted as XML (DITA, DocBook, or custom) and combined with XSL-FO stylesheets for layout. While mature, XSL-FO is complex to learn, and the standard was abandoned in 2013. Many users now switch to browser-based workflows, transforming XML to XHTML with XSLT and then applying CSS for styling – requiring two steps instead of one.
In addition, there are more specialized tools for businesses immersed in a specific ecosystem like Power BI Report Server and SAP Crystal Reports. These solutions have great integrations with their ecosystem, but are costly to phase in and focus on data and do not offer strong formatting capabilities.
Instead, let's take a look at Typst. Its properties make it ideal for batch document generation, with capabilities that scale from individual documents to millions of PDFs per day:
- Compilations in Typst commonly complete in milliseconds, making it cost-effective to run with large workloads.
- Typst allows you to load data from any format, with parsers for XML, JSON, and CSV included. With a package, you can also use Markdown.
- Documents compiled with Typst are accessible out of the box. You can choose to target PDF/UA-1 to ensure compliance with accessibility standards.
- Typst is designed for paged media and includes rich styling and layout tools.
- Typst integrates its own scripting language, allowing you to load, analyze, transform, and format your data, all in one system.
- Typst is a single lightweight (~40MB) and secure binary that is easy to deploy and run in containerized cloud environments.
- Typst is actively maintained by our team at Typst GmbH and a community of over 300 open-source volunteers.
- Typst can be integrated as a CLI tool or Rust library, giving you flexibility for your deployment.
Below, you will learn how you can use Typst for your own batch generation pipeline. This article should give you a head start and show you all the tricks you need to know to effectively generate PDFs with Typst.
In order to set up a Typst project for automated document generation, you will go through these steps:
- Define a template for your data
- Load your data with
sys.inputs - Parse your data
- Insert your data into the template and compile the file
First, let's start by defining a template. When using Typst for manually edited documents, you use markup to insert all of your content. Let's have a look at some basic markup:
Hello *Peter,*
You have accrued
#underline[100 GlorboCorp
Rewards Points] last year!

In this file, we typed all of our content manually. We used asterisks to render the customer's first name bold and underlined their loyalty account balance. The objective is clear: the point balance and the customer's name should be automatically inserted.
To do this, let's first learn about variables: Because Typst integrates a programming language, Typst documents can define and use variables. Here is a quick look:
#let first-name = [Peter]
#let points-balance = 100
Hello *#first-name,*
You have accrued
#underline[#points-balance
GlorboCorp Rewards Points]
last year!

We have used let to define a name and value for our variable. Then, we have inserted the value in markup using the hash. You can learn more about variable definitions in our tutorial and the reference.
Of course, the name and points balance is still static between compilations. This is where, for other tools, you would write your own script to populate these fields using your data. In Typst, this approach is strongly discouraged: Our markup is not designed to be generated by machines. Instead, skip external scripting and just do it in Typst itself.
Normally, when you compile your document using the open-source Typst compiler, your CLI call may look something like this:
typst compile main.typ
In this call, Typst will compile the file main.typ to a PDF. To customize the document, we can add the --input flag to pass custom data:
typst compile --input first-name=Peter --input points-balance=100 main.typ
Now, we can change our Typst file to use the inputs we passed on the shell:
#let (first-name, points-balance) = sys.inputs
Hello *#first-name,*
You have accrued
#underline[#points-balance
GlorboCorp Rewards Points]
last year!

Try to call this with different inputs and you will see that we successfully set up our first automated document in Typst! The principles for all batch generation in Typst are the same: Use the --input flag to pass inputs and then use them in your template using sys.inputs. Note that CLI inputs are provided as strings. If you need numeric values, convert or parse them (int, float, JSON parsing, etc.).
Now, we can use Typst's styling capabilities to build out our template:
#import "@preview/zero:0.5.0": num
#let (
first-name,
points-balance,
) = sys.inputs
#let num = num.with(
math: false,
group: (
separator: ",",
size: 3,
threshold: 0,
),
)
#set page(header: align(
right + bottom,
image("logo.svg"),
))
#set text(font: "IBM Plex Sans")
Hello *#first-name,*
You have accrued
#underline[#num(points-balance)
GlorboCorp Rewards Points]
last year!

In this example, we have used two set rules to insert the company logo in the header and to set the brand font. The logo image should be in the same directory as your Typst file. All fonts on the system will be automatically discovered by Typst. You can add an additional directory for fonts with the --font-path flag.
We also imported the package zero from Typst Universe. It provides powerful capabilities for number formatting. Here, we are using it to introduce a decimal separator in the point balance. You can use all of the packages and templates on Typst Universe to create your document. Many of them can give you a head start, so take a look!
Another tweak we made is to use destructuring to assign the keys from the sys.inputs dictionary to our two variables. Destructuring can help to write more concise templates.
Finally, let's explore how you can load and transform data in structured formats with Typst. So far, we have only seen single, self-contained pieces of data being passed in individual inputs. Let's instead have a look at how to load data from a single JSON file:
To start out, here's the JSON file from which we will create the document. In production, you may have retrieved this data from your API or your database.
{ "firstName": "Mike", "totalSpend": 12048.1 }
The JSON file contains a single object with two keys, firstName and totalSpend. The firstName key will just be pasted in our document like the first-name input before. However, we do not have the points balance anymore, instead, we got the total spend of the customer in USD as a number.
Suppose we can calculate the point balance from the customer’s total spend using the formula $8 spent = 1 point, rounding down. With other systems, it would now be more convenient to pre-process your data in a custom script. However, in Typst, we can do it right in the template:
#import "@preview/zero:0.5.0": num
#let customer = json(bytes(sys.inputs.customer))
#let first-name = customer.firstName
#let total-spend = customer.totalSpend
#let points-balance = calc.floor(total-spend / 8)
#let num = num.with(
math: false,
group: (
separator: ",",
size: 3,
threshold: 0,
),
)
#set page(header: align(
right + bottom,
image("logo.svg"),
))
#set text(font: "IBM Plex Sans")
Hello *#first-name,*
You have accrued
#underline[#num(points-balance)
GlorboCorp Rewards Points]
last year!

Compared to the examples before, this document does not expect multiple inputs, but just one, customer. It is expected to contain a JSON object. We use the JSON function to parse it and can then access its keys. Below, we calculate the number of points using a division and calc.floor. In this example, we have seen that we can load input data from formats like JSON and apply complex transformations with Typst's integrated scripting language.
Let's try this with this JSON file called mike.json. We will call Typst with the contents of the file provided by a subshell:
typst compile --input customer="$(cat mike.json)" main.typ
Now we got our document with Mike's data, including the point balance computed from his revenue.
With these fundamentals in place, you're ready to build production-scale PDF generation pipelines with Typst. You can process thousands of documents by iterating through your data files, calling the Typst CLI for each record, and leverage parallel processing for maximum throughput.
For production deployments, consider:
- Using Typst as a Rust library to shave off the startup overhead from launching the CLI and discovering fonts for each job. The time saved per compilation ranges between 5 milliseconds (minimal fonts) and 100 milliseconds (desktop system with many fonts).
- Deploying with our official Docker container for consistent, production-ready environments
- Implementing caching strategies for templates and assets to minimize overhead
- Disabling the use of system fonts for better reproducability and startup speed using the
--ignore-system-fontsflag and instead specifying just the required fonts using--font-path - Choosing an appropriate PDF standard like PDF/A or PDF/UA
- Monitoring compilation times and PDF sizes to catch issues early
- Setting up automated testing with sample data to catch template regressions
Ready to get started? Download the Typst open-source compiler or explore the full documentation. If you need enterprise support, contact us. We work with organizations from startups to Fortune 500 companies and can help you build a fast, maintainable PDF generation pipeline that meets your specific requirements.
