Skip to contents

If your team uses metacore + xportr + Pinnacle 21, this guide shows how to migrate to herald. The migration is usually a find-and-replace across your pipeline scripts — the concepts map 1:1, but the API is simpler and the toolchain collapses from three packages (plus Java) to one.

Why migrate?

Concern metacore + xportr + P21 herald
Dependencies 3 R packages + Java (P21) 1 R package
Compiled code xportr uses Rcpp Pure R, no compilation
Auditability Multiple source code bases Single auditable codebase
Define-XML Requires P21 Enterprise (license) write_define_xml()
Validation Requires P21 Community/Enterprise (Java) validate()
Format XPT only XPT + Dataset-JSON v1.1
ARM support P21 Enterprise only Included
Submission packaging Manual SOP submit()
CRAN-ready metacore/xportr on CRAN herald (pre-CRAN, GitHub)

Function mapping

Task metacore + xportr herald
Build spec object metacore::metacore(ds_spec, var_spec, ...) herald_spec(ds_spec, var_spec, ...)
Read P21 Excel spec metacore::spec_to_metacore("spec.xlsx") read_spec("spec.xlsx")
Read Define-XML spec metacore::spec_to_metacore("define.xml") read_spec("define.xml")
Save spec to file (not available) write_spec(spec, "spec.json")
List datasets metacore$ds_spec$dataset spec_datasets(spec)
Get variable metadata metacore$var_spec %>% filter(dataset == "DM") spec_vars(spec, "DM")
Get codelist metacore$codelist %>% filter(code_id == "SEX") spec_codelist(spec, "SEX")
Set variable labels xportr::xportr_label(dm, meta, "DM") set_label(dm, AGE="Age", ...)
Coerce types xportr::xportr_type(dm, meta, "DM") coerce_types(dm, spec, "DM")
Set lengths xportr::xportr_length(dm, meta, "DM") set_length(dm, AGE=8L, ...)
Set formats xportr::xportr_format(dm, meta, "DM") set_format(dm, AGE="8.")
Reorder columns xportr::xportr_order(dm, meta, "DM") order_cols(dm, spec, "DM")
All metadata in one xportr_*() calls apply_spec(dm, spec, "DM")
Write XPT xportr::xportr_write(dm, "dm.xpt") write_xpt(dm, "dm.xpt")
Read XPT haven::read_xpt("dm.xpt") read_xpt("dm.xpt")
Write Dataset-JSON (not available) write_json(dm, "dm.json", dataset="DM")
Generate Define-XML P21 Enterprise (GUI) write_define_xml(spec, "define.xml")
Generate define.html P21 Enterprise (GUI) write_define_html(spec, "define.html")
Validate dataset P21 Community/Enterprise (Java, GUI) validate(dir, spec = spec)
FDA SDTM rules P21 Enterprise license validate(dir, config = "fda-sdtm-ig-3.3")
PMDA rules P21 Enterprise + PMDA add-on validate(dir, rules = "pmda")
CDISC CORE rules P21 Enterprise validate(dir, rules = "core")
HTML validation report P21 GUI export validation_report(result, "report.html")
Excel validation report P21 GUI export validation_report(result, "report.xlsx")
Package full submission Manual SOP submit(path, spec = spec)

Side-by-side: SDTM DM workflow

Old way (metacore + xportr)

# 5 separate transformation steps
dm <- dm %>%
  xportr::xportr_label(metacore, domain = "DM") %>%
  xportr::xportr_type(metacore, domain = "DM") %>%
  xportr::xportr_length(metacore, domain = "DM") %>%
  xportr::xportr_format(metacore, domain = "DM") %>%
  xportr::xportr_order(metacore, domain = "DM")

xportr::xportr_write(dm, path = "sdtm/dm.xpt",
                     metadata = metacore, domain = "DM")

herald way

