Reports with Quarto and R Markdown
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. A report is a document whose numbers, tables and figures are computed by the code that sits inside it, so that rendering the document again with new data gives a new report and nothing is copied and pasted. R has had this idea since Sweave, and it has two current implementations, which share the same engine: Quarto, a command-line program (version 1.10.19 here, with the Pandoc 3.10.0 that it carries inside it), and R Markdown, the R package rmarkdown 2.32. Both run the R code with knitr 1.52, so the chunks, the chunk options and most of the documents are the same. This page builds one real report, the shelf monthly report, as a Quarto document and as an R Markdown document, and renders it to HTML, Word, GitHub Markdown, revealjs slides and PDF. Along the way it covers chunks and their options, inline code, parameters, the engines for other languages, caching, Quarto projects with _quarto.yml, the state of R Markdown, extracting the code from a document, and Sweave, the older form that ships with R. The add-on packages that ran are knitr 1.52, rmarkdown 2.32, DBI 1.3.0 and RSQLite 3.53.3 (one sql chunk) and reticulate 1.47.0 with the system Python 3.12.3 and pandas 3.0.6 (one python chunk); rmarkdown found Pandoc 3.1.3 on the system, while Quarto used its own. The image has no TeX installation, so the one thing that needs it, a PDF made through LaTeX, is shown failing with the real message, and the PDF that was rendered for real went through Typst, which Quarto carries. The quarto R package, bookdown, blogdown, xaringan, flexdashboard and distill are not installed, and tinytex 0.61 is installed but was not run: where a sentence mentions them it says so. The default linters of lintr 3.4.0 were used only to check the scripts.
Every R example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters; the documents themselves (.qmd, .Rmd, .Rnw, _quarto.yml) are not linted, they are shown byte for byte as they were rendered, and the shell lines are the ones typed in the project directory. The running scenario is the library shelf described on the section index, seen as a report: the documents live in a project directory, ~/report, next to books.csv and loans.csv, and read them with relative paths, because the working directory of a chunk is the directory of its document. The scenario’s fixed constants, shelf_today (2026-06-30) and loan_days (21), are typed at the top of the documents, so that no result depends on the day the page is rendered; a report that asked the system for today’s date would give a different report tomorrow. Terminal transcripts have the colour codes (NO_COLOR was set), trailing blanks and the usual blank line at the start of a quarto render removed, and the order of an ls listing is the locale’s. Timings are not shown, because they depend on the machine.
This page is one of seven in the performance and interoperability group. The {python} chunk is the subject of Python Interop with reticulate, the interactive documents that run a Shiny server are in Shiny, plumber and Web APIs, vignettes are a package feature that Documentation with Rd, roxygen2 and Vignettes covers, plots are the topics of Base Graphics and Devices and ggplot2 and the Grammar of Graphics, reading the CSV files is in Reading and Writing Text Data, and running R without a person at the console, which is how a report is rendered on a schedule, is in Scripts and the Command Line.
Literate programming
The idea is to write one document for people and let the machine extract and run the program that is inside it. The document has the explanation, the code and, after it is processed, the results, so the explanation cannot drift away from the numbers it describes. Two operations are named after the original tools: to weave is to run the code and produce the readable document, and to tangle is to extract the code and produce the program. In R, Sweave was the first implementation and it is still part of R; knitr, which grew out of the same idea with more output formats and more options, is the engine of both current systems, and its function names remember the words: knit() weaves, purl() tangles, after the knitting term "knit one, purl one" in the help page of knit().
A document in either system is a Markdown file with three kinds of content. The YAML header, between two lines of three hyphens at the top of the file, holds metadata and options: the title, the output format, the parameters, the execution options. Markdown is the prose. Code chunks, fenced with three backticks and the language in braces, are code that is run when the document is rendered, and inline code is a short expression inside a sentence. Rendering has two steps, shown in the table with the programs that do each of them.
| Piece | What it does | Where it comes from |
|---|---|---|
knitr |
Runs the code in the chunks and inline expressions, captures the text, messages, warnings, errors and plots, and writes a plain Markdown file in which the results are inserted after the code that made them (weaving). It also tangles ( |
R package, 1.52 here |
Pandoc |
Converts the Markdown file to HTML, Word, slides, PDF through LaTeX or Typst, and many other formats |
a separate program: rmarkdown uses the one it finds (3.1.3 here), Quarto uses the one it ships (3.10.0) |
rmarkdown |
|
R package, 2.32 here |
Quarto |
The |
a separate program, 1.10.19 here; it is not an R package |
|
A wrapper that calls the |
not installed here |
Sweave |
The first weaving system in R, for LaTeX documents with the extension |
part of R |
Source: Quarto documentation: Using R
The consequence of this arrangement is that the R side of a document does not change when the program around it changes. A Quarto document with R chunks is executed by knitr, exactly like an R Markdown document; the Quarto documentation says so, and says that Quarto can render most existing .Rmd files without modification. What differs is the packaging: how options are written, the names of the output formats, and what is available on top (projects, cross-references, a larger set of formats). The next sections use the shelf report to show each piece in the order in which a reader meets it.
The shelf monthly report
The report answers two questions about the loans: how many loans were made in each month since a given month, and which members have loans that were overdue. A loan is overdue when it was out for more than loan_days days; a loan that has not been returned counts up to shelf_today. The project directory has the two data files and the document:
~/report
├── books.csv
├── loans.csv
└── report.qmd
The document, report.qmd, is the following. Its header gives the title, asks for the html format, declares one parameter, since, with the default 2026-01, and sets echo: false for the whole document with the execute key, so that the code is not shown unless a chunk asks for it. The first chunk, with include: false, reads the data and computes the loans of interest: it runs, and nothing of it is shown. The paragraph after it contains four pieces of inline code. The table and the figure chunks have labels that start with tbl- and fig-, which is how Quarto makes them cross-referenceable, so the text can say @tbl-months and @fig-months. The last chunk shows its code (echo: true) because the way the overdue loans are computed is part of what a reader of the report wants to check.
---
title: "Shelf monthly report"
format: html
params:
since: "2026-01"
execute:
echo: false
---
```{r}
#| label: load
#| include: false
books <- read.csv("books.csv")
loans <- read.csv("loans.csv", colClasses = "character")
loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
shelf_today <- as.Date("2026-06-30")
loan_days <- 21L
loans$month <- format(loans$loan_date, "%Y-%m")
recent <- loans[!is.na(loans$month) & loans$month >= params$since, ]
```
This report covers the loans made from `{r} params$since` to `{r} format(shelf_today)`:
`{r} nrow(recent)` loans of the `{r} nrow(books)` books in the catalogue.
@tbl-months and @fig-months show how they are spread over the months.
## Loans per month
```{r}
#| label: tbl-months
#| tbl-cap: "Loans per month."
per_month <- as.data.frame(table(recent$month))
knitr::kable(per_month, col.names = c("Month", "Loans"))
```
```{r}
#| label: fig-months
#| fig-cap: "Loans per month."
#| fig-width: 6
#| fig-height: 3
barplot(table(recent$month), ylab = "Loans", col = "grey60")
```
## Overdue loans per member
A loan is overdue when it was out for more than `{r} loan_days` days. A loan
that has not been returned counts up to `{r} format(shelf_today)`.
```{r}
#| label: overdue
#| echo: true
end <- recent$return_date
end[is.na(end)] <- shelf_today
recent$days_out <- as.integer(end - recent$loan_date)
overdue <- recent[recent$days_out > loan_days, ]
overdue$member <- tools::toTitleCase(tolower(trimws(overdue$member)))
by_member <- as.data.frame(
sort(table(overdue$member[nzchar(overdue$member)]), decreasing = TRUE)
)
knitr::kable(by_member, col.names = c("Member", "Overdue loans"))
```
In total `{r} nrow(overdue)` of the `{r} nrow(recent)` loans were overdue.
Source: Quarto documentation: Using R
The data are read with read.csv("books.csv") and read.csv("loans.csv", colClasses = "character"), the way every page of this section reads them, and the dates are parsed with as.Date() in the first chunk; the cleaning of the messy member names is reduced to trimws(), tolower() and tools::toTitleCase(), enough to group Ana Ruiz together with ana ruiz followed by a space and to drop the loan whose member is empty. Rendering is one command, run in the project directory, and the next block shows it with what it prints.
quarto render report.qmd
Source: Quarto documentation: Using R
processing file: report.qmd
1/9
2/9 [load]
3/9
4/9 [tbl-months]
5/9
6/9 [fig-months]
7/9
8/9 [overdue]
9/9
output file: report.knit.md
pandoc
to: html
output-file: report.html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
metadata
document-css: false
link-citations: true
date-format: long
lang: en
title: Shelf monthly report
Output created: report.html
Source: Quarto documentation: Using R
The output is Quarto’s progress report. Lines such as 4/9 [tbl-months] come from knitr, which splits the document into nine pieces (text pieces and chunks alternate, and the label of a chunk is shown when it has one; the chunks without a label would show unnamed-chunk-1). The line output file: report.knit.md names the intermediate Markdown file that knitr wrote, which Quarto deletes after the conversion unless --debug is given. The pandoc block lists the Pandoc settings Quarto chose for the html format, and the metadata block the document metadata it passed on (the title; the options that Quarto handles itself, such as params and execute, are not in the list). The last line names the result. The directory now has the report and the directory with its support files:
ls
Source: Quarto documentation: Using R
books.csv
loans.csv
report_files
report.html
report.qmd
Source: Quarto documentation: Using R
report.html is the report and report_files holds what it needs: the figure as figure-html/fig-months-1.png and the CSS and JavaScript of the page in libs. The HTML file is about 25 kB and loads those files, so it has to be moved together with its directory; the option embed-resources: true of the HTML format makes a single self-contained file instead (the older name of the idea in R Markdown is self_contained). The rendered page has the title, the paragraph with its computed numbers (17 loans from 2026-01 to 2026-06-30 of the 16 books in the catalogue), a captioned table and figure that the text refers to by number, the code of the overdue chunk, a table of the six members with overdue loans, and a last sentence with the totals (12 of the 17 loans were overdue). The report that came out is the one shown, as Markdown, in the section on GitHub Markdown below.
How a document is processed
A few rules explain most of what happens in a render and most of the surprises.
The document is evaluated top to bottom, in one R session and one environment: a variable created in an early chunk exists in later chunks, so the order of the chunks matters, and nothing is saved between renders unless caching is on. Quarto starts a new R process for every render, so objects of an interactive session are not visible to the document, and a render starts from a clean state, which is the point of a report. rmarkdown::render() is different: its argument envir defaults to parent.frame(), so called from the console it evaluates the chunks in the calling environment, where the console’s objects are visible and the document’s objects are left behind. The help page suggests new.env() to guarantee an empty environment, and rendering from a script run with Rscript (as the R Markdown examples below do) avoids the problem. Code that works in a render only because of an object defined earlier in the console is the most common reason for a report that cannot be reproduced.
The working directory is the directory of the document. That is why read.csv("books.csv") finds the file, and why a document that is moved without its data fails. The help page of knit() says it so: the working directory when evaluating chunks is the directory of the input document by default, it is possible to change it with the package option opts_knit$set(root.dir = ...), and changing it with a call to setwd in a chunk is not recommended, because figure and cache files may be written to the wrong places. Quarto has its own switch for a project: execute-dir in the project key, whose values are file (the directory of the document being executed) and project (the root of the project). The quarto render option --execute-dir sets it for one render.
Everything the code writes to the standard output, standard error, the graphics devices and the condition system is collected: output text becomes a block after the chunk, a message, warning or error is inserted as text, and a plot becomes an image file in the support directory. A chunk that fails stops the render with the chunk’s label and line numbers unless the error is allowed to continue; both cases are shown in the sections on chunk options and on errors.
Quarto finds R on the system PATH (the Quarto documentation names the environment variable QUARTO_R for pointing it to another installation), and needs the rmarkdown package, which brings knitr with it. The command quarto check reports what it found. Its output has many lines with paths; this is the part about the versions used on this page:
quarto check
Source: Quarto documentation: Using R
Quarto 1.10.19
Pandoc version 3.10.0: OK
Dart Sass version 1.101.0: OK
Typst version 0.15.1: OK
TinyTeX: (not installed)
Tex: (not detected)
knitr: 1.52
rmarkdown: 2.32
Source: Quarto documentation: Using R
(The command printed more lines, with installation paths, progress spinners and the checks for Jupyter and Julia, and they were left out.) The listing shows that Quarto 1.10.19 carries Pandoc 3.10.0, Dart Sass, Typst 0.15.1 and the Deno runtime, that no TeX distribution is installed, and that knitr 1.52 and rmarkdown 2.32 were found. The Pandoc that Quarto uses is not the one on the PATH; rmarkdown, in the section on R Markdown below, reports the other one.
Chunks and chunk options
A code chunk starts with three backticks and the language in braces, {r} for R, and ends with three backticks. What happens to it is controlled by chunk options. There are two places to write them.
```{r overdue, echo=TRUE, fig.width=6}
summary(cars)
```
```{r}
#| label: overdue
#| echo: true
#| fig-width: 6
summary(cars)
```
Source: Quarto documentation: Using R
The first chunk is the R Markdown form, in the header line after the language: a label (the first unnamed item), then name=value pairs separated by commas, with R syntax for the values. The second is the Quarto form: comment lines at the top of the chunk that start with the characters hash and pipe (#|), with YAML syntax for the values. The Quarto documentation explains why: long options such as captions are easier to write on their own line, and editors that do not understand chunk headers can still edit the options. The form is not Quarto’s invention. It is a feature of knitr since version 1.35, whose NEWS says that options written inside a chunk after #| override the same options in the header, so the hash-pipe form also works in R Markdown documents, with either YAML syntax or the comma-separated syntax of the header. The comment character is that of the language of the chunk: knitr 1.51 made a pipe comment in a chunk follow it, and the next block of this section shows what that means for a sql chunk. A value that has to be computed is written with !expr, which is a YAML tag: the Quarto documentation gives #| fig-cap: !expr 'paste("Air", "Quality")'. The Quarto FAQ adds that !expr works inside chunk options and not in the YAML header of the document or the project, and that !r (the R Markdown spelling) is not accepted by Quarto’s validation anywhere.
Quarto also translates names. In its own spelling, the words of an option are separated by hyphens (fig-width, out-width, fig-cap), because Quarto follows Pandoc and YAML conventions, and knitr’s dotted spelling (fig.width) is accepted as well. The next document uses both on two chunks that draw the same figure; the first gives the hyphenated names, the second the dotted ones, and the output document is the same for both.
---
title: "Figure options"
format: gfm
---
```{r}
#| fig-width: 4
#| fig-height: 2
#| fig-cap: "Pages per book."
#| out-width: 50%
hist(read.csv("books.csv")$pages, main = "", xlab = "Pages")
```
```{r}
#| fig.width: 4
#| fig.height: 2
#| fig.cap: "The same, in knitr spelling."
#| out.width: "50%"
hist(read.csv("books.csv")$pages, main = "", xlab = "Pages")
```
# Figure options
``` r
hist(read.csv("books.csv")$pages, main = "", xlab = "Pages")
```
<img src="figures_files/figure-commonmark/unnamed-chunk-1-1.png"
style="width:50.0%" alt="Pages per book." />
``` r
hist(read.csv("books.csv")$pages, main = "", xlab = "Pages")
```
<img src="figures_files/figure-commonmark/unnamed-chunk-2-1.png"
style="width:50.0%" alt="The same, in knitr spelling." />
In both chunks the caption became the alternative text of the image, and out-width: 50% became a width of half of the line (the format: gfm output keeps the HTML img tag because plain Markdown has no width). The figure itself is a PNG file in a figures_files directory next to the document.
The options that matter
The knitr documentation lists the options with their defaults; the table has the ones this page uses and the spelling for each system. A default that differs between knitr’s own knit() and a render is noted, because it is the source of a common surprise.
| knitr (R Markdown) | Quarto | What it does and its default |
|---|---|---|
|
|
Whether to run the chunk ( |
|
|
Whether to show the source code ( |
|
|
Whether the chunk’s output goes into the document ( |
|
|
How text results are shown. In knitr, |
|
|
Whether messages and warnings are kept ( |
|
|
Whether an error is kept in the output and the evaluation continues. knitr’s own default is |
|
|
Merge the source and its output into one block ( |
|
|
The prefix of each line of text output; knitr’s default is |
|
|
The size of a plot in inches. The knitr default is 7 by 7; Quarto sets a default for each format (7 by 5 in general, 9 by 5 for revealjs, 5.5 by 3.5 for PDF) and passes it on to these options. |
|
|
Caption and alternative text of a figure. |
|
|
The size of the plot in the output, which may differ from the size at which it was drawn. |
|
|
Caption of a table. In Quarto, a |
|
|
The name of the chunk, used for file names, for the progress report, for the cache and for references. |
|
|
Whether to cache the chunk’s results ( |
|
|
Paths of documents to run and include. |
|
|
A list of R options set while the chunk runs and restored afterwards. |
|
the language in the braces |
The language of the chunk ( |
Quarto adds options of its own that knitr does not have: code-fold and code-summary, fig-subcap, fig-align, layout-ncol, column, output-location for revealjs, and the cross-reference conventions of the label. The execution options that exist at the document level (eval, echo, output, warning, error, include) are written under the execute key of the header or of a project file, as the report does with echo: false; the Quarto documentation says that all of them can be set globally in the front matter or for each code block.
What the options do
The best way to learn the options is to see them act. The next R Markdown file has one chunk for each of the main options, and the script after it runs knitr::knit() on it and prints the Markdown that comes out. knit() is the first step of every render, and calling it by hand is a good way to see the intermediate file that the render pipeline normally deletes. Its output file is a temporary file, so no path is shown.
---
title: "Chunk options"
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(comment = "#>")
```
`echo = FALSE` hides the code and keeps the result:
```{r hidden, echo=FALSE}
nchar("shelf")
```
`eval = FALSE` shows the code and does not run it:
```{r not-run, eval=FALSE}
stop("never runs")
```
`include = FALSE` runs the chunk and shows neither code nor output:
```{r silent, include=FALSE}
secret <- 42
```
The value is still there: `r secret`.
`collapse = TRUE` puts the code and its output in one block:
```{r merged, collapse=TRUE}
x <- c(3, 5, 8)
mean(x)
sum(x)
```
`results = 'hold'` prints all output after the code:
```{r hold, results='hold'}
cat("first\n")
print("second")
cat("third\n")
```
`results = 'asis'` passes the output through as markdown:
```{r asis, results='asis'}
cat("**bold** text written by cat()\n")
```
`message = FALSE` and `warning = FALSE` drop those conditions:
```{r quiet, message=FALSE, warning=FALSE}
message("a message")
warning("a warning")
1 + 1
```
`error = TRUE` keeps going and shows the error:
```{r failing, error=TRUE}
stop("stopped")
```
`child` pulls in another file:
```{r child-doc, child="intro.md"}
```
The child file is one line of text, intro.md, without a header. The script writes the knitted file to a temporary location and prints it:
out <- knitr::knit("options.Rmd", output = tempfile(fileext = ".md"),
quiet = TRUE)
cat(readLines(out), sep = "\n")
#> ---
#> title: "Chunk options"
#> ---
#>
#>
#>
#> `echo = FALSE` hides the code and keeps the result:
#>
#>
#> ```
#> #> [1] 5
#> ```
#>
#> `eval = FALSE` shows the code and does not run it:
#>
#>
#> ``` r
#> stop("never runs")
#> ```
#>
#> `include = FALSE` runs the chunk and shows neither code nor output:
#>
#>
#>
#> The value is still there: 42.
#>
#> `collapse = TRUE` puts the code and its output in one block:
#>
#>
#> ``` r
#> x <- c(3, 5, 8)
#> mean(x)
#> #> [1] 5.333333
#> sum(x)
#> #> [1] 16
#> ```
#>
#> `results = 'hold'` prints all output after the code:
#>
#>
#> ``` r
#> cat("first\n")
#> print("second")
#> cat("third\n")
#> ```
#>
#> ```
#> #> first
#> #> [1] "second"
#> #> third
#> ```
#>
#> `results = 'asis'` passes the output through as markdown:
#>
#>
#> ``` r
#> cat("**bold** text written by cat()\n")
#> ```
#>
#> **bold** text written by cat()
#>
#> `message = FALSE` and `warning = FALSE` drop those conditions:
#>
#>
#> ``` r
#> message("a message")
#> warning("a warning")
#> 1 + 1
#> ```
#>
#> ```
#> #> [1] 2
#> ```
#>
#> `error = TRUE` keeps going and shows the error:
#>
#>
#> ``` r
#> stop("stopped")
#> ```
#>
#> ```
#> #> Error:
#> #> ! stopped
#> ```
#>
#> `child` pulls in another file:
#>
#>
#> This paragraph lives in a child document.
Reading the output against the file shows the effect of each option. With echo = FALSE the result is there and the code is not; eval = FALSE is the reverse; include = FALSE leaves nothing but the object it created remains available, which the inline expression after it uses; collapse = TRUE puts the code and the result in one block and comment = "#>" (set once, in the setup chunk) produces the #> prefix used in all the examples of this section; results = 'hold' runs the three statements of the chunk and then prints their output together; results = 'asis' hands the text to Pandoc as Markdown, so the **bold** is bold; message = FALSE and warning = FALSE drop the message and the warning but not the value; and error = TRUE shows the error and carries on. The error is printed in the form of R 4.6 with knitr 1.52, which is Error: and, on the next line, ! stopped. The child chunk was replaced by the text of the file.
Setting the options for the whole document
An option that every chunk needs is not repeated on every chunk. There are four places to set it, and a more specific place wins over a more general one: the chunk, the document, the project.
-
In the setup chunk with
knitr::opts_chunk$set(), which works in both systems (the R Markdown way; the setup chunk usually hasinclude = FALSE). -
In the YAML header:
execute:for the execution options of Quarto, as the report does, andknitr: opts_chunk:to pass any knitr option, in Quarto. The Quarto documentation gives the second form withcollapse: trueandcomment: "#>", and alsoopts_knitfor package options; the section on inline code below uses it. -
In the project file
_quarto.ymlor, for the documents of one directory, in_metadata.yml, the same keys; the section on projects shows it. -
On the command line,
quarto render report.qmd -M echo:false, where-M(--metadata) sets a metadata value for one render.
The functions behind the first form can be queried, which shows the defaults and the effect of set():
library(knitr)
opts_chunk$get(c("echo", "eval", "include", "collapse", "comment", "cache"))
#> $echo
#> [1] TRUE
#>
#> $eval
#> [1] TRUE
#>
#> $include
#> [1] TRUE
#>
#> $collapse
#> [1] FALSE
#>
#> $comment
#> [1] "##"
#>
#> $cache
#> [1] FALSE
#>
opts_chunk$set(collapse = TRUE, comment = "#>")
opts_chunk$get(c("collapse", "comment"))
#> $collapse
#> [1] TRUE
#>
#> $comment
#> [1] "#>"
#>
# The language names that knitr has an engine for.
sort(names(knit_engines$get()))
#> [1] "asis" "asy" "awk" "bash" "block" "block2"
#> [7] "bslib" "c" "cat" "cc" "coffee" "comment"
#> [13] "css" "ditaa" "dot" "embed" "eviews" "exec"
#> [19] "fortran" "fortran95" "gawk" "go" "groovy" "haskell"
#> [25] "highlight" "js" "julia" "lein" "mysql" "node"
#> [31] "octave" "perl" "php" "psql" "python" "R"
#> [37] "Rcpp" "Rscript" "ruby" "sas" "sass" "scala"
#> [43] "scss" "sed" "sh" "sql" "stan" "stata"
#> [49] "targets" "tikz" "verbatim" "zsh"
# Outside a render there is no output format.
is_html_output()
#> [1] FALSE
The defaults are the ones in the table above, and the second query shows that set() changed the values for the rest of the session. The list at the end is names(knit_engines$get()), the languages that knitr can run. is_html_output() is FALSE here because the script is not inside a render; inside a chunk of an HTML document it is TRUE, and it, with the other predicates such as is_latex_output() and pandoc_to(), is what a chunk uses to behave differently by format. The Quarto documentation also describes the environment variable QUARTO_EXECUTE_INFO, a JSON description of the execution context from which a cell can learn the format.
Labels, references and reuse
Give chunks labels. A label is the name used for the figure file (fig-months-1.png), for the cache files, and in the progress report and the error message (Quitting from ... [read-loans]), and a label must be unique in the document: knitr refuses duplicates, as the script below shows. A chunk can reuse the code of another with ref.label (the code of the referenced chunks is inherited), and the older form <<label>> inside a chunk includes a chunk by name.
The script gives two chunks the same label and shows what knitr says; it needs no files, because the document is passed as text:
chunks <- c("```{r a}", "1", "```", "```{r a}", "2", "```")
tryCatch(
knitr::knit(text = chunks, quiet = TRUE),
error = \(e) conditionMessage(e)
)
#> [1] "Duplicate chunk label 'a', which has been used for the chunk:\n1"
In Quarto, labels that start with fig-, tbl- and lst- mark figures, tables and code listings that can be cited with @fig-months; unless a cross reference is wanted, the Quarto documentation advises against labels with those prefixes.
Figures and tables
Plots made by base graphics, lattice or ggplot2 are saved as files and inserted at the point where they were drawn, with the size from fig-width and fig-height and the caption from fig-cap; one chunk may draw several. The graphics devices are the subject of Base Graphics and Devices, and a ggplot2 object is printed in a chunk the same way (ggplot2 4.0.3 is installed; the report uses base graphics so that it needs no package, and no ggplot2 chunk was run for this page). Tables are Markdown tables made by knitr::kable(), which takes a data frame and returns Markdown, so it works in every output format; col.names and caption are its everyday arguments, and in Quarto a tbl-cap option can supply the caption instead. The document option df-print (default, kable, tibble or paged in HTML) sets how data frames are printed when a chunk ends with one; kable as the choice makes every printed data frame a Markdown table.
Engines: other languages in the same document
The language in the braces selects an engine. knitr has engines for R, Python, Bash and other shells, SQL, C and Fortran, Rcpp, Stan, JavaScript, CSS and more (the list printed above), and Quarto adds Jupyter, Observable and Julia. A document with R chunks is run by knitr; a Quarto document with only Python chunks is run by Jupyter, which is not installed in the image (quarto check reports Jupyter: (None)). This is the engine binding rule of Quarto: the engine is chosen from the languages of the code blocks, and engine: knitr or jupyter: python3 in the header says so explicitly. The next document uses the knitr engine for R, Bash, SQL and Python in one file, and renders to GitHub Markdown so that the output is plain text.
---
title: "Engines"
format: gfm
engine: knitr
---
```{r}
#| label: load
loans <- read.csv("loans.csv", colClasses = "character")
nrow(loans)
```
```{bash}
#| label: shell
head -n 2 loans.csv
```
```{r}
#| label: db
library(DBI)
con <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(con, "loans", loans)
```
```{sql}
--| label: query
--| connection: con
SELECT member, COUNT(*) AS n FROM loans GROUP BY member ORDER BY n DESC LIMIT 3
```
```{python}
#| label: py
import pandas as pd
df = pd.read_csv("loans.csv")
print(len(df))
```
# Engines
``` r
loans <- read.csv("loans.csv", colClasses = "character")
nrow(loans)
```
[1] 60
``` bash
head -n 2 loans.csv
```
"loan_id","isbn","member","loan_date","return_date"
"L001","9780141439518","Hugo Silva","2025-01-14","2025-02-04"
``` r
library(DBI)
con <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(con, "loans", loans)
```
``` sql
SELECT member, COUNT(*) AS n FROM loans GROUP BY member ORDER BY n DESC LIMIT 3
```
| member | n |
|:-------------|----:|
| Hugo Silva | 10 |
| Ana Ruiz | 10 |
| Chloe Martin | 9 |
3 records
``` python
import pandas as pd
df = pd.read_csv("loans.csv")
print(len(df))
```
60
Four things in this document are worth noting. The bash chunk runs the command with the shell and shows the first two lines of loans.csv (during the render knitr prints a running: line with the command). The sql chunk needs a database connection, which the chunk option connection: con takes from the R object con created in the previous chunk (an in-memory SQLite database with the loans table, made with DBI 1.3.0 and RSQLite 3.53.3); its result is a table with a count of records. The python chunk went through reticulate 1.47.0. The test set the environment variable RETICULATE_PYTHON to the system interpreter (Python 3.12.3 with pandas 3.0.6); without it, in a first test run, reticulate set up an environment of its own with uv, downloaded an interpreter, and the chunk then failed with ModuleNotFoundError: No module named 'pandas' because that interpreter had no pandas; a build machine that is offline or locked down does not want that (Python Interop with reticulate has the rules, including py_require()). And in the SQL chunk the options start with two hyphens and a pipe, --|, because -- is the comment character of SQL. Writing the hash-pipe form there is an error with a message that says what to do:
---
title: "SQL chunk options"
format: gfm
---
```{r}
library(DBI)
con <- dbConnect(RSQLite::SQLite(), ":memory:")
```
```{sql}
#| connection: con
SELECT 1 AS one
```
processing file: sql-options.qmd
Error: The chunk options should start with '--| ' instead of '#| '.
Execution halted
WARN: Error encountered when rendering files
1
Caching
Some chunks are slow, and re-running them at every render is a waste while the text around them is being written. The cache stores a chunk’s results after the first evaluation and loads them in later renders when the chunk has not changed. It is enabled with the cache option, for one chunk or for the document under execute; with cache: true in the header, the next document caches its only chunk.
---
title: "A cached chunk"
format: gfm
execute:
cache: true
---
```{r}
#| label: slow
loans <- read.csv("loans.csv")
nrow(loans)
```
After quarto render cached.qmd the directory has the output and a cache directory named after the document, with one subdirectory per output format:
ls -d cached*
ls cached_cache
cached_cache
cached.md
cached.qmd
commonmark
(The two listings are the output of the two commands, one after the other.) Inside the format directory the chunk is stored in three files named after its label plus a hash, with the extensions .RData, .rdb and .rdx, and a file called __packages. The knitr documentation explains the hash: it is an MD5 digest of the R code and the chunk options, so any change to the chunk produces a new name and invalidates the cache. That is also the weakness. The digest covers the chunk, not the data files it reads or the other chunks it depends on, so a chunk that reads loans.csv is not recomputed when the file changes, and a chunk that uses an object made in another chunk must say so with dependson or autodep. Chunks with side effects (writing a file, setting an option, loading a package) are not replayed from the cache. The command-line switches --cache, --no-cache and --cache-refresh of quarto render force the use of the cache, disable it, or refresh it without editing the document. Two alternatives that do not have these problems are described in the Quarto documentation: disable execution when the page is prose (execute: enabled: false), and freeze in a project, shown in the project section below. For a short report like this one, the best cache is no cache.
Inline code
Numbers in prose are where reports go stale, so they are computed too. Inline code is an expression in the middle of a sentence, between single backticks. There are two syntaxes for R. R Markdown uses the form with a backtick, the letter r, a space, the expression and a closing backtick. Quarto 1.4 added a form that is the same for all engines: the language in braces, as in a chunk, then the expression. Both work in a Quarto document with knitr, and the report uses the second. The difference is in how the result is treated: the Quarto form treats the result as text and escapes any Markdown in it, while the knitr form treats the result as Markdown, which the Quarto documentation says is equivalent to wrapping the expression in I(). The next document shows the two forms, the effect of I(), the escape for documenting the syntax, the knitr: key of the header that sets the options of all chunks, and the Quarto shortcode for including another file.
---
title: "Inline code"
format: gfm
knitr:
opts_chunk:
collapse: true
comment: "#>"
---
```{r}
loans <- read.csv("loans.csv", colClasses = "character")
n_loans <- nrow(loans)
n_loans
```
Quarto syntax: `{r} n_loans` loans.
Knitr syntax: `r n_loans` loans.
The Quarto form escapes markdown: `{r} "**bold**"`.
The knitr form keeps it: `r "**bold**"`.
With I(), the Quarto form keeps it too: `{r} I("**bold**")`.
An unevaluated example: ``{r} n_loans``.
Doubled braces do the same: `{{r}} n_loans`.
{{< include intro.md >}}
# Inline code
``` r
loans <- read.csv("loans.csv", colClasses = "character")
n_loans <- nrow(loans)
n_loans
#> [1] 60
```
Quarto syntax: 60 loans. Knitr syntax: 60 loans.
The Quarto form escapes markdown: \*\*bold\*\*. The knitr form keeps it:
**bold**. With I(), the Quarto form keeps it too: **bold**.
An unevaluated example: `{r} n_loans`. Doubled braces do the same:
`{r} n_loans`.
This paragraph lives in a child document.
Reading the result: both forms gave 60; the Quarto form escaped the asterisks of **bold** (they appear as \*\*bold\*\* in the Markdown, so the reader sees two asterisks on each side), the knitr form made the word bold, and I() restored that in the Quarto form. The extra backtick and the doubled braces both turn the example into text that is not evaluated, which is how the Quarto documentation writes about the syntax, and the shortcode {{< include intro.md >}} inserted the file (it needs no chunk, unlike the child option). The knitr: key in the header is the way to give the whole document the options collapse: true and comment: "#>", and the output shows #> in the first chunk.
The rules to remember are the ones in the Quarto documentation. Keep inline expressions simple, confined to values already computed in chunks, because the protocol they use is not compatible with some libraries. Inline code cannot be used in the YAML header, so a title or a figure caption that must contain a computed number cannot use it; for a caption the documentation recommends combining inline code with the cross-reference div syntax, and a chunk option can take a computed value with !expr. The inline expressions of a document with no executable chunk are not evaluated unless the engine is set explicitly. And a result is formatted as R formats a single value, so format() and round() decide what the reader sees: {r} nrow(recent) is an integer, but the share of overdue loans needs round(100 * mean(...)) or format().
The version history is short. The form with the letter r is knitr’s own and the one in all older tutorials. The 1.4 release notes of Quarto (January 2024) list support for executing inline expressions, with the Python spelling as the example, under the Jupyter engine; the Inline Code article documents the single {lang} syntax for all engines, with {r} for knitr, and says that the native knitr syntax stays supported. This page ran the {r} form on Quarto 1.10.19 and does not claim in which 1.x release it started to work for knitr documents.
Parameters
A parameterized report is a document with inputs. The monthly report is the example: it is the same document every month, and only the first month to include changes. Parameters are declared in the YAML header with default values, as report.qmd does:
params:
since: "2026-01"
Source: Quarto documentation: Parameters
In the code, the values are the list params, so params$since is "2026-01". For the knitr engine, Quarto uses the standard knitr params YAML option (for Jupyter it uses a cell tagged parameters, in the style of Papermill, and for Julia the keys become variables). The default is a character string and not a number, and it is quoted in the YAML for that reason: the comparison with the month strings of the loans is a comparison of text.
To render with other values, pass them on the command line with -P (--execute-param), one key:value for each parameter, or put them in a YAML file and give it to --execute-params. Each command writes to a different file with --output, so that the first report is not overwritten. The file params-2026-03.yml has the single line since: "2026-03".
quarto render report.qmd -P since:2026-03 --output report-2026-03.html
quarto render report.qmd --execute-params params-2026-03.yml --output report-yml.html
Source: Quarto documentation: Parameters
processing file: report.qmd
1/9
2/9 [load]
3/9
4/9 [tbl-months]
5/9
6/9 [fig-months]
7/9
8/9 [overdue]
9/9
output file: report.knit.md
pandoc --output report-2026-03.html
to: html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
metadata
...
Output created: report-2026-03.html
processing file: report.qmd
1/9
2/9 [load]
3/9
4/9 [tbl-months]
5/9
6/9 [fig-months]
7/9
8/9 [overdue]
9/9
output file: report.knit.md
pandoc --output report-yml.html
to: html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
metadata
...
Output created: report-yml.html
Source: Quarto documentation: Parameters
(The two transcripts are shown one after the other, with the list of metadata shortened.) Both reports start with "This report covers the loans made from 2026-03 to 2026-06-30: 10 loans of the 16 books in the catalogue." The first command shows how --output appears in the Pandoc line.
What a wrong parameter looks like is useful to know, because the message comes from R, not from Quarto. A parameter that the document does not declare is rejected, and so is any use of params in a document that has no params key:
quarto render report.qmd -P until:2026-06 --output x.html
Source: Quarto documentation: Parameters
Error in knit_params_get(input_lines, params) :
render params not declared in YAML: until
Calls: .main -> execute -> <Anonymous> -> knit_params_get
Execution halted
WARN: Error encountered when rendering files
1
Source: Quarto documentation: Parameters
The message is render params not declared in YAML: until. It comes from the rmarkdown function knit_params_get(), which the traceback names: Quarto’s knitr engine calls the same code, and it checks the names against the declared parameters. The reverse mistake, a chunk that reads params$since in a document with no params key, stops the render at the chunk. This is the document no-params.qmd from the test directory, which has one chunk labelled use-since whose only line is params$since:
1/3
processing file: no-params.qmd
2/3 [use-since]
Error:
! object 'params' not found
Quitting from no-params.qmd:6-9 [use-since]
Execution halted
WARN: Error encountered when rendering files
1
Source: Quarto documentation: Parameters
The error is the plain R error object 'params' not found, followed by the line range and the label of the chunk. A parameter that is declared but not given a value on the command line takes its default, so a report with parameters always renders, which is convenient and also a way to publish a report with stale defaults; the default should be the value the report is normally made with, or the report should stop if it is missing.
Parameters in R Markdown
R Markdown has the same params key, and the value is passed from R: rmarkdown::render() takes params, a named list that overrides the defaults in the header. The script renders the twin document report.Rmd (shown in the R Markdown section below) with since = "2026-03", and then shows the message for a parameter that is not declared, captured with tryCatch():
out <- rmarkdown::render("report.Rmd", output_file = "report-rmd-2026-03.html",
params = list(since = "2026-03"), quiet = TRUE)
basename(out)
#> [1] "report-rmd-2026-03.html"
msg <- tryCatch(
rmarkdown::render("report.Rmd", params = list(until = "2026-06"),
quiet = TRUE),
error = \(e) conditionMessage(e)
)
msg
#> [1] "render params not declared in YAML: until"
Source: R Markdown documentation: render
The same message appears in both systems because the check is the same code, which is also why the two behave alike. The help page of render() adds that passing params = "ask" starts an application that helps to configure the parameters; the function rmarkdown::knit_params_ask() is documented as running a Shiny application that asks for the parameter values of a document (this page did not run it, because it needs a browser), and the R Markdown lesson on parameters describes the "Knit with Parameters" entry in the menu next to the Knit button of RStudio, which does the same in one step. Types matter in R Markdown too: the YAML decides whether 2026-03 is text or a date or a number, and a parameter that is a date can be declared with the R tag !r as.Date("2026-06-30"), which works in R Markdown and, as the Quarto FAQ states, not in Quarto (use !expr inside chunk options, or convert in the code).
One report per month
A parameterized report is made to be rendered in a loop, once for each value. The script renders the R Markdown report for three months from a script; the function builds a file name from the month so that the outputs do not overwrite one another, and returns the names. vapply() is used instead of a loop that grows a vector.
months <- c("2026-01", "2026-02", "2026-03")
render_month <- function(month) {
out <- rmarkdown::render("report.Rmd",
output_file = paste0("monthly-", month, ".html"),
params = list(since = month), quiet = TRUE)
basename(out)
}
vapply(months, render_month, character(1L), USE.NAMES = FALSE)
#> [1] "monthly-2026-01.html" "monthly-2026-02.html" "monthly-2026-03.html"
Source: R Markdown documentation: render
The same loop for the Quarto document can be written without the quarto R package by calling the program from R with system2(); the script renders the February report and returns the exit status of the command, which is 0 when it succeeded:
status <- system2("quarto", c("render", "report.qmd", "--quiet",
"-P", "since:2026-02",
"--output", "report-feb.html"))
status
#> [1] 0
file.exists("report-feb.html")
#> [1] TRUE
Source: R documentation: system2
With the quarto R package installed, the same call is quarto::quarto_render("report.qmd", execute_params = list(since = "2026-02")); that package is not installed in the image, so the call is described and not run. The help page of quarto_render() has the arguments input, output_format, output_file, execute, execute_params, execute_dir, cache, metadata, quiet and quarto_args, and says that it runs the program, which is why the program has to be installed in any case. To produce the reports in a scheduled job, put the loop in a script and run it with Rscript, as in Scripts and the Command Line.
Output formats
The format is chosen in the header, with the format key in Quarto (output in R Markdown), or for one render on the command line with --to. Quarto’s format names follow Pandoc’s, and so do their options, with hyphens instead of underscores. The table compares the two systems for the formats this page produced or tried; the Quarto FAQ for R Markdown users has the full correspondence, including bookdown, blogdown, xaringan, flexdashboard and distill, packages of the R Markdown family that are not installed here and that Quarto replaces with its own books, websites, slides and dashboards.
| Output | Quarto | R Markdown | On this page |
|---|---|---|---|
HTML page |
|
|
rendered by both: |
Word |
|
|
rendered by both, Pandoc only |
GitHub Markdown |
|
|
rendered by both |
Slides in HTML |
|
|
Quarto’s revealjs rendered |
PDF through LaTeX |
|
|
fails: the image has no TeX |
PDF through Typst |
|
not available |
rendered: |
The command-line form of the choice appears in all the examples below. A document can also list several formats under format:, and quarto render without --to then renders every format listed (the Quarto documentation says the command renders all formats in the document YAML, and renders HTML when none is given).
HTML
HTML is the default and the richest format. Its options are under format: html: in the header or the project file: toc: true for a table of contents, number-sections, theme, code-fold (collapse the code behind a toggle), df-print, embed-resources (one self-contained file) and many more; the reference page lists them all. The R Markdown function html_document() has the same ideas under other names: toc, toc_float, number_sections, code_folding, df_print, and self_contained = TRUE, which is why the R Markdown report is a single file of about 650 kB with the figure embedded in the page, while the Quarto report is a small file with a directory beside it. The report was rendered as HTML by both systems, and the two pages have the same numbers and tables.
Word
Word output needs no TeX, only Pandoc. Quarto renders it with --to docx; the output is report.docx, and the figure and the tables are inside it.
quarto render report.qmd --to docx
Source: Quarto documentation: MS Word
pandoc
to: docx
output-file: report.docx
default-image-extension: png
metadata
title: Shelf monthly report
Output created: report.docx
Source: Quarto documentation: MS Word
The progress lines of the render (processing file and the chunk numbers) are the same as for HTML and are left out of this and the next transcripts, which start at the Pandoc block. A .docx file is a ZIP archive of XML parts, which the script at the end of this section opens to check what was made. The Word styles can be changed with a reference document (reference-doc in Quarto, reference_docx in word_document()).
GitHub Markdown
The gfm format writes a Markdown file that GitHub renders, with the figure files in a directory beside it. It is the format to use for a README that has results in it, and the easiest way to see what Quarto’s pipeline produced, because the file is plain text. This is the shelf report as report.md:
quarto render report.qmd --to gfm
cat report.md
# Shelf monthly report
This report covers the loans made from 2026-01 to 2026-06-30: 17 loans
of the 16 books in the catalogue.
<a href="#tbl-months" class="quarto-xref">Table 1</a> and
<a href="#fig-months" class="quarto-xref">Figure 1</a> show how they are
spread over the months.
## Loans per month
<div id="tbl-months">
Table 1: Loans per month.
<div class="cell-output-display">
| Month | Loans |
|:--------|------:|
| 2026-01 | 4 |
| 2026-02 | 3 |
| 2026-03 | 3 |
| 2026-04 | 4 |
| 2026-05 | 3 |
</div>
</div>
<div id="fig-months">

Figure 1: Loans per month.
</div>
## Overdue loans per member
A loan is overdue when it was out for more than 21 days. A loan that has
not been returned counts up to 2026-06-30.
``` r
end <- recent$return_date
end[is.na(end)] <- shelf_today
recent$days_out <- as.integer(end - recent$loan_date)
overdue <- recent[recent$days_out > loan_days, ]
overdue$member <- tools::toTitleCase(tolower(trimws(overdue$member)))
by_member <- as.data.frame(
sort(table(overdue$member[nzchar(overdue$member)]), decreasing = TRUE)
)
knitr::kable(by_member, col.names = c("Member", "Overdue loans"))
```
| Member | Overdue loans |
|:--------------|--------------:|
| Ana Ruiz | 3 |
| Chloe Martin | 3 |
| Dev Patel | 2 |
| Hugo Silva | 2 |
| Eva Lindqvist | 1 |
| Grace Liu | 1 |
In total 12 of the 17 loans were overdue.
(The quarto render transcript of this step is like the one for Word and is not shown.) The Markdown shows what the pipeline does. The cross references became HTML a elements with the class quarto-xref, because GitHub Markdown has no cross-reference syntax; the table is a pipe table in a div with the caption written as text; the figure is an image file in report_files/figure-commonmark; and the chunk with echo: true is a fenced block with the language r. In R Markdown, github_document(html_preview = FALSE) does the same job and html_preview = TRUE also writes an HTML preview next to the Markdown file.
Slides with revealjs
Quarto’s revealjs format makes slides in HTML with the reveal.js library, which Quarto carries. The slides are Markdown: a second-level heading starts a slide, and chunks work as in a report. In presentation formats the default of echo is false, so the file has no execute key and still shows no code.
---
title: "Shelf loans"
subtitle: "Overdue loans, January to June 2026"
format: revealjs
---
```{r}
#| label: load
#| include: false
loans <- read.csv("loans.csv", colClasses = "character")
loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
shelf_today <- as.Date("2026-06-30")
loan_days <- 21L
end <- loans$return_date
end[is.na(end)] <- shelf_today
loans$days_out <- as.integer(end - loans$loan_date)
overdue <- loans[!is.na(loans$days_out) & loans$days_out > loan_days, ]
```
## Overdue loans
`{r} nrow(overdue)` of the `{r} nrow(loans)` loans were out for more than
`{r} loan_days` days.
## How long loans stay out
```{r}
#| fig-width: 8
#| fig-height: 4
hist(loans$days_out, breaks = 10, main = "", xlab = "Days out")
abline(v = loan_days, lty = 2)
```
Source: Quarto documentation: Revealjs
quarto render slides.qmd
ls slides_files slides_files/libs
Source: Quarto documentation: Revealjs
pandoc
to: revealjs
output-file: slides.html
standalone: true
wrap: none
default-image-extension: png
html-math-method:
method: mathjax
url: >-
https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS_HTML-full
slide-level: 2
metadata
...
Output created: slides.html
slides_files:
figure-revealjs
libs
slides_files/libs:
clipboard
quarto-html
revealjs
Source: Quarto documentation: Revealjs
(The list of reveal.js settings that Quarto passed on, a dozen lines of width, height, transition and the like, is shortened to three dots, and the transcript is followed by the listing.) slides.html is the presentation; slides_files has the plot of the second slide in figure-revealjs, and the libraries of reveal.js in libs. The figure of a slide has a default size of 9 by 5 inches, the revealjs entry in the table of default sizes. R Markdown has ioslides_presentation(), slidy_presentation() and beamer_presentation(), and the xaringan package, none of which was run.
A PDF can be made in two ways. Through LaTeX, format: pdf in Quarto (pdf_document in R Markdown), the output is typeset by a TeX engine, and the image has no TeX installation. The Quarto command fails with a message that says what is missing:
quarto render report.qmd --to pdf
echo $?
Source: Quarto documentation: PDF Engine
pandoc
to: latex
output-file: report.tex
standalone: true
pdf-engine: lualatex
variables:
graphics: true
tables: true
default-image-extension: pdf
metadata
...
Rendering PDF
running lualatex - 1
No TeX installation was detected.
Please run 'quarto install tinytex' to install TinyTex.
If you prefer, you may install TexLive or another TeX distribution.
WARN: Error encountered when rendering files
1
Source: Quarto documentation: PDF Engine
The Pandoc block shows pdf-engine: lualatex, Quarto’s default engine (the default of pdf_document() is latex_engine = "pdflatex"); the render got as far as the intermediate report.tex and stopped at "No TeX installation was detected", and the command’s exit status, printed by echo $?, is 1, which makes a script or a scheduled job fail. The file report.tex is left in the directory (the next step removed it with rm report.tex), and the cause is fixed by installing TeX. The message names the Quarto way, and the R way is the tinytex package, which installs TinyTeX; both are shown as fragments, not run, because they download and install software:
quarto install tinytex
Source: Quarto documentation: PDF Engine
tinytex::install_tinytex()
Source: TinyTeX documentation: TinyTeX
Those two lines are fragments, not run: tinytex 0.61 is installed in the image, but TinyTeX itself was not. With a TeX distribution, quarto render report.qmd --to pdf writes report.pdf; the Quarto documentation recommends TinyTeX and says that Quarto’s own PDF compilation installs missing TeX packages automatically for TinyTeX and TeX Live. R Markdown’s pdf_document fails the same way, with a message of its own; the script in the section on R Markdown formats, below, prints it.
The second way to a PDF needs no TeX at all. Typst is a newer typesetting system that is part of Quarto (the 1.4 release notes list Typst among the dependencies updated, to 0.10.0), and the Quarto program carries the Typst compiler (0.15.1 here, as quarto check showed), so --to typst writes a PDF directly:
quarto render report.qmd --to typst
pandoc
to: typst
output-file: report.typ
standalone: true
shift-heading-level-by: -1
default-image-extension: svg
wrap: none
citeproc: false
metadata
title: Shelf monthly report
[typst]: Compiling report.typ to report.pdf...DONE
Output created: report.pdf
The Pandoc line says to: typst, and Quarto then calls the Typst compiler (Compiling report.typ to report.pdf). This is the PDF that was rendered for real on this page; the figure is drawn as SVG because Typst can embed it. The Typst documentation of Quarto lists known limitations, such as the default sizes of images, which differ from the other formats, and the output has Typst’s look and not LaTeX’s, so a journal or a thesis that requires LaTeX still needs TeX.
Checking the outputs
The files that the steps above made are checked with a script that is run in the project directory: it tests that each file exists, lists the first parts of the Word archive, and reads the first bytes of the PDF, which for any PDF are the characters %PDF-.
files <- c("report.html", "report.docx", "report.md", "report.pdf",
"slides.html")
file.exists(files) |> setNames(files)
#> report.html report.docx report.md report.pdf slides.html
#> TRUE TRUE TRUE TRUE TRUE
# A Word file is a zip archive of XML parts.
head(utils::unzip("report.docx", list = TRUE)$Name, 4L)
#> [1] "[Content_Types].xml" "_rels/.rels"
#> [3] "word/document.xml" "word/_rels/document.xml.rels"
# A PDF starts with a header line.
rawToChar(readBin("report.pdf", "raw", 5L))
#> [1] "%PDF-"
Source: R documentation: unzip
After all the renders, the project directory has the outputs of every format next to the sources:
ls
Source: Quarto documentation: Using R
books.csv
loans.csv
monthly-2026-01.html
monthly-2026-02.html
monthly-2026-03.html
params-2026-03.yml
report-2026-03.html
report.docx
report-feb.html
report_files
report.html
report.md
report.pdf
report.qmd
report.Rmd
report-rmd-2026-03.html
report-rmd.docx
report-rmd_files
report-rmd.html
report-rmd.log
report-rmd.md
report-rmd.tex
report-yml.html
slides_files
slides.html
slides.qmd
Source: Quarto documentation: Using R
The listing (in the locale’s order) has the sources (books.csv, loans.csv, params-2026-03.yml, report.qmd, report.Rmd, slides.qmd); the HTML reports of Quarto (report.html, the parameter runs report-2026-03.html and report-yml.html, and report-feb.html from the script that called the program) and of R Markdown (report-rmd.html, report-rmd-2026-03.html and the three monthly- files of the loop); report.docx and report-rmd.docx; report.md and report-rmd.md; report.pdf from Typst; slides.html; the support directories report_files, report-rmd_files and slides_files; and what the R Markdown PDF attempt left behind, report-rmd.tex and report-rmd.log. A real project collects the outputs in a directory with output-dir (see the project section below), so that the sources and the products do not mix.
Errors in chunks
A chunk that fails is the most common problem in a render, and the two systems treat it the same way. By default the render stops at the first error, names the chunk and the lines, and writes no output. The document fails.qmd has one chunk, labelled read-loans, that reads a file that does not exist:
---
title: "A chunk that fails"
format: html
---
```{r}
#| label: read-loans
loans <- read.csv("no-such-file.csv")
```
This text is never reached.
quarto render fails.qmd
echo $?
processing file: fails.qmd
1/3
2/3 [read-loans]
Error in `file()`:
! cannot open the connection
Backtrace:
▆
1. └─utils::read.csv("no-such-file.csv")
2. └─utils::read.table(...)
3. └─base::file(file, "rt")
Quitting from fails.qmd:6-9 [read-loans]
Execution halted
WARN: Error encountered when rendering files
1
The error is R 4.6’s: the message cannot open the connection with the call file() and a backtrace (the symbols are the Unicode box characters that rlang prints), then the line that matters for the document: Quitting from fails.qmd:6-9 [read-loans], the file, the lines of the chunk and its label, and Execution halted. The warning that read.csv() also raises about the missing file does not appear because the error stopped the render first. Quarto adds WARN: Error encountered when rendering files and the exit status is 1. The label in the message is the reason to name the chunks; an unlabelled chunk is reported as unnamed-chunk-1, which tells the reader nothing in a long document.
Sometimes the error is the point of the page, for example in a tutorial, or when a chunk is allowed to fail. The option error: true (error = TRUE in R Markdown) keeps the error in the output and continues. The document error-true.qmd has the same failing call:
---
title: "A chunk that fails"
format: gfm
---
```{r}
#| error: true
loans <- read.csv("no-such-file.csv")
```
The document goes on after the failed chunk.
quarto render error-true.qmd
cat error-true.md
# A chunk that fails
``` r
loans <- read.csv("no-such-file.csv")
```
Warning in file(file, "rt"): cannot open file 'no-such-file.csv': No such file
or directory
Error in `file()`:
! cannot open the connection
The document goes on after the failed chunk.
(The transcript of quarto render, which ends with Output created: error-true.md and the exit status 0, is not repeated.) This time both conditions are in the document, the warning from file() first, then the error, and the text after the chunk follows. The Quarto documentation notes that error: true implies that errors in the code do not stop the processing of the document; the exit status of the render is 0, so a pipeline sees a green build over a page that contains an error. Use it on a chunk that is meant to fail, not on a document, and look at the output. include: false and error: true do not combine well: knitr stops on errors in a chunk whose output is not included, because the error would be invisible.
The table collects the messages of this page, which are all real.
| Message | Cause | What to do |
|---|---|---|
|
A chunk raised an error; the file, lines and label identify it |
Fix the code, or run the chunks interactively; |
|
The chunk reads |
Declare the parameters in the header |
|
A parameter was given with |
Declare it, or correct the name |
|
A pipe comment in a |
Use the comment character of the language |
|
|
|
|
|
Install TinyTeX and read the |
|
|
The same |
Quarto projects and _quarto.yml
A Quarto project is a directory with a _quarto.yml file. The project gives four things, according to the documentation: a way to render all or some of the files of a directory with one command, a way to share YAML options between documents, the ability to redirect the outputs to another directory, and the ability to freeze rendered output so that documents are not re-executed unless they changed. A project has a type: default (a set of documents), website, book or manuscript (the documentation also lists blog and confluence among the choices of quarto create project), and the type adds behaviour: a website has navigation and a search box, a book has chapters and cross-references. Documents do not need a project, as the render of report.qmd above shows; a project is for several documents, or for options that should apply to all of them.
The project file for the shelf report is the smallest useful one. It names the document to render, so that other .qmd files in the directory (a draft, a test) are not rendered; it gives all HTML documents a table of contents; and it freezes the execution of the documents.
project:
type: default
render:
- report.qmd
format:
html:
toc: true
execute:
freeze: auto
The options come from the documentation. Under project: type, render (the files to render; the default is all valid input files, apart from those whose name starts with a dot or an underscore and README files), output-dir, execute-dir, lib-dir, resources, pre-render and post-render scripts, and preview. The format key holds the options for each output format, here toc: true for the HTML of every document of the project, and the execute key the defaults for chunks, here freeze: auto. The render list accepts wildcards and a ! prefix to exclude files, for example "*.qmd" and "!draft.qmd". Document options are merged with the project options: objects and arrays are merged instead of replaced, the document wins on a conflict, and the exception is format: a document that defines format must define the whole list of formats it wants. The order is _quarto.yml, then a _metadata.yml in a directory (the options of the documents under it), then the YAML of the document. A file _quarto.yml.local holds changes that are not for version control, such as execute: cache: true on one machine, and metadata-files includes other YAML files.
In the project directory, with report.qmd, books.csv, loans.csv and _quarto.yml, rendering the project is quarto render without arguments. The transcript shows the Pandoc block with toc: true, which came from the project file:
quarto render
pandoc
to: html
output-file: report.html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
toc: true
metadata
document-css: false
link-citations: true
date-format: long
lang: en
title: Shelf monthly report
Output created: report.html
(The progress lines before the Pandoc block are the same as in the first render and are left out.) The directory now has, in addition to the outputs, a hidden .quarto directory with Quarto’s own bookkeeping, which belongs in the ignore file of version control, and a _freeze directory, which does not:
ls -A
find _freeze -type f
books.csv
_freeze
loans.csv
.quarto
_quarto.yml
report_files
report.html
report.qmd
_freeze/report/figure-html/fig-months-1.png
_freeze/report/execute-results/html.json
(The two listings are the output of the two commands.) With freeze: auto, the results of executing report.qmd are stored in _freeze/report/execute-results/html.json, with the figure beside it. The Quarto documentation advises checking _freeze into version control, so that others can render the project without reproducing the computational environment, and says to delete it when things outside the source change, such as the input data. The second quarto render shows the effect: the source did not change, so the code did not run again.
pandoc
to: html
output-file: report.html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
toc: true
metadata
document-css: false
link-citations: true
date-format: long
lang: en
title: Shelf monthly report
Output created: report.html
There is no processing file line and no chunk progress, only the Pandoc conversion. The two values are freeze: true, which never re-renders during a project render, and freeze: auto, which re-renders when the source changes. Note the limit that the documentation gives: freeze applies to project renders, and rendering a single document or a subdirectory always executes the code. So quarto render report.qmd ignores the freeze, and quarto render with the project does not.
Two more commands belong to project work. quarto preview starts a local server, renders the project and re-renders when a file is saved, and opens the result in a browser; it is a fragment here, not run, because it does not end by itself:
quarto preview
quarto create project asks for a type and a name and writes a project skeleton (the types are default, website, blog, manuscript, book and confluence), also a fragment. The listing of the options of quarto render is useful to have close; it is the real text of the program’s help, with the trailing blanks removed:
quarto render --help
Usage: quarto render [input] [args...]
Version: 1.10.19
Description:
Render files or projects to various document types.
Options:
-h, --help - Show this help.
-t, --to - Specify output format(s).
-o, --output - Write output to FILE (use '--output -' for stdout).
--output-dir - Write output to DIR (path is input/project relative)
-M, --metadata - Metadata value (KEY:VALUE).
--site-url - Override site-url for website or book output
--execute - Execute code (--no-execute to skip execution).
-P, --execute-param - Execution parameter (KEY:VALUE).
--execute-params - YAML file with execution parameters.
--execute-dir - Working directory for code execution.
--execute-daemon - Keep Jupyter kernel alive (defaults to 300 seconds).
--execute-daemon-restart - Restart keepalive Jupyter kernel before render.
--execute-debug - Show debug output when executing computations.
--use-freezer - Force use of frozen computations for an incremental file render.
--cache - Cache execution output (--no-cache to prevent cache).
--cache-refresh - Force refresh of execution cache.
--no-clean - Do not clean project output-dir prior to render
--debug - Leave intermediate files in place after render.
pandoc-args... - Additional pandoc command line arguments.
--log <file> - Path to log file
--log-level <level> - Log level (debug, info, warning, error, critical)
--log-format <format> - Log format (plain, json-stream)
--quiet - Suppress console output.
--profile - Active project profile(s)
Commands:
help [command] - Show this help or the help of a sub-command.
Examples:
Render Markdown: quarto render document.qmd
quarto render document.qmd --to html
quarto render document.qmd --to pdf --toc
Render Notebook: quarto render notebook.ipynb
quarto render notebook.ipynb --to docx
quarto render notebook.ipynb --to pdf --toc
Render Project: quarto render
quarto render projdir
Render w/ Metadata: quarto render document.qmd -M echo:false
quarto render document.qmd -M code-fold:true
Render to Stdout: quarto render document.qmd --output -
The options used on this page are --to, --output, -P/--execute-param and --execute-params, --cache/--no-cache/--cache-refresh, --quiet, --debug and, for rendering without running the code, --no-execute. --execute-dir and --output-dir repeat the project options for one render, and --profile selects a project profile, the mechanism by which one project has a development and a production configuration.
R Markdown is still maintained
R Markdown is the older of the two systems, and it did not stop when Quarto appeared. The Quarto FAQ for R Markdown users says it plainly: R Markdown is not going away, it will continue to be actively supported, everything that is in it will continue to work, and there are no plans to deprecate it; it also says that no major feature initiatives are planned for R Markdown and its related packages, which keep being maintained (smaller improvements and bug fixes) for a long time, that some new features may exist only in Quarto, that there is no need to switch, and that switching is cheap, because most .Rmd files render unmodified with Quarto. The facts agree with that: rmarkdown 2.32 was published on CRAN on 2026-09-01 and knitr 1.52 on 2026-09-06, and the rmarkdown NEWS for 2.32 lists fixes (LaTeX auxiliary files written to the output directory, a fix for intermediates_dir, render() in a forked cluster), a new minimum Pandoc of 2.8 and a change of the licence to MIT. The R Markdown ecosystem (bookdown for books, blogdown for sites, xaringan for slides, flexdashboard for dashboards, distill for articles) is the part that Quarto replaces with built-in features; those packages are separate and none was run on this page. All but distill had a CRAN release in the last year (distill’s last release is from 2023), as the CRAN pages show. A rule of thumb that follows from this: keep the documents you have as they are, and start a new project in Quarto when it needs a feature that only Quarto has, such as its projects, its cross-references or Typst.
The R Markdown twin of the report differs from report.qmd only in syntax. The header uses output: with the function html_document and its option toc: true; the options of the chunks are in the header line ({r setup, include=FALSE}, {r months-plot, fig.cap="Loans per month.", fig.width=6, fig.height=3}); the echo = FALSE default comes from knitr::opts_chunk$set() in the setup chunk; the inline code is the knitr form; the caption of the table is the caption argument of kable(); and there are no cross references, because that feature belongs to Quarto and to the bookdown package.
---
title: "Shelf monthly report"
output:
html_document:
toc: true
params:
since: "2026-01"
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE)
books <- read.csv("books.csv")
loans <- read.csv("loans.csv", colClasses = "character")
loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
shelf_today <- as.Date("2026-06-30")
loan_days <- 21L
loans$month <- format(loans$loan_date, "%Y-%m")
recent <- loans[!is.na(loans$month) & loans$month >= params$since, ]
```
This report covers the loans made from `r params$since` to `r format(shelf_today)`:
`r nrow(recent)` loans of the `r nrow(books)` books in the catalogue.
## Loans per month
```{r months-table}
per_month <- as.data.frame(table(recent$month))
knitr::kable(per_month, col.names = c("Month", "Loans"),
caption = "Loans per month.")
```
```{r months-plot, fig.cap="Loans per month.", fig.width=6, fig.height=3}
barplot(table(recent$month), ylab = "Loans", col = "grey60")
```
## Overdue loans per member
A loan is overdue when it was out for more than `r loan_days` days. A loan
that has not been returned counts up to `r format(shelf_today)`.
```{r overdue, echo=TRUE}
end <- recent$return_date
end[is.na(end)] <- shelf_today
recent$days_out <- as.integer(end - recent$loan_date)
overdue <- recent[recent$days_out > loan_days, ]
overdue$member <- tools::toTitleCase(tolower(trimws(overdue$member)))
by_member <- as.data.frame(
sort(table(overdue$member[nzchar(overdue$member)]), decreasing = TRUE)
)
knitr::kable(by_member, col.names = c("Member", "Overdue loans"))
```
In total `r nrow(overdue)` of the `r nrow(recent)` loans were overdue.
An R Markdown document is rendered with rmarkdown::render(), from a script or the console. The script renders the twin to report-rmd.html, so that it does not overwrite the Quarto report, and checks that the file exists. quiet = TRUE hides the Pandoc command line and the progress bar.
out <- rmarkdown::render("report.Rmd", output_file = "report-rmd.html",
quiet = TRUE)
basename(out)
#> [1] "report-rmd.html"
file.exists("report-rmd.html")
#> [1] TRUE
Source: R Markdown documentation: render
The arguments of render() are the knobs of the system, and printing them is the quickest reference. The script also reports the version of Pandoc that rmarkdown found, and reads back what the document says about itself with yaml_front_matter() and all_output_formats().
names(formals(rmarkdown::render))
#> [1] "input" "output_format" "output_file"
#> [4] "output_dir" "output_options" "output_yaml"
#> [7] "intermediates_dir" "knit_root_dir" "runtime"
#> [10] "clean" "params" "knit_meta"
#> [13] "envir" "run_pandoc" "quiet"
#> [16] "encoding"
rmarkdown::pandoc_version()
#> [1] ‘3.1.3’
rmarkdown::yaml_front_matter("report.Rmd")
#> $title
#> [1] "Shelf monthly report"
#>
#> $output
#> $output$html_document
#> $output$html_document$toc
#> [1] TRUE
#>
#>
#>
#> $params
#> $params$since
#> [1] "2026-01"
#>
#>
rmarkdown::all_output_formats("report.Rmd")
#> [1] "html_document"
Source: R Markdown documentation: render
The sixteen arguments include output_format (a name such as "word_document" or an object such as html_document(toc = TRUE)), output_file and output_dir (the file name and the directory of the result), output_options (a list that overrides the format’s options), params, envir (the environment the chunks are evaluated in, parent.frame() by default, as discussed above), knit_root_dir (the working directory to knit in; by default the directory of the document), intermediates_dir, clean, run_pandoc and quiet. The Pandoc that rmarkdown used is 3.1.3, the system’s, and not the 3.10.0 of Quarto: the two systems can differ in small details of the HTML or Word output for the same Markdown, and a document that is rendered by both should be compared. The functions yaml_front_matter() and all_output_formats() return the header as a list, with the parameter default and the toc option, and the vector of the formats the document asks for.
Formats with rmarkdown
R Markdown has an output function per format; render() takes either its name or the object. The script renders the twin to Word and to GitHub Markdown, and tries PDF; the third call is wrapped in tryCatch() because it fails here:
rmd_to <- function(format, file) {
out <- rmarkdown::render("report.Rmd", output_format = format,
output_file = file, quiet = TRUE)
basename(out)
}
rmd_to("word_document", "report-rmd.docx")
#> [1] "report-rmd.docx"
rmd_to(rmarkdown::github_document(html_preview = FALSE), "report-rmd.md")
#> [1] "report-rmd.md"
tryCatch(
rmd_to("pdf_document", "report-rmd.pdf"),
error = \(e) conditionMessage(e)
)
#> Warning in system2(..., stdout = if (use_file_stdout()) f1 else FALSE, stderr = f2) :
#> error in running command
#> ! sh: 1: pdflatex: not found
#>
#> [1] "LaTeX failed to compile report-rmd.tex. See https://yihui.org/tinytex/r/#debugging for debugging tips. See report-rmd.log for more info."
The first two formats are written by Pandoc without TeX, and the files are report-rmd.docx and report-rmd.md. The third fails for the same reason as Quarto’s PDF: there is no pdflatex. The Warning lines are printed by R as it happens (the script ran with warnings printed immediately), come before the value of the call, and are the failure the script demonstrates, not a problem with the script: rmarkdown ran pdflatex and the shell reported that it was not found. The error message names the TeX file it was compiling and points to the log for details; report-rmd.tex and report-rmd.log are left in the directory. The value "word_document" is a format name and github_document(html_preview = FALSE) is an object with options; both are valid values of output_format.
Rendering an R Markdown file with Quarto
Quarto renders .Rmd files too, which is how a collection of old documents can be moved without rewriting them. The command is the same, with the .Rmd as the input. It uses the knitr engine, treats the file as a Quarto document, and the output: key of the header, which belongs to R Markdown, is ignored; the format is the default (HTML). The test directory had only the twin and the data:
quarto render report.Rmd
ls
pandoc
to: html
output-file: report.html
standalone: true
section-divs: true
html-math-method: mathjax
wrap: none
default-image-extension: png
metadata
document-css: false
link-citations: true
date-format: long
lang: en
title: Shelf monthly report
Output created: report.html
books.csv
loans.csv
report_files
report.html
report.Rmd
(The transcript starts at the Pandoc block, followed by the listing.) The Pandoc block has no toc: true: the option of html_document was not applied, because Quarto does not read output:. This is the practical difference to plan for when moving documents: the chunk code runs unchanged, and the format options are written again under format:. Moving a document for good is mostly three edits: rename .Rmd to .qmd; replace output: by format: with the Quarto option names (the Using R article shows the pair of headers side by side, with number_sections becoming number-sections, and self_contained has the counterpart embed-resources); and, optionally, move the chunk options into the hash-pipe form. knitr has a helper for the last step, knitr::convert_chunk_header(), which rewrites the chunk headers of a file. The script reads the headers of the twin, converts them to the YAML form of the hash-pipe options (type = "yaml") in a temporary file, and shows the result, including the renaming of fig.cap to fig-cap:
rmd <- readLines("report.Rmd")
grep("^```\\{r", rmd, value = TRUE)
#> [1] "```{r setup, include=FALSE}"
#> [2] "```{r months-table}"
#> [3] "```{r months-plot, fig.cap=\"Loans per month.\", fig.width=6, fig.height=3}"
#> [4] "```{r overdue, echo=TRUE}"
out <- tempfile(fileext = ".Rmd")
knitr::convert_chunk_header("report.Rmd", output = out, type = "yaml")
hashpipe <- readLines(out)
grep("^```\\{r", hashpipe, value = TRUE)
#> [1] "```{r}" "```{r}" "```{r}" "```{r}"
hashpipe[grep("^#\\|", hashpipe)]
#> [1] "#| label: setup" "#| include: false"
#> [3] "#| label: months-table" "#| label: months-plot"
#> [5] "#| fig-cap: Loans per month." "#| fig-width: 6"
#> [7] "#| fig-height: 3" "#| label: overdue"
#> [9] "#| echo: true"
Extracting the code
The inverse of weaving is tangling: extracting the R code of a document into a script. knitr::purl() does it, as Stangle() does for Sweave files. It is the tool for running the code of a report without rendering it, for debugging it with the usual tools of Debugging, and for linting it. The script tangles the twin with the default level of documentation, which adds a comment with the chunk header before each chunk, and the Quarto document, where the hash-pipe options are kept as comments (since knitr 1.44). Only the first lines of each are shown.
rmd <- knitr::purl("report.Rmd", output = tempfile(fileext = ".R"),
quiet = TRUE)
cat(head(readLines(rmd), 14), sep = "\n")
#> params <-
#> list(since = "2026-01")
#>
#> ## ----setup, include=FALSE-----------------------------------------------------
#> knitr::opts_chunk$set(echo = FALSE)
#> books <- read.csv("books.csv")
#> loans <- read.csv("loans.csv", colClasses = "character")
#> loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
#> loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
#> shelf_today <- as.Date("2026-06-30")
#> loan_days <- 21L
#> loans$month <- format(loans$loan_date, "%Y-%m")
#> recent <- loans[!is.na(loans$month) & loans$month >= params$since, ]
#>
qmd <- knitr::purl("report.qmd", output = tempfile(fileext = ".R"),
quiet = TRUE)
cat(head(readLines(qmd), 12), sep = "\n")
#> params <-
#> list(since = "2026-01")
#>
#> ## -----------------------------------------------------------------------------
#> #| label: load
#> #| include: false
#> books <- read.csv("books.csv")
#> loans <- read.csv("loans.csv", colClasses = "character")
#> loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
#> loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
#> shelf_today <- as.Date("2026-06-30")
#> loan_days <- 21L
Two details deserve attention. The tangled script starts with params <- and the default values of the parameters, so the script runs on its own with the defaults; and inline expressions are not included in the script, because the option knitr.purl.inline that includes them is off by default, according to the help page of knit(). A chunk can opt out with purl = FALSE (Quarto: purl: false). With documentation = 0 only the code is written; with documentation = 2 the text of the document is written as roxygen comments, which is a way to turn a report into a documented script.
Sweave, the older form
Sweave is the original weaving system of R, and it ships with R, in the utils package. It processes .Rnw files, which are LaTeX documents with R code, and it is the format that the manual section on package vignettes is written around. A chunk begins with <<label, options>>= at the start of a line and ends with @ at the start of a line; an inline expression is \Sexpr{...}. The file for the shelf is the smallest example that has a chunk with its code and result, an inline expression and a chunk with echo=FALSE:
\documentclass{article}
\begin{document}
<<load>>=
loans <- read.csv("loans.csv", colClasses = "character")
nrow(loans)
@
The shelf recorded \Sexpr{nrow(loans)} loans.
<<years, echo=FALSE>>=
table(substr(loans$loan_date, 1, 4))
@
\end{document}
Sweave() runs the code and writes a .tex file; it does not run LaTeX, which is why it works without a TeX installation. Stangle() writes the R code. The script writes both to temporary files and prints them (quiet = TRUE hides the messages that name the files):
tex <- tempfile(fileext = ".tex")
utils::Sweave("shelf.Rnw", output = tex, quiet = TRUE)
cat(readLines(tex), sep = "\n")
#> \documentclass{article}
#> \usepackage{Sweave}
#> \begin{document}
#>
#> \begin{Schunk}
#> \begin{Sinput}
#> > loans <- read.csv("loans.csv", colClasses = "character")
#> > nrow(loans)
#> \end{Sinput}
#> \begin{Soutput}
#> [1] 60
#> \end{Soutput}
#> \end{Schunk}
#>
#> The shelf recorded 60 loans.
#>
#> \begin{Schunk}
#> \begin{Soutput}
#> 2025 2026
#> 43 17
#> \end{Soutput}
#> \end{Schunk}
#>
#> \end{document}
code <- tempfile(fileext = ".R")
utils::Stangle("shelf.Rnw", output = code, quiet = TRUE)
cat(readLines(code), sep = "\n")
#> ### R code from vignette source 'shelf.Rnw'
#>
#> ###################################################
#> ### code chunk number 1: load
#> ###################################################
#> loans <- read.csv("loans.csv", colClasses = "character")
#> nrow(loans)
#>
#>
#> ###################################################
#> ### code chunk number 2: years
#> ###################################################
#> table(substr(loans$loan_date, 1, 4))
Source: R documentation: Sweave
The .tex file has the LaTeX package Sweave loaded (\usepackage{Sweave} inserted after the document class), the code in Sinput environments with the prompts >, the output in Soutput, and the whole in a Schunk. The tangled script is plain R code with a comment for each chunk. Both belong to the RweaveLatex driver, the default driver for weaving, and Rtangle, the one for tangling; the help page RweaveLatex documents the chunk options: engine, echo, keep.source, eval, results (verbatim, tex or hide), print, term, split, strip.white, prefix, include, and, for figures, fig (only one figure per chunk can be produced this way), pdf, png, eps, jpeg, width and height. The Sweave help page adds that Stangle() is a wrapper to Sweave() with a different default driver, and that code inside \Sexpr{} is ignored by it, and that the environment variable SWEAVE_OPTIONS can override the initial options of the driver. Sweave has fewer options and fewer formats than knitr: the default driver writes LaTeX only. The command-line forms are R CMD Sweave and R CMD Stangle, which write the files into the current directory:
R CMD Sweave shelf.Rnw
R CMD Stangle shelf.Rnw
ls
Source: R documentation: Sweave
Output file: shelf.tex
Output/used file: shelf.R
books.csv
loans.csv
shelf.R
shelf.Rnw
shelf.tex
Source: R documentation: Sweave
(The output of the three commands, in order.) R CMD Sweave wrote shelf.tex, R CMD Stangle wrote shelf.R, and the data file and the source are as they were. To go on to a PDF, R CMD Sweave --pdf runs LaTeX on the result, which needs TeX; without it:
R CMD Sweave --pdf shelf.Rnw
echo $?
Source: R documentation: Sweave
Warning in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
texi2dvi script/program not available, using emulation
Error in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet, :
pdflatex is not available
Calls: <Anonymous> -> do.call -> <Anonymous> -> texi2pdf -> texi2dvi
Execution halted
1
Source: R documentation: Sweave
The first line is a warning that the texi2dvi program was not found and R used its emulation, and the error says that pdflatex is not available; the exit status is 1. The usage text of the two commands, as printed by R, lists their options; those of R CMD Sweave are --driver, --engine, --encoding, --clean, --options, --pdf and --compact:
Usage: R CMD Sweave [options] file
A front-end for Sweave and other vignette engines, via buildVignette()
Options:
-h, --help print this help message and exit
-v, --version print version info and exit
--driver=name use named Sweave driver
--engine=pkg::engine use named vignette engine
--encoding=enc default encoding 'enc' for file
--clean corresponds to --clean=default
--clean= remove some of the files created in the working directory:
"default" removes intermediate files;
"keepOuts" keeps e.g. *.tex even when PDF is produced
--options= comma-separated list of Sweave/engine options
--pdf convert to PDF document
--compact= try to compact PDF document:
"no" (default), "qpdf", "gs", "gs+qpdf", "both"
--compact same as --compact=qpdf
Usage: R CMD Stangle file
A front-end for Stangle and other vignette engines
Options:
-h, --help print this help message and exit
-v, --version print version info and exit
--engine=pkg::engine use named vignette engine
--encoding=enc assume encoding 'enc' for file
--options= comma-separated list of Stangle options
(The two help texts, one after the other; the "Report bugs at" line that ends each of them is omitted.) The options --engine and the text "and other vignette engines" show that the two commands are no longer only for Sweave: since R 3.1.0 they process vignettes of any engine registered by a package, and tools::buildVignette() does the same from R, according to the R 3.x NEWS. knitr can process an .Rnw file too, with its own LaTeX output, and knit() was used for this above. The script runs knit() on the same file and prints the part of the result after the preamble, which shows the way knitr marks up the code and the output (the long preamble of definitions for the syntax highlighting is left out):
tex <- tempfile(fileext = ".tex")
invisible(knitr::knit("shelf.Rnw", output = tex, quiet = TRUE))
lines <- readLines(tex)
start <- grep("begin{document}", lines, fixed = TRUE)
cat(lines[start:length(lines)], sep = "\n")
#> \begin{document}
#>
#> \begin{knitrout}
#> \definecolor{shadecolor}{rgb}{0.969, 0.969, 0.969}\color{fgcolor}\begin{kframe}
#> \begin{alltt}
#> \hldef{loans} \hlkwb{<-} \hlkwd{read.csv}\hldef{(}\hlsng{"loans.csv"}\hldef{,} \hlkwc{colClasses} \hldef{=} \hlsng{"character"}\hldef{)}
#> \hlkwd{nrow}\hldef{(loans)}
#> \end{alltt}
#> \begin{verbatim}
#> ## [1] 60
#> \end{verbatim}
#> \end{kframe}
#> \end{knitrout}
#>
#> The shelf recorded 60 loans.
#>
#> \begin{knitrout}
#> \definecolor{shadecolor}{rgb}{0.969, 0.969, 0.969}\color{fgcolor}\begin{kframe}
#> \begin{verbatim}
#> ##
#> ## 2025 2026
#> ## 43 17
#> \end{verbatim}
#> \end{kframe}
#> \end{knitrout}
#>
#> \end{document}
The result has knitrout and kframe environments instead of Schunk, the code is highlighted by LaTeX macros (\hlkwd{read.csv}), and the output lines have the knitr prefix ##. That is the difference in miniature: the document is the same, the engine is knitr, and the options and the look come from knitr. The cost of Sweave and .Rnw is that everything, including the text, is LaTeX, so a web page, a Word file or slides need a different source. Sweave is still supported and is still the format that the manual Writing R Extensions describes, with an engine utils::Sweave that needs no VignetteBuilder field; other engines are named by the package that provides them, and Non-Sweave vignettes is the section that explains how.
Vignettes and the quarto package
A package vignette is a report that is a part of a package, and it is built when the package is built. The shelf package has one, vignettes/shelf.Rmd, which is an R Markdown document with the header lines that R reads (%\VignetteEngine{knitr::rmarkdown}) and the field VignetteBuilder: knitr in DESCRIPTION; Documentation with Rd, roxygen2 and Vignettes shows the file and builds it, and Creating Packages the package. A vignette in Quarto uses VignetteBuilder: quarto in DESCRIPTION, quarto in Suggests and the engine %\VignetteEngine{quarto::html} in the .qmd file; the quarto R package, which provides the engine and quarto_render(), is not installed in the image, so that route is described and not run (the documentation page shows the Quarto program rendering such a file by hand). The quarto package version on CRAN at the time of writing is 1.5.1.
For a vignette the choice between the systems is practical. knitr with R Markdown has no dependency outside R and Pandoc, and is the default of the package tooling. Quarto vignettes need the Quarto program on every machine that builds the package, and are best kept for a package whose other documentation is already in Quarto.
Pitfalls
-
Paths. Every path in a chunk is relative to the directory of the document (or to
execute-dirin a project, or toroot.dir). A report that works from the project directory and fails from another one is reading a file by a path that depends on where it was started; keep the data next to the document or build the path from the project, and do not callsetwdin a chunk. -
The clock. A report that asks for
Sys.DateorSys.timeis a different report every day, so it cannot be compared with last month’s or reproduced from the same inputs. Put the date of the report in a parameter, as the report putsshelf_todayat the top, and show it in the text. -
Randomness. A chunk that simulates or samples gives different numbers at each render. Call
set.seed()in the first chunk, and remember that the sequence depends on the order of the chunks, and on the RNG kind, which Distributions and Random Numbers explains. -
The session. Under
rmarkdown::render()with the defaultenvir, the document sees the objects of the calling environment. Render from a clean process (a script run withRscript, or Quarto), and withenvir = new.env()when you must call it from a session. -
The defaults of
echo,errorandfig-widthdiffer between knitr’sknit(), R Markdown, Quarto and the output format (revealjs shows no code by default). Set what you rely on in the header. -
Inline code in the YAML header does not work (the Quarto documentation says so), and Markdown in the result of the Quarto inline form is escaped; the knitr form keeps it.
-
A figure and a table that are
fig-andtbl-labelled are numbered by Quarto; R Markdown has no such cross references of its own (the bookdown package adds them), so the same labels only name the chunks. -
Large outputs. An HTML file with embedded resources is one large file, and a cache directory can grow quickly; keep
*_cacheand.quartoout of version control, and commit_freezedeliberately. -
Dependencies. A document that loads a package that is not installed fails at that chunk. Declare the packages with
library()in the first chunk, so that the failure is early and clear, and pin the versions of a long-lived report with Reproducible Environments with renv. -
Secrets. A report that is rendered on a server needs credentials for its data; take them from environment variables (
Sys.getenv()), never from the document, because the document and its HTML are often published.
Version notes
| Version | Fact |
|---|---|
R 3.1.0 |
|
R 3.1.1 |
|
R 4.2.0 |
The default |
R 4.6.0 |
The Sweave driver |
knitr 1.35 |
Chunk options can be written inside the chunk after |
knitr 1.44 |
|
knitr 1.50 |
A warning is given when a chunk option is repeated in the chunk header and in the pipe comments. |
knitr 1.51 |
Pipe comments must use the comment character of the language of the chunk, which is why SQL chunks use |
knitr 1.52 (2026-09-06) |
The version used on this page. New in it: the chunk option |
rmarkdown 2.32 (2026-09-01) |
The version used on this page. Pandoc 2.8 is the minimum; LaTeX auxiliary files are written to the output directory; relicensed MIT. The NEWS file on GitHub has a 2.33 section, not released. |
Quarto 1.4 (January 2024) |
The release notes list support for executing inline expressions (the single |
Quarto 1.7 |
The |
Quarto 1.10.19 (2026-10-06) |
The version used on this page; the current release at the time of writing. The download page lists, as the highlight of the 1.10 series, a new |
R |
Nothing in R 4.x changes how documents are woven apart from the Sweave items above. The R side of both systems works the same on R 4.1 and later. |
Source: R NEWS: Changes in R 3.1.0
Source: knitr NEWS
Source: rmarkdown NEWS
The R 4.2.0 and 4.6.0 entries come from the NEWS of those releases and are about Sweave only. The Quarto and knitr facts come from their release notes and NEWS files, read on 2026-10-21; the Quarto release dates are those of the GitHub releases (1.4.549 on 2024-01-24, 1.10.19 on 2026-10-06, 1.11.6 on 2026-10-14). Posit’s tools change faster than R itself; anything on this page that depends on a Quarto or knitr feature should be rechecked against the versions on the reader’s machine, which quarto --version and packageVersion("knitr") report.
Where older tutorials differ
| Older tutorial writes | Current practice | Why | Since |
|---|---|---|---|
|
|
One source gives HTML, Word, slides and PDF; Sweave writes LaTeX only |
- |
|
|
|
- |
|
|
Quarto is Posit’s multi-language system; R Markdown remains maintained |
Quarto 1.0, 2022 |
|
The hash-pipe form |
Long options and captions are easier to write, and the documents are portable between Quarto and R Markdown |
knitr 1.35 |
|
No cache for a short report; |
The cache is keyed by the chunk’s code, not by the data or the other chunks, and goes stale quietly |
always |
|
|
The call shows where the function comes from and does not depend on what is attached |
always |
A call to |
Relative paths from the document’s directory, or |
The help page of |
always |
Only the Knit button of RStudio, with no command that can be put in a script |
|
A report that can only be made by a person clicking is not reproducible |
always |
Inline code written only as backtick-r |
Both forms; |
One syntax for all engines in Quarto; the old form is still valid |
Quarto 1.4 |
Packages for each kind of output: bookdown, blogdown, xaringan, distill, flexdashboard |
Quarto books, websites, revealjs and dashboards for new projects |
Built into Quarto; the packages still work (all but distill had a CRAN release in the last year) |
Quarto 1.x |
|
|
Typst needs no TeX installation |
Quarto 1.4 |
|
|
The same idea, with Quarto’s name; |
Quarto 1.0 |
Where to go next
-
Shiny, plumber and Web APIs for interactive documents (
runtime: shiny, Quarto’s Shiny support) and dashboards that need a server, which are the next step after a static report. -
Python Interop with reticulate for
{python}chunks in a knitr document, interpreters and environments, and the conversion of data frames. -
Documentation with Rd, roxygen2 and Vignettes for vignettes,
VignetteBuilderand theshelfvignette, and pkgdown websites that render documents like these. -
Scripts and the Command Line for running the rendering in a script, exit statuses, arguments and scheduling.
-
ggplot2 and the Grammar of Graphics and Base Graphics and Devices for the figures that a chunk draws, and the devices behind
fig-format. -
Reading and Writing Text Data for the CSV files that the report reads.
-
Creating Packages and Debugging for the package side of a report and for debugging the code that
purl()extracts. -
R Reference for the whole section, and Docker for a container in which a report is rendered with a fixed R, Quarto and package set.
-
Community books on this subject, which this page did not use as a source: the R Markdown guide and cookbook by Yihui Xie and co-authors, and the chapter on Quarto in R for Data Science.
References
-
Quarto documentation: Using R
-
Quarto documentation: Execution Options
-
Quarto documentation: Code Cells: Knitr
-
Quarto documentation: Parameters
-
Quarto documentation: Inline Code
-
Quarto documentation: Project Basics
-
Quarto documentation: Managing Execution
-
Quarto documentation: Project Options
-
Quarto documentation: All Formats
-
Quarto documentation: HTML Options
-
Quarto documentation: MS Word
-
Quarto documentation: GitHub Markdown
-
Quarto documentation: Revealjs
-
Quarto documentation: PDF Engine
-
Quarto documentation: Typst Basics
-
Quarto documentation: FAQ for R Markdown Users
-
Quarto documentation: Quarto 1.4 release notes
-
Quarto documentation: Download Quarto, and the Quarto releases on GitHub
-
quarto package documentation: quarto_render
-
R Markdown documentation: lessons Introduction, Parameters and Output Formats
-
R Markdown documentation: render
-
R Markdown documentation: html_document
-
R Markdown documentation: word_document
-
R Markdown documentation: pdf_document
-
R Markdown documentation: github_document
-
R Markdown documentation: knit_params_ask
-
knitr documentation: Chunk options and package options
-
knitr documentation: Language engines
-
knitr package documentation: knit and purl
-
knitr package documentation: opts_chunk
-
knitr package documentation: convert_chunk_header
-
knitr package documentation: knit2html
-
Writing R Extensions: Writing package vignettes; Non-Sweave vignettes
-
R documentation: Sweave and Stangle; RweaveLatex
-
R documentation: system2; unzip
-
R NEWS and R 3.x NEWS