brapiR2 provides pipe-friendly, stateless access to BrAPI v2 servers. Unlike stateful clients where you navigate by setting a “current” program or trial, brapiR2 functions are independent — you pass a connection object and get a tibble back. This makes them ideal for scripting, pipelines, and building downstream tools.
Installation
brapiR2 is on rOpenSci’s R-universe, so
install.packages() works once that repository is added.
# Not run here: this would reinstall the package while building its own
# documentation.
install.packages(
"brapiR2",
repos = c("https://ropensci.r-universe.dev", "https://cloud.r-project.org")
)Connecting to a Server
The examples below run live against the public BrAPI test server, which requires no authentication.
library(brapiR2)
con <- brapi_connection("https://test-server.brapi.org")
con
#>
#> ── BrAPI Connection
#> • Server: <https://test-server.brapi.org>
#> • Version: v2
#> • Auth: ✗ no token
#> • Page size: 1000
#> • Timeout: 120s
#> • Cache: disabledEvery function takes con as its first argument. No
global state is modified.
Most real servers require credentials. See Authentication below for username/password login, OAuth 2.0, and bearer tokens, and Handling Credentials Safely for keeping them out of your scripts.
Exploring Programs and Trials
BrAPI organises breeding data as a nesting of four levels, and the calls below walk down it. A program is a long-running breeding effort with a continuing objective, such as a maize program developing mid-altitude tropical hybrids; it persists across decades. A trial groups experiments that share a purpose within that program, typically one season’s worth of work, such as the 2022 advanced yield trials. A study is a single trial grown at one location in one season, which is the level at which plots physically exist and measurements are actually taken. Below that sit observation units, the individual plots, plants or samples that carry the measurements themselves.
You narrow from one level to the next because each level’s identifier
is the filter for the one below it. In practice you rarely know a
studyDbId up front; you know you want last season’s yield
trials from a particular program, and you find your way down. Note that
what a breeder calls an “environment” in a multi-environment analysis
corresponds to a BrAPI study, not a location, since the same site in two
seasons is two environments.
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
# List all breeding programs
programs <- brapi_programs(con)
programs
#> # A tibble: 3 × 12
#> additionalInfo externalReferences abbreviation commonCropName documentationURL
#> <list> <list> <chr> <chr> <chr>
#> 1 <named list> <list [1]> P1 Tomatillo https://brapi.o…
#> 2 <named list> <list [1]> P2 Tomatillo https://brapi.o…
#> 3 <named list> <list [1]> P3 Paw Paw https://brapi.o…
#> # ℹ 7 more variables: leadPersonDbId <chr>, leadPersonName <chr>,
#> # objective <chr>, programName <chr>, programType <chr>,
#> # fundingInformation <chr>, programDbId <chr>
# List trials in the first program
trials <- brapi_trials(con, programDbId = programs$programDbId[1])
trials
#> # A tibble: 1 × 16
#> active additionalInfo commonCropName contacts datasetAuthorships
#> <lgl> <list> <chr> <list> <list>
#> 1 TRUE <named list [1]> Tomatillo <list [1]> <list [1]>
#> # ℹ 11 more variables: documentationURL <chr>, endDate <chr>,
#> # externalReferences <list>, programDbId <chr>, programName <chr>,
#> # publications <list>, startDate <chr>, trialDescription <chr>,
#> # trialName <chr>, trialPUI <chr>, trialDbId <chr>
# List studies within that trial
studies <- brapi_studies(con, trialDbId = trials$trialDbId[1])
studies
#> # A tibble: 1 × 29
#> additionalInfo externalReferences active commonCropName contacts
#> <list> <list> <lgl> <chr> <list>
#> 1 <named list [1]> <list [1]> TRUE Tomatillo <list [1]>
#> # ℹ 24 more variables: culturalPractices <chr>, dataLinks <list>,
#> # documentationURL <chr>, endDate <chr>, environmentParameters <list>,
#> # experimentalDesign <list>, growthFacility <list>, lastUpdate <list>,
#> # license <chr>, locationDbId <chr>, locationName <chr>,
#> # observationLevels <list>, observationUnitsDescription <chr>,
#> # seasons <list>, startDate <chr>, studyCode <chr>, studyDescription <chr>,
#> # studyName <chr>, studyPUI <chr>, studyType <chr>, trialDbId <chr>, …Fetching Phenotypic Data
The /observations endpoint returns data in long format,
one row per measurement, with the trait identified in a column and the
value in another. That is the right shape for transport and the wrong
shape for analysis: nearly every R modelling function expects one row
per experimental unit and one column per variable.
brapi_study_data() does that reshaping for you. It
fetches the study’s observations, pivots traits into columns, and joins
the germplasm and plot identifiers you need to link phenotypes back to
genotypes or to a field layout. Doing this by hand is not difficult, but
it is fiddly in ways that are easy to get subtly wrong, particularly
when an observation unit carries more than one value for the same trait.
Repeated measurements are kept as list-columns rather than silently
averaged or dropped, which is why the summary below unnests before
taking means. The function also falls back to fetching all observations
and filtering client-side when a server does not honour the
studyDbId filter, so it returns data on servers where a
direct brapi_observations() call would come back empty.
# Get analysis-ready wide format: one row per plot, one column per trait
data <- brapi_study_data(con, studies$studyDbId[1])
data
#> # A tibble: 2 × 6
#> observationUnitDbId observationUnitName germplasmDbId germplasmName studyDbId
#> <chr> <chr> <chr> <chr> <chr>
#> 1 observation_unit1 Plot 1 germplasm1 Tomatillo Fan… study1
#> 2 observation_unit2 Plot 2 germplasm2 Tomatillo Fan… study1
#> # ℹ 1 more variable: `Corn Stalk Height` <list>The public test server’s studies currently have no recorded
observations, so brapi_study_data() returns an empty tibble
and a warning rather than fabricated numbers — this is the function’s
real, documented behaviour when a study has no data, not a bug in the
example. Downstream code should check for this:
if (nrow(data) > 0) {
trait_cols <- setdiff(
names(data),
c(
"observationUnitDbId", "observationUnitName",
"germplasmDbId", "germplasmName", "studyDbId", "studyName"
)
)
# Some observation units have more than one recorded value for the same
# trait (repeated measurements), which brapi_study_data() keeps as a
# list-column. Unnest those into one row per observation before
# summarising, so mean() sees plain numbers either way.
data |>
tidyr::unnest_longer(dplyr::any_of(trait_cols)) |>
mutate(across(all_of(trait_cols), as.numeric)) |>
summarise(across(all_of(trait_cols), \(x) mean(x, na.rm = TRUE)))
} else {
cat("No observations available for this study on the public test server.\n")
}
#> # A tibble: 1 × 1
#> `Corn Stalk Height`
#> <dbl>
#> 1 25Fetching Genotypic Data
A variant set is a genotyping dataset: a defined panel of markers scored on a defined set of samples, usually corresponding to one genotyping run or one platform. A server may hold several, and they will differ in marker density, in which individuals were scored, and often in reference genome build, so picking the right one matters.
The dosage matrix is the form nearly every downstream analysis wants.
Each cell counts how many copies of the alternate allele an individual
carries at that marker, so for diploid maize the values are 0 for
homozygous reference, 1 for heterozygous, 2 for homozygous alternate,
and NA where the call failed. For polyploids the range
extends to the ploidy level, which is why the function returns counts
rather than a genotype string. Representing genotypes numerically this
way is what allows them to enter a mixed model as a design matrix:
additive marker effects are then simply regression coefficients on
allele count.
# List available variant sets (genotyping datasets)
vsets <- brapi_variant_sets(con)
vsets
#> # A tibble: 1 × 11
#> additionalInfo externalReferences analysis availableFormats callSetCount
#> <list> <list> <list> <list> <int>
#> 1 <named list [1]> <list [1]> <list [1]> <list [3]> 13
#> # ℹ 6 more variables: referenceSetDbId <chr>, studyDbId <chr>,
#> # variantCount <int>, variantSetDbId <chr>, variantSetName <chr>,
#> # metadataFields <list>
vs_id <- vsets$variantSetDbId[1]brapi_get_marker_map() used to read a marker’s
chromosome and position off brapi_variants()’s
referenceName/start columns. On this server
those come back NA for every variant — and per the BrAPI
spec, that is legal: Variant.start and
Variant.referenceName place a variant on a reference
assembly, and the spec explicitly allows a server to leave them
unset if it has no such assembly configured for that variant, rather
than treating NA as an error condition.
This server has simply chosen not to populate assembly coordinates on
its variants. Positions for the same markers do exist, just under a
different resource: the Genome Maps entity
(/maps, /markerpositions) places a marker on a
named map instead of a reference assembly — genetic (measured
in cM) or physical (measured in bp), per the map’s own type
and unit fields — and the same marker can appear on more
than one map. brapi_get_marker_map() now reads from there
instead:
# Positions for every variant in the set, wherever they have been placed
markers <- brapi_get_marker_map(con, variantSetDbId = vs_id)
#> ℹ Async search started (ID: ed6fd637-1607-4561-b446-915680461790). Polling...
#> Warning: 14 of 20 variants in "variantset1" have no marker position record; returning
#> positions for the remaining 6.
markers
#> # A tibble: 6 × 8
#> variantDbId variantName mapDbId mapName type unit linkageGroupName position
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <int>
#> 1 variant01 M1 genome_… Primar… Phys… cM Chromosome 1 200
#> 2 variant02 M2 genome_… Primar… Phys… cM Chromosome 1 4000
#> 3 variant03 M3 genome_… Primar… Phys… cM Chromosome 1 60000
#> 4 variant04 M4 genome_… Primar… Phys… cM Chromosome 2 200
#> 5 variant05 M5 genome_… Primar… Phys… cM Chromosome 2 4000
#> 6 variant06 M6 genome_… Primar… Phys… cM Chromosome 2 60000Not every variant in a set is guaranteed to have a map placement. On this server, only some of the variant set’s variants have one — the warning above states the exact count rather than silently returning a shorter tibble. If you already know which map you care about, querying it directly skips the variant-set lookup and returns the same shape:
maps <- brapi_maps(con)
maps[, c("mapDbId", "mapName", "type", "unit")]
#> # A tibble: 2 × 4
#> mapDbId mapName type unit
#> <chr> <chr> <chr> <chr>
#> 1 genome_map1 Primary Paw Paw Map Physical Map cM
#> 2 genome_map2 Primary Paw Paw Map Physical Map cMBoth maps on this test server declare a “Physical Map” type with a “cM” unit. That’s inconsistent fixture data on this particular server, not something brapiR2 or the BrAPI specification produces. On a real server the two fields agree.
brapi_get_marker_map(con, mapDbId = maps$mapDbId[1])
#> # A tibble: 3 × 8
#> variantDbId variantName mapDbId mapName type unit linkageGroupName position
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <int>
#> 1 variant01 M1 genome_… Primar… Phys… cM Chromosome 1 200
#> 2 variant02 M2 genome_… Primar… Phys… cM Chromosome 1 4000
#> 3 variant03 M3 genome_… Primar… Phys… cM Chromosome 1 60000
# Get dosage matrix for genomic selection (samples x markers, values 0/1/2)
dosage <- brapi_get_dosage_matrix(con, vs_id)
#> ℹ Fetching allele matrix for variant set "variantset1"...
#> ℹ Encoding 260 genotype calls as allele dosages...
#> ✔ Dosage matrix ready: 13 samples x 20 markers.
dim(dosage)
#> [1] 13 20
dosage[seq_len(min(3, nrow(dosage))), seq_len(min(5, ncol(dosage)))]
#> variant01 variant02 variant03 variant04 variant05
#> callset01 0 0 0 0 0
#> callset02 1 0 NA NA NA
#> callset03 1 0 1 1 0Authentication
Most production BrAPI servers require authentication. These calls need your own server and credentials, so they are shown but not run here.
# Username/password login - used by Breedbase, BMS, and Germinate
con <- brapi_connection("https://my-breedbase.org")
con <- brapi_login(con, "username", "password")
# OAuth 2.0 (EBS and similar)
con <- brapi_login_oauth2(
con,
client_id = "my_id",
client_secret = "my_secret",
authorize_url = "https://auth.example.org/authorize",
access_url = "https://auth.example.org/token"
)
# Bearer token (GIGWA, custom servers)
con <- brapi_set_token(con, Sys.getenv("BRAPI_TOKEN"))Handling Credentials Safely
The example above reads a token from an environment variable rather
than writing it as a literal string, and that’s deliberate: a password
or token typed directly into a script gets committed to version control
the moment someone runs git add, and stays in the project’s
history even after it’s “removed” in a later commit. There are two safer
places to keep it.
.Renviron and
Sys.getenv(). For scripts you run yourself, put
credentials in a .Renviron file (in your home directory for
all projects, or in the project directory for that project only) as
NAME=value pairs, one per line:
BRAPI_TOKEN=abc123...
BRAPI_PASSWORD=my_password
R reads .Renviron automatically at startup, so after
restarting your R session the values are available via
Sys.getenv():
con <- brapi_connection("https://my-breedbase.org")
con <- brapi_login(
con, Sys.getenv("BRAPI_USERNAME"), Sys.getenv("BRAPI_PASSWORD")
)Add .Renviron to .gitignore so it never
gets committed. usethis::edit_r_environ() opens the right
file for you.
The keyring package. For credentials you want stored
in your operating system’s own credential manager (Keychain on macOS,
Credential Manager on Windows, Secret Service on Linux) rather than in a
plaintext file, use keyring.
key_set() prompts interactively for a secret and stores it;
key_get() retrieves it later, in this session or a future
one, without the value ever appearing in your script or your R
history:
# Run once, interactively - prompts for the password and stores it
keyring::key_set("brapiR2_my-breedbase", username = "my_username")
# In scripts, from then on:
con <- brapi_connection("https://my-breedbase.org")
con <- brapi_login(
con,
username = "my_username",
password = keyring::key_get("brapiR2_my-breedbase", username = "my_username")
)Either approach keeps the secret out of the script itself, out of
shell history, and out of anything you might git commit or
share.
The token lives in the connection object. Once you
have authenticated, the credential is held inside con and
goes wherever con goes:
saveRDS(con, "con.rds") writes it to disk in plain text,
and str(con) or unclass(con) prints it in
full. print(con) shows only whether the connection is
authenticated, never the token itself, so that is what to paste when
sharing a session or reporting a problem.
Caching and Parallel Fetching
Two features for when you are fetching the same data repeatedly, or a lot of it at once. Both run against the public test server here; neither needs authentication.
Caching stores each response on disk under a key built from the URL and query parameters, and returns it without a network call for as long as the TTL lasts. Reach for it when you are developing a script against a slow or distant server and re-running the same calls, or when a knitted report would otherwise hit the server once per render. It is off by default, per connection, and never shared between connections.
# Enable caching — repeated calls within the TTL return instantly
cache_dir <- tempfile("brapi_cache_")
dir.create(cache_dir)
perf_con <- brapi_cache_enable(con, ttl = 3600, dir = cache_dir)
#> ✔ Caching enabled at /tmp/Rtmpo5aqio/brapi_cache_bdf7d5199a (TTL: 3600s)
# First call: hits the server
invisible(brapi_programs(perf_con))
# Second call: reads from disk (sub-millisecond)
brapi_programs(perf_con)
#> ℹ Using cached response for /programs.
#> # A tibble: 3 × 12
#> additionalInfo externalReferences abbreviation commonCropName documentationURL
#> <list> <list> <chr> <chr> <chr>
#> 1 <named list> <list [1]> P1 Tomatillo https://brapi.o…
#> 2 <named list> <list [1]> P2 Tomatillo https://brapi.o…
#> 3 <named list> <list [1]> P3 Paw Paw https://brapi.o…
#> # ℹ 7 more variables: leadPersonDbId <chr>, leadPersonName <chr>,
#> # objective <chr>, programName <chr>, programType <chr>,
#> # fundingInformation <chr>, programDbId <chr>
# Clear all cached files
brapi_cache_clear(perf_con)
#> ✔ Cleared 1 cached response(s).Parallel fetching runs one call across many
identifiers at once, which helps when you are pulling data for dozens of
studies or germplasm and each request is slow.
brapi_fetch_parallel() uses whatever future
plan is already active — sequential unless you say otherwise — so
choosing the backend, and shutting it down afterwards, is yours to do.
See future’s own
documentation for the available backends, and DESIGN.md for why the
package does not set one for you:
future::plan(future::multisession, workers = 2)
# Fetch study data from multiple studies in parallel (all studies on the
# server, not just the one attached to the trial filtered above)
study_ids <- brapi_studies(con)$studyDbId
all_data <- brapi_fetch_parallel(perf_con, brapi_study_data, study_ids)
#> ℹ Fetching 3 items...
#> ℹ Using cached response for /observations.
#> ! No observations found for study "study2".
#> ℹ Using cached response for /observations.
#> ! No observations found for study "study3".
#> ✔ Fetched 2 rows from 3 sources.
all_data
#> # A tibble: 2 × 6
#> observationUnitDbId observationUnitName germplasmDbId germplasmName studyDbId
#> <chr> <chr> <chr> <chr> <chr>
#> 1 observation_unit1 Plot 1 germplasm1 Tomatillo Fan… study1
#> 2 observation_unit2 Plot 2 germplasm2 Tomatillo Fan… study1
#> # ℹ 1 more variable: `Corn Stalk Height` <list>As above, this server’s studies have no observations, so
all_data is an empty tibble here too — the call itself, and
the parallel-fetch mechanics, are real.
Since you set the plan, shutting the workers back down when you’re done with them is your responsibility too, not something the package does for you on exit:
future::plan(future::sequential)Comparison with QBMS
QBMS is a well-established R package for BrAPI access that has been widely adopted in the breeding community. brapiR2 and QBMS are complementary tools designed for different workflows.
| Feature | QBMS | brapiR2 |
|---|---|---|
| Style | Stateful (set program → set trial → set study) | Stateless (pass con everywhere) |
| Returns | Data frames | Tibbles (tidy, list-columns for nested data) |
| Pipe-friendly | Partial | Yes, all functions follow f(con, ...)
|
| Caching | No | Yes, disk-based with TTL |
| Parallel fetch | No | Via the caller’s own future backend |
| Genotyping | Limited | Allele matrix, dosage matrix, marker map (32/37 BrAPI entities covered overall, read-only) |
| Authentication | Yes | Yes |
brapiR2 targets BrAPI v2 only, by design - see the “Design History” section of DESIGN.md for why. QBMS’s own BrAPI version support is a separate question from this comparison; check QBMS’s own documentation for its current v1/v2 coverage rather than relying on a claim made here.
Same workflow, different style
The examples below use a private Breedbase-style server and credentials, so they are illustrative only.
QBMS — navigational, stateful:
library(QBMS)
set_crop("Wheat")
login_bms("https://my-bms.org", "user", "pass")
list_programs()
set_program("Wheat Breeding")
list_trials()
set_trial("Yield Trial 2022")
list_studies()
set_study("Ithaca 2022")
data <- get_study_data()brapiR2 — functional, pipe-friendly:
library(brapiR2)
library(dplyr)
con <- brapi_connection("https://my-bms.org") |>
brapi_login("user", "pass")
data <- brapi_programs(con) |>
filter(programName == "Wheat Breeding") |>
pull(programDbId) |>
(\(pid) brapi_trials(con, programDbId = pid))() |>
filter(trialName == "Yield Trial 2022") |>
pull(trialDbId) |>
(\(tid) brapi_studies(con, trialDbId = tid))() |>
filter(studyName == "Ithaca 2022") |>
pull(studyDbId) |>
brapi_study_data(con = con)Choose QBMS for interactive exploration in the console. Choose brapiR2 for reproducible scripts, pipelines, or when you need genotypic data and caching. Many users use both.
References
- Selby, P., Abbeloos, R., Backlund, J.E., et al., & The BrAPI Consortium (2019). BrAPI — an application programming interface for plant breeding applications. Bioinformatics, 35(20), 4147–4155. https://doi.org/10.1093/bioinformatics/btz190