spec <- herald_spec(
  ds_spec = data.frame(dataset = "DM", label = "Demographics",
                       keys = "STUDYID, USUBJID", stringsAsFactors = FALSE),
  var_spec = data.frame(
    dataset   = c("DM","DM","DM","DM"),
    variable  = c("STUDYID","USUBJID","AGE","SEX"),
    label     = c("Study Identifier","Unique Subject Identifier","Age","Sex"),
    data_type = c("text","text","integer","text"),
    length    = c(12L,11L,8L,1L),
    order     = c(1L,2L,3L,4L),
    stringsAsFactors = FALSE
  )
)

dm <- data.frame(
  STUDYID = rep("CDISCPILOT01", 3L),
  USUBJID = c("01-701-1015","01-701-1023","01-701-1028"),
  AGE     = c("63","64","71"),   # character from CSV
  SEX     = c("F","M","M"),
  stringsAsFactors = FALSE
)

# One call replaces all 5 xportr_* steps
dm <- suppressMessages(apply_spec(dm, spec, "DM"))

xpt_path <- file.path(tempdir(), "dm.xpt")

write_xpt(dm, xpt_path)

Side-by-side: ADaM ADSL workflow

Old way (metacore + xportr + P21)

# Separate calls for each transformation
adsl <- adsl %>%
  xportr::xportr_label(adam_metacore, domain = "ADSL") %>%
  xportr::xportr_type(adam_metacore, domain = "ADSL") %>%
  xportr::xportr_length(adam_metacore, domain = "ADSL") %>%
  xportr::xportr_format(adam_metacore, domain = "ADSL") %>%
  xportr::xportr_order(adam_metacore, domain = "ADSL")

# Write XPT separately
xportr::xportr_write(adsl, path = "adam/adsl.xpt",
                     metadata = adam_metacore, domain = "ADSL",
                     label = "Subject-Level Analysis Dataset")

# Define-XML: launch P21 Enterprise GUI manually
# Validate: launch P21 Community validator manually

herald way

adam_spec <- herald_spec(
  ds_spec = data.frame(
    dataset = "ADSL",
    label   = "Subject-Level Analysis Dataset",
    keys    = "STUDYID, USUBJID",
    stringsAsFactors = FALSE
  ),
  var_spec = data.frame(
    dataset   = c("ADSL","ADSL","ADSL","ADSL"),
    variable  = c("STUDYID","USUBJID","TRTP","AGE"),
    label     = c("Study Identifier","Unique Subject Identifier",
                  "Planned Treatment for Period","Age"),
    data_type = c("text","text","text","integer"),
    length    = c(12L,11L,40L,8L),
    order     = c(1L,2L,3L,4L),
    stringsAsFactors = FALSE
  )
)

adsl <- data.frame(
  STUDYID = rep("CDISCPILOT01", 3L),
  USUBJID = c("01-701-1015","01-701-1023","01-701-1028"),
  TRTP    = c("Xanomeline High Dose","Placebo","Xanomeline Low Dose"),
  AGE     = c(63L, 64L, 71L),
  stringsAsFactors = FALSE
)

# One call
adsl <- suppressMessages(apply_spec(adsl, adam_spec, "ADSL"))

adam_dir <- tempfile("adam_")
dir.create(adam_dir)

write_xpt(adsl, file.path(adam_dir, "adsl.xpt"))

# Validate ADSL against spec
result <- validate(adam_dir, spec = adam_spec, standard = "adamig",
                   version = "1.1", rules = NULL)
#>  Auto-selected config: "fda-adam-ig-1.1"
result$summary
#> $reject
#> [1] 0
#> 
#> $high
#> [1] 0
#> 
#> $medium
#> [1] 4
#> 
#> $low
#> [1] 0
#> 
#> $total
#> [1] 4

Spec migration from existing Excel

If you already have a Pinnacle 21 Excel specification, read_spec() reads it directly:

# Works with standard P21 Excel tab names
spec <- read_spec("path/to/existing_spec.xlsx")

# Check what was read
spec
spec_datasets(spec)

All nine P21 Excel tabs are parsed: ds_spec, var_spec, value_spec, codelist, study, dictionaries, methods, comments, documents.

Migration checklist