Personalized Mass Communications: Invitations and Letters
Another possible use-case for Quarto in academic work is producing personalized mass communications, such as invitations to a conference or workshop. Achieving this is as simple as creating a template .qmd file with placeholder yaml entries for the personalized information and other details such as the date etc. Then using a simple R or Python script to loop over a data file (e.g. a CSV or yaml file) containing the real personalized information and render the template for each recipient. The output can be in any format supported by Quarto, such as PDF, Word or HTML.
Because the template and the data both live in your repository, the whole run is version-controlled and reproducible — far more trustworthy than a one-off mail merge in a word processor, and something you would actually be happy to print.
A worked example
The repository blenback/wedding-invite is a complete, working implementation of this pattern. It happens to produce wedding invitations as print-ready PDFs, but nothing about the approach is wedding-specific — swap the template content and it becomes participant letters, certificates, recruitment letters, or personalized result summaries.
The project boils down to the three ingredients described above:
- A template — a single
.qmdwhose front matter holds the fields that change from one document to the next. - A data file — a list of recipients, kept separately from the template.
- A generator — a small R script that loops over the data, fills in the template, and renders one output per recipient.
Let’s look at each in turn.
1. The template
Everything that varies — and everything that is shared — lives in the YAML front matter of a single .qmd file. The body of the document is deliberately short, because the actual layout is handled by a custom output format (more on that below).
---
title: "Wedding Invitation"
format: invitation-typst
papersize: a5
# Fields shared by every output
couple-name-1: "Bride"
couple-name-2: "Groom"
event-date: "8th of July"
venue: "Farmhouse Wandlitz"
rsvp-url: "https://www.example.com/rsvp"
# The field that changes per recipient
recipient-names:
- "Gabriella"
- "Wouter"
---
We warmly invite you to celebrate our wedding with usThe important idea here is that the front matter is acting as a set of parameters. For a research use case those same fields might be participant-name, workshop-title, certificate-id, or result-figure — the mechanism is identical.
2. The recipient data
The list of who-gets-what is kept in its own file, recipients.yaml, separate from the template. This is the single source of truth for the run:
english:
template: "invitation-eng.qmd"
recipients:
- names: ["Gabriella", "Wouter"]
- names: ["Sarah"]
- names: ["Michael", "Emma"]
german:
template: "invitation-de.qmd"
recipients:
- names: ["Klaus", "Petra"]
- names: ["Hans"]Two things are worth noticing. First, each recipient entry can hold one or several names, so a single output can be addressed to a household, a pair of co-authors, or a whole lab. Second, this file is grouped by language, with each group pointing at its own template — which is how the project produces the same document in English and German from one command. Although, you don’t have to include this in your own project if you only need one language, it is a nice feature to have if you are working in a multilingual context.
Because the recipient list is just a data file, you can generate it from wherever your information already lives — a registration spreadsheet exported to CSV or YAML, a database query, or the output of an earlier analysis step. You never edit documents by hand; you edit the list and re-run.
3. The generator
A short wrapper script kicks off the whole run:
source("_extensions/invitation/generate.R")
generate_invitations(
config_file = "recipients.yaml",
output_dir = "_output"
)The generate_invitations() function it calls does the real work, and the logic is short enough to follow completely. For each language group it:
- reads the template and finds the
recipient-names:block in its front matter; - for every recipient, rewrites that block with the current name(s);
- writes the personalized copy to a new
.qmdin the output directory; and - renders it to PDF with
quarto::quarto_render().
# (simplified) — for each recipient, swap in their name(s) and render
for (i in seq_along(recipients)) {
recipient_names <- recipients[[i]]$names
new_content <- c(
before_section, # template up to recipient-names
generate_recipient_yaml(recipient_names), # this recipient's names
after_section # the rest of the template
)
output_path <- file.path(output_dir, filename)
writeLines(new_content, output_path)
quarto::quarto_render(
input = output_path,
output_format = "invitation-typst"
)
}The script also copies the _extensions/ and assets/ folders into the output directory before rendering, so that the custom format and any images resolve correctly, and it prints a small summary of how many documents succeeded or failed. Run Rscript generate_invitations.R and you get a directory full of finished PDFs, one per recipient.
params
Quarto has a built-in parameters mechanism (params: in the YAML, overridden with -P at render time), which is the natural choice for computational reports. This example instead rewrites the YAML front matter directly as text, because the personalized fields are consumed by the Typst format rather than by code in the document. Both approaches loop over a data file and render once per row — pick whichever fits how your template uses its inputs.
Key features worth borrowing
Even if you never send an invitation, several ideas from this project are worth lifting into your own work:
- Parameterized front matter. Treating YAML fields as the inputs to a document is the core trick that makes batch generation possible.
- A custom output format. The polished layout comes from a Quarto extension (
_extensions/invitation/) that contributes a Typst template. Typst produces print-quality PDFs quickly and without a LaTeX installation — well suited to certificates, letters, and formal documents. - Centralized branding. A
_brand.ymlfile holds the colors and fonts, so every output is visually consistent and the whole look can be re-skinned in one place. This is exactly what you want for institutional or project templates. - Built-in multilingual output. Grouping recipients by language and pointing each group at its own template is a simple, robust way to handle several languages at once — complementary to the package-based approach covered in Multi-Lingual Quarto.
- Reproducibility. Template, data, and generator all live in git. Anyone can clone the repository and regenerate the entire set on demand.
Adapting it to research workflows
To repurpose the project, you mainly change the template body and the field names. Some realistic academic uses:
- Workshop and conference logistics — personalized invitation, joining, or acceptance letters generated from the registration list.
- Certificates — attendance or completion certificates with a name, date, and unique ID per participant.
- Study correspondence — recruitment, consent, or debrief letters for each participant in a cohort, in their preferred language.
- Personalized reporting — a tailored one-page summary of results sent back to each survey respondent, field site, or partner organization.
The fastest way in is to clone blenback/wedding-invite, render a single template with quarto render invitation-eng.qmd to see the format work, then replace the template content and recipients.yaml with your own.