R Tidyverse skill for Codex
R analysis that reruns from a clean session. Codex writes R the way a careful analyst does: renv pins packages, readr declares column types and reports parse problems, dplyr joins state their expected relationship so duplicates raise errors, and ggplot2 figures are saved at explicit sizes. Reports are parameterised Quarto documents rendered with one command from a clean session.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- r-tidyverse Installs to ~/.agents/skills/r-tidyverse/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install r-tidyverse The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project r-tidyverse Installs to .agents/skills/r-tidyverse/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/r-tidyverse.zipunzip -o r-tidyverse.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) r-tidyverse PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $r-tidyverse, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes reproducible R analysis with the tidyverse - readr imports with explicit col_types, dplyr 1.1 verbs with .by and join_by, tidyr pivots, purrr iteration, ggplot2 charts saved at fixed sizes, renv lockfiles, and parameterised Quarto reports rendered from the command line. Use when working in an R project or R Markdown/Quarto report, translating base R or spreadsheet logic to tidyverse, fixing join or grouping bugs in dplyr, or producing ggplot2 figures and HTML/PDF reports that must rerun identically.
Use it when
- Working in an R project, R Markdown or Quarto report
- Translating base R or spreadsheet logic into tidyverse code
- dplyr joins, grouping or imports produce surprising results
- ggplot2 figures or reports must rerun identically for others
Use something else when
- The analysis is in Python dataframes: use pandas-wrangling
- You need typeset PDF documents rather than analysis reports: use latex-documents
How to check the result
From a clean session, renv::restore and quarto render succeed, readr problems() is empty or explained, and headline numbers match a known total.
The mistake it prevents
Letting read_csv guess types so ID columns become numbers, then joining on them and silently losing or duplicating rows.
Good R analysis reruns from a clean session with renv::restore() and one render
command, declares column types on import, makes joins state their expected
relationship so silent row multiplication fails loudly, and saves figures at explicit
sizes. Results never depend on objects left in someone’s global environment.
Inspect first
- Read
AGENTS.md,DESCRIPTION(if it is a package),renv.lock,_quarto.ymland.Rprofile. Note the R version (R --version) and whether renv is active. - Check for a project file (
*.Rproj) orhereusage; paths should resolve from the project root withhere::here("data", "orders.csv"), neversetwd(). - Run existing scripts with
Rscriptfrom a clean session, not inside an IDE session that already holds objects:
Rscript -e 'renv::status()'
Rscript analysis/01_import.R
quarto render reports/monthly.qmd
Installing packages (renv::restore(), install.packages) needs network; in a Codex
sandbox with network off, ask for approval or work with the installed library.
Environment with renv
renv::init() # once: creates renv.lock, project library, .Rprofile hook
renv::install("dplyr")
renv::snapshot() # record versions after adding packages
renv::restore() # collaborators and CI reproduce the library
Commit renv.lock, renv/activate.R and .Rprofile; never commit renv/library/.
Import with explicit types
library(tidyverse)
orders <- read_csv(
here::here("data", "orders.csv"),
col_types = cols(
order_id = col_character(),
customer_id = col_character(),
zip = col_character(), # keep leading zeros
amount = col_double(),
order_date = col_date(format = "%Y-%m-%d"),
channel = col_factor(levels = c("web", "app", "store")),
.default = col_character()
),
na = c("", "NA", "NULL")
)
problems(orders) # parsing failures with row and column; must be empty or explained
European files: read_csv2() or locale = locale(decimal_mark = ",", grouping_mark = ".").
Excel: readxl::read_excel(path, sheet = "Data", col_types = "text"), then convert.
dplyr 1.1 idioms
summary <- orders |>
filter(order_date >= as.Date("2025-01-01")) |>
mutate(month = floor_date(order_date, "month")) |> # lubridate, part of tidyverse 2.0
summarise(
orders = n(),
revenue = sum(amount, na.rm = TRUE),
aov = revenue / orders,
.by = c(month, channel) # per-operation grouping, no ungroup()
) |>
arrange(month, desc(revenue))
- Prefer
.byovergroup_by()+ungroup(): grouping cannot leak into later steps. - Use
across()for the same operation on many columns:summarise(across(c(amount, qty), \(x) sum(x, na.rm = TRUE)), .by = channel). case_when()for multi-branch logic; the final.default =catches the rest.- Native pipe
|>and lambda\(x)need R 4.1+; follow the project’s existing choice if it uses%>%.
Joins that fail loudly
orders_cust <- orders |>
inner_join(
customers,
by = join_by(customer_id),
relationship = "many-to-one", # error if customers has duplicate ids
unmatched = c("error", "drop") # error if an order has no customer; customers without orders are fine
)
# rolling / inequality joins: price valid at the order date
priced <- orders |>
left_join(prices, by = join_by(product_id, closest(order_date >= valid_from)))
unmatched only guards rows a join could drop: in a left_join it checks y, not x,
so use inner_join with c("error", "drop") to require a match for every left row.
Always compare nrow() before and after a join when not using relationship. Use
anti_join() to list unmatched keys and report them.
Reshaping and iteration
long <- wide |> pivot_longer(cols = starts_with("q"), names_to = "quarter", values_to = "sales")
wide <- long |> pivot_wider(names_from = quarter, values_from = sales, values_fill = 0)
files <- fs::dir_ls(here::here("data", "monthly"), glob = "*.csv")
all_months <- files |>
set_names(basename) |>
map(\(f) read_csv(f, col_types = cols(.default = col_character()))) |>
list_rbind(names_to = "source_file")
list_rbind() (purrr 1.0) replaces map_dfr().
ggplot2
p <- summary |>
ggplot(aes(month, revenue, colour = channel)) +
geom_line(linewidth = 0.9) +
scale_y_continuous(labels = scales::label_currency(prefix = "EUR ", scale_cut = scales::cut_short_scale())) +
scale_colour_viridis_d(end = 0.85) +
labs(title = "App revenue overtook web in June", x = NULL, y = "Monthly revenue",
colour = NULL, caption = "Source: orders export, 2025-01 to 2025-09") +
theme_minimal(base_size = 11) +
theme(legend.position = "top", plot.title.position = "plot")
ggsave(here::here("figures", "revenue_by_channel.png"), p, width = 8, height = 4.5, dpi = 200)
ggsave(here::here("figures", "revenue_by_channel.pdf"), p, width = 7, height = 3.5)
- Use
linewidthfor lines (ggplot2 3.4+);sizeis for points and text. - Always pass
width,heightanddpitoggsave; defaults depend on the open device. - Facets for many groups:
facet_wrap(vars(region), ncol = 3).
Quarto reports
---
title: "Monthly commerce report"
format:
html:
embed-resources: true
code-fold: true
execute:
echo: false
warning: false
params:
month: "2025-09-01"
region: "EU"
---
#| label: setup
month <- as.Date(params$month)
quarto render reports/monthly.qmd -P month:2025-10-01 -P region:US --output monthly-US-2025-10.html
paramswork with the knitr engine; refer to them asparams$month.embed-resources: truemakes a single shareable HTML file.- Use
freeze: autoin_quarto.ymlfor projects so unchanged documents are not re-executed; clear_freeze/when data changes. - Sharing or publishing a report (
quarto publish) is outward-facing; confirm the target with the user first.
Gotchas
read_csvguesses types from the first 1000 rows; a column of IDs becomes double and loses leading zeros. Declarecol_types.NApropagates:sum(x)isNAif any value is missing; decide onna.rmper metric.- Factors in joins and
bind_rowswith different levels coerce with warnings; convert to character for joining. group_by()withoutungroup()makes latermutate()calls compute per group.- Dates from Excel arrive as numbers; convert with
as.Date(x, origin = "1899-12-30"). set.seed()once at the top for anything random, and record it.
Verification
Rscript -e 'renv::restore(prompt = FALSE)'
Rscript -e 'testthat::test_dir("tests/testthat")' # if helper functions are tested
quarto render reports/monthly.qmd
problems()empty after every import, or each issue explained.- Row counts logged before and after joins and filters.
- Headline numbers in the rendered report match a known total.
lintr::lint_dir()andstyler::style_dir()if the project uses them.
Delivery template
**Report:** reports/monthly.qmd -> monthly-EU-2025-09.html (self-contained)
**Run:** renv::restore(); quarto render reports/monthly.qmd -P month:2025-09-01 -P region:EU
**Data:** data/orders.csv (184,201 rows, 0 parse problems)
**Checks:** joins declare relationship and unmatched handling; revenue 1.28M EUR matches finance
**Figures:** figures/revenue_by_channel.png (8 x 4.5 in, 200 dpi) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "R Tidyverse"
short_description: "R analysis that reruns from a clean session"
default_prompt: "$r-tidyverse Turn this R script into a parameterised Quarto report we can render per region" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.