Parameterized Quarto Reports
Question
How can one .qmd file produce several customized reports without making the document awkward to develop interactively?
The key is to separate three things:
- The template defines and uses parameters.
- The render command can override their default values.
- An interactive helper loads the defaults when code cells are run outside a render.
Quarto and Knitr use the name params (plural). During a render, Knitr creates this list before executing the document. Running an individual R cell does not perform that render setup, so params may not exist in the interactive R session.
A minimal template
Define sensible defaults in the document’s YAML:
---
title: "Regional Sales Report"
params:
region: "Midwest"
start_date: "2026-01-01"
include_detail: false
---
```r
#| echo: false
region <- params$region
start_date <- as.Date(params$start_date)
include_detail <- params$include_detail
```Defaults are useful even when production renders always override them: the document remains runnable, and the defaults document the expected parameter names and types.
Render with the defaults
quarto render report.qmdThe editor’s Preview or Render button also uses these defaults unless the editor has been configured to call Quarto with overrides.
Override values at the command line
quarto render report.qmd \
-P region:West \
-P start_date:2026-07-01 \
-P include_detail:true-P is short for --execute-param. Repeating it is convenient for a few values.
Keep a named set of values in YAML
For repeatable report variants, put only the overrides in a separate file:
# west.yml
region: "West"
start_date: "2026-07-01"
include_detail: trueThen render with:
quarto render report.qmd --execute-params west.ymlThis is easier to review and reuse than a long command. Do not store passwords, tokens, or other secrets in a parameter file committed to Git.
Interactive development
A small utility can read the template’s default parameters when params does not already exist. The guard matters: during a real render, it preserves the values supplied by Quarto instead of replacing them with defaults.
read_quarto_params <- function(input) {
yaml_params <- rmarkdown::yaml_front_matter(input)$params
if (is.null(yaml_params)) {
stop("No `params` section found in ", input, call. = FALSE)
}
lapply(yaml_params, function(parameter) {
if (is.list(parameter) && "value" %in% names(parameter)) {
parameter$value
} else {
parameter
}
})
}Near the beginning of report.qmd, use the helper only as an interactive fallback:
if (!exists("params", inherits = FALSE)) {
source("R/read-quarto-params.R")
params <- read_quarto_params("report.qmd")
}This helper loads YAML defaults, not command-line overrides. That is a feature: interactive work gets a predictable example configuration, while a render remains the authoritative test of a customized report. To explore a different case interactively, change selected values explicitly:
params$region <- "West"Validate early
Parameters are inputs to the analysis. Check them once in a setup cell so a bad render fails with a useful message rather than producing a plausible but wrong report.
required_params <- c("region", "start_date", "include_detail")
missing_params <- setdiff(required_params, names(params))
if (length(missing_params) > 0) {
stop("Missing parameters: ", paste(missing_params, collapse = ", "))
}
allowed_regions <- c("Midwest", "Northeast", "South", "West")
if (!params$region %in% allowed_regions) {
stop("Unknown region: ", params$region)
}
start_date <- as.Date(params$start_date)
if (is.na(start_date)) {
stop("`start_date` must use YYYY-MM-DD format")
}
if (!is.logical(params$include_detail) || length(params$include_detail) != 1) {
stop("`include_detail` must be one TRUE or FALSE value")
}Keeping dates in YAML as quoted ISO strings and converting them in R makes the type contract visible and avoids relying on a YAML parser’s implicit date conversion.
Produce several reports from R
The quarto R package can pass a named list through execute_params. Give each render a distinct output filename so one variant does not overwrite another.
regions <- c("Midwest", "West")
for (region in regions) {
quarto::quarto_render(
input = "report.qmd",
output_file = paste0("sales-", tolower(region), ".html"),
execute_params = list(
region = region,
start_date = "2026-07-01",
include_detail = FALSE
)
)
}Parameterized rendering is usually enough for a modest reporting workflow. If the number of reports, dependencies, or scheduling rules grows substantially, a pipeline tool such as targets may become a better orchestration layer.
What I learned
- A parameterized document is a function-like template: YAML supplies defaults, and each render supplies inputs.
paramsexists during Knitr rendering, not automatically during interactive cell execution.- A guarded helper makes interactive work convenient without changing render behavior.
- Parameter values should control analysis choices, not contain secrets or replace input validation.
- A customized report is only verified when it is rendered with the intended overrides; successfully running cells with defaults is not the same test.
- Parameters control computation. Quarto metadata such as
format,toc, andoutput-filecontrols document rendering, even though both are written in YAML.