Export an XPT File in R with xportr: The CDISC-Compliant Workflow

Take a validated ADaM dataset to a submission-ready SAS Transport (XPT v5) file — the full xportr pipeline (metadata, type, length, label, format, order, write), the strict_checks compliance gate, and how a variable-name or label violation is caught and fixed

Pharma & Clinical

A hands-on tutorial on exporting a clinical dataset to a submission-compliant SAS Transport (XPT) version-5 file with the pharmaverse xportr package. Learn the full export pipeline — attach a metadata spec once, then set each variable’s type, length, label, display format, and order and write the transport file; read it back to prove the labels, order, and dataset label survived. Then the part that matters for a submission: the strict_checks compliance gate (why the default silently ships a non-conformant file, and how strict_checks = TRUE turns a violation into a hard error), a worked violation-and-fix (an over-8-character variable name and an over-40-character label flagged, then corrected to a CDISC name and re-exported clean), and driving the whole thing from a metacore spec. On public pharmaverse data, so every line runs.

Published

July 1, 2026

Modified

July 7, 2026

TipKey takeaways
  • xportr writes a submission-compliant transport file, haven writes a raw one. Both emit XPT (SAS Transport, XPORT) version 5, but only xportr applies a metadata spec and checks the CDISC (Clinical Data Interchange Standards Consortium) constraints as it goes.
  • The pipeline is one spec attached once, then applied step by step. xportr_metadata() attaches the spec; xportr_type(), xportr_length(), xportr_label(), xportr_format(), xportr_order(), and xportr_df_label() set each variable’s storage type, length, label, display format, and column order plus the dataset label; xportr_write() emits the .xpt.
  • strict_checks = FALSE is the dangerous default — it ships a non-conformant file with only a warning. Use strict_checks = TRUE for a submission: it promotes every violation to a hard error and refuses to write.
  • The v5 limits are strict: names ≤ 8 characters, labels ≤ 40, character values ≤ 200. A too-long name (TREATMENTARM) or label is flagged; the fix is a proper CDISC name (TRT01P) and a shorter label.
  • For an ADaM date, the spec type is numeric with a DATE9. format — never date. type = "date" corrupts a real Date column on write; type = "numeric" + format = "DATE9." round-trips cleanly.

Introduction

You have a validated ADaM (Analysis Data Model) dataset — derived, double-programmed, and signed off. It cannot ship as an .rds or a .sas7bdat. A regulatory submission moves datasets in exactly one format: XPT, the SAS Transport format (XPORT) version 5 — the exchange format named in the FDA’s Study Data Standards Resources and enforced across CDISC (Clinical Data Interchange Standards Consortium) deliverables. Your job in this lesson is the last mile of packaging: turn that analysis-ready data frame into a .xpt a reviewer can open, with the right types, lengths, labels, and variable order — and prove it is conformant before it leaves your machine.

You do not hand-craft an XPT. In R, the xportr package (part of the pharmaverse) writes a compliant v5 transport file from a data frame plus a metadata spec — the same variable-level information a Define-XML metadata file carries — and checks the v5 constraints as it writes. The companion lesson mapped where the .xpt sits in the submission; this one is the mechanics: the full pipeline, the compliance gate, and what happens when a variable breaks the rules.

Here is the shape of the whole job — the pipeline you are about to run, end to end:

A left-to-right flow schematic of the xportr export pipeline in five boxes joined by arrows. Box 1 (light blue): an ADaM data.frame, labelled ADSL (validated). Box 2 (mid blue): xportr_metadata(), attach the spec plus domain. Box 3 (mid blue): apply the spec, listing the six step functions type, length, label, format, order, df_label. Box 4 (dark navy): xportr_write() with strict_checks = TRUE, marked as the gate. Box 5 (brand azure): adsl.xpt, a SAS Transport version-5 file that is compliant. The visual point is that one metadata spec is attached once, applied variable by variable, then written and gate-checked into a compliant transport file.

We work on a real subject-level dataset — ADSL (the Subject-Level Analysis Dataset, one row per subject) — from pharmaverseadam, so every block runs as written.

The full export pipeline

The pattern is always the same: attach the spec once, then apply it one property at a time, then write. The spec is a plain data frame with a fixed set of columns — dataset, variable, type, label, length, order, and format — one row per variable. In a real submission this information comes from your Define-XML source; here we write it inline so you can see exactly what each step reads.

xportr_metadata() attaches the spec and the domain name so the later steps do not each need it. Then, in order: xportr_type() coerces each variable to its SAS storage type, xportr_length() sets the v5 length, xportr_label() applies the ≤ 40-character variable labels, xportr_format() attaches SAS display formats, xportr_order() reorders the columns to match the spec, and xportr_df_label() sets the dataset-level label. Finally xportr_write() runs the conformance check and emits the .xpt.

library(xportr)
library(haven)
library(pharmaverseadam)

data("adsl", package = "pharmaverseadam")

keep  <- c("STUDYID", "USUBJID", "SUBJID", "SITEID", "AGE",
           "AGEU", "SEX", "RACE", "ARM", "SAFFL")
adsl2 <- adsl[, keep]

# The variable-level metadata spec — the information a Define-XML carries.
var_spec <- data.frame(
  dataset  = "ADSL",
  variable = keep,
  type     = c("character", "character", "character", "character", "numeric",
               "character", "character", "character", "character", "character"),
  length   = c(20, 30, 10, 10, 8, 10, 2, 40, 40, 2),
  label    = c("Study Identifier", "Unique Subject Identifier", "Subject Identifier",
               "Study Site Identifier", "Age", "Age Units", "Sex", "Race",
               "Description of Planned Arm", "Safety Population Flag"),
  format   = "",
  order    = 1:10,
  stringsAsFactors = FALSE
)

# The dataset-level label — note the 2nd argument is a metadata frame, not a string.
df_spec <- data.frame(dataset = "ADSL", label = "Subject-Level Analysis Dataset",
                      stringsAsFactors = FALSE)

out <- file.path(tempdir(), "adsl.xpt")   # the member name ("adsl") must be <= 8 characters
adsl2 |>
  xportr_metadata(var_spec, "ADSL")       |>  # attach the spec + domain once
  xportr_type()                           |>  # coerce each variable to its SAS type
  xportr_length(length_source = "metadata") |>  # set the v5 lengths from the spec
  xportr_label()                          |>  # apply the <= 40-char labels
  xportr_order()                          |>  # reorder columns per the spec
  xportr_format()                         |>  # attach SAS display formats
  xportr_df_label(df_spec)                |>  # set the dataset label
  xportr_write(out, strict_checks = TRUE)     # conformance-check, then write

The pipe prints a few informational rules (variables found in the spec, columns ordered) — those are progress messages, not warnings. Because we passed strict_checks = TRUE, the write only succeeded because the dataset is already conformant. Now prove the .xpt is a genuine, readable v5 file: read it back and check that the labels, the column order, and the dataset label all survived the transport.

library(haven)

back <- read_xpt(out)

names(back)                         # column order preserved from the spec
 [1] "STUDYID" "USUBJID" "SUBJID"  "SITEID"  "AGE"     "AGEU"    "SEX"    
 [8] "RACE"    "ARM"     "SAFFL"  
attr(back$AGE, "label")             # the variable label survived
[1] "Age"
attr(back, "label")                 # the dataset-level label survived
[1] "Subject-Level Analysis Dataset"
file.info(out)$size                 # the file, in bytes
[1] 54800

The columns come back in spec order, AGE still carries its "Age" label, and the dataset label "Subject-Level Analysis Dataset" is attached to the frame — the metadata round-tripped. That .xpt is the deliverable, the exact object that goes into analysis/adam/datasets/.

Dates need type = "numeric", not type = "date"

ADaM dates (TRTSDT, ADT, RFSTDTC-derived numerics) are the one place the spec catches people out. An R Date is stored as a number, and SAS represents dates the same way with a display format. So the spec type for a date is numeric, and you set format = "DATE9." to control how it displays. Writing a real Date column with type = "date" corrupts it on write — type = "date" is treated as a character type and breaks the underlying date value. Set the type to numeric and let the format do the display work:

library(xportr)
library(haven)

# A subject-level slice with a real ADaM date variable (class "Date").
dates <- data.frame(
  USUBJID = c("01-701-1015", "01-701-1023"),
  TRTSDT  = as.Date(c("2013-08-05", "2012-07-21")),
  stringsAsFactors = FALSE
)

# For an ADaM date: type = "numeric" with a SAS display format, NOT type = "date".
date_spec <- data.frame(
  dataset  = "ADSL",
  variable = c("USUBJID", "TRTSDT"),
  type     = c("character", "numeric"),
  length   = c(20, 8),
  label    = c("Unique Subject Identifier", "Date of First Exposure to Treatment"),
  format   = c("", "DATE9."),
  order    = 1:2,
  stringsAsFactors = FALSE
)

out_dt <- file.path(tempdir(), "adsldt.xpt")
dates |>
  xportr_metadata(date_spec, "ADSL") |>
  xportr_type()                      |>
  xportr_length(length_source = "metadata") |>
  xportr_label()                     |>
  xportr_format()                    |>  # applies the DATE9. display format
  xportr_order()                     |>
  xportr_write(out_dt, strict_checks = TRUE)

back_dt <- read_xpt(out_dt)
class(back_dt$TRTSDT)                 # "Date" — round-tripped as a real date
[1] "Date"
back_dt$TRTSDT                        # the dates, intact
[1] "2013-08-05" "2012-07-21"
attr(back_dt$TRTSDT, "format.sas")    # "DATE9" — the SAS display format is carried
[1] "DATE9"

The dates come back as a real Date column with the DATE9 display format attached. This is the pattern for every ADaM date and datetime variable — numeric type, a DATEw./DATETIMEw. format — and it is the trap worth committing to memory before you export a dataset with any dates in it.

The compliance gate: strict_checks

Everything above wrote cleanly because the data was already conformant. The interesting question is what happens when it is not — and here xportr has a default that will bite you. xportr_write() runs a validation check first, but its strict_checks argument defaults to FALSE, which means a failing check is only a warning and the file is written anyway. For an interactive exploration that is convenient; for a submission it is a landmine — you can ship a non-conformant .xpt and never notice.

Take a dataset that breaks two v5 rules: a variable named TREATMENTARM (12 characters, over the 8-character limit) with a label well over 40 characters. First, run the check directly with xpt_validate() — the engine behind strict_checks — as a programmatic pre-flight. It returns a character vector of findings (empty means clean):

library(xportr)

bad <- data.frame(
  USUBJID      = c("01-701-1015", "01-701-1023"),
  TREATMENTARM = c("Placebo", "Xanomeline High Dose"),
  AGE          = c(63, 64),
  stringsAsFactors = FALSE
)
spec_bad <- data.frame(
  dataset  = "ADSL",
  variable = c("USUBJID", "TREATMENTARM", "AGE"),
  type     = c("character", "character", "numeric"),
  length   = c(20, 40, 8),
  label    = c("Unique Subject Identifier",
               "Planned Treatment Arm for the Analysis Period Number One in the Study",
               "Age"),
  format   = "",
  order    = 1:3,
  stringsAsFactors = FALSE
)

badm <- bad |>
  xportr_metadata(spec_bad, "ADSL") |>
  xportr_type()   |>
  xportr_length(length_source = "metadata") |>
  xportr_label()

xpt_validate(badm)   # a character vector of findings — empty means conformant
[1] "Variable `TREATMENTARM` must be 8 characters or less."                                                                    
[2] "Label 'TREATMENTARM=Planned Treatment Arm for the Analysis Period Number One in the Study' must be 40 characters or less."

Two findings: the name is too long, and the label is too long. Now watch the default betray you. With strict_checks = FALSE, xportr_write() prints those same findings as a warning and still writes the file:

out_bad <- file.path(tempdir(), "adslbad.xpt")
badm |> xportr_write(out_bad, strict_checks = FALSE)
Warning: The following validation checks failed:
• Variable `TREATMENTARM` must be 8 characters or less.
• Label 'TREATMENTARM=Planned Treatment Arm for the Analysis Period Number One in the Study' must be 40 characters or less.
file.exists(out_bad)   # TRUE — the non-conformant file was written anyway
[1] TRUE

The file exists. A non-conformant dataset just slipped through with nothing but a warning you might have scrolled past. Set strict_checks = TRUE and the same call becomes a hard error that aborts before writing anything:

out_bad2 <- file.path(tempdir(), "adslbad2.xpt")
badm |> xportr_write(out_bad2, strict_checks = TRUE)
Error in `xportr_write()`:
! The following validation checks failed:
• Variable `TREATMENTARM` must be 8 characters or less.
• Label 'TREATMENTARM=Planned Treatment Arm for the Analysis Period Number One in the Study' must be 40 characters or less.
file.exists(out_bad2)   # FALSE — strict_checks aborted before any file was written
[1] FALSE

The error lists exactly what failed and no file is created. This is the rule to internalize: always write submission datasets with strict_checks = TRUE. A conformance failure should stop your pipeline, not decorate it with a warning.

Fix a violation the CDISC way

The gate told you what is wrong; fixing it is a metadata change, not a hack. The name TREATMENTARM should be the standard CDISC variable TRT01P (Planned Treatment for Period 01) — which is why CDISC names are short in the first place — and the label needs to come under 40 characters. Correct both in the data and the spec, and the same strict write succeeds:

library(xportr)
library(haven)

good <- data.frame(
  USUBJID = c("01-701-1015", "01-701-1023"),
  TRT01P  = c("Placebo", "Xanomeline High Dose"),   # renamed to the CDISC variable
  AGE     = c(63, 64),
  stringsAsFactors = FALSE
)
spec_good <- data.frame(
  dataset  = "ADSL",
  variable = c("USUBJID", "TRT01P", "AGE"),
  type     = c("character", "character", "numeric"),
  length   = c(20, 40, 8),
  label    = c("Unique Subject Identifier",
               "Planned Treatment for Period 01",   # now <= 40 characters
               "Age"),
  format   = "",
  order    = 1:3,
  stringsAsFactors = FALSE
)

out_fix <- file.path(tempdir(), "adslfix.xpt")
good |>
  xportr_metadata(spec_good, "ADSL") |>
  xportr_type()   |>
  xportr_length(length_source = "metadata") |>
  xportr_label()  |>
  xportr_order()  |>
  xportr_write(out_fix, strict_checks = TRUE)   # passes the gate

back_fix <- read_xpt(out_fix)
names(back_fix)                         # TRT01P in place of TREATMENTARM
[1] "USUBJID" "TRT01P"  "AGE"    
attr(back_fix$TRT01P, "label")          # the shortened label
[1] "Planned Treatment for Period 01"

Clean write, and the exported file carries TRT01P with its conformant label. That is the whole loop: pre-flight with xpt_validate() → write with strict_checks = TRUE → fix the flagged name or label in the spec → re-export. You iterate until the gate is silent.

Drive it from a metacore spec

Writing the spec inline is fine for teaching, but on a real study the spec is not something you retype — it is the study metadata, the single source of truth that also produces the Define-XML. The metacore package holds that spec as a structured object, and xportr accepts a metacore object anywhere it accepts a metadata frame. So the same pipeline runs straight off the spec, with no hand-written data frame at all:

library(metacore)
library(xportr)
library(pharmaverseadam)

# Load the pilot ADaM metacore object (a machine-readable spec).
e  <- new.env()
load(metacore_example("pilot_ADaM.rda"), envir = e)
mc <- get("metacore", envir = e)
adsl_mc <- select_dataset(mc, "ADSL")   # the ADSL slice of the spec

data("adsl", package = "pharmaverseadam")
common  <- intersect(adsl_mc$var_spec$variable, names(adsl))
adsl_in <- adsl[, common]

out_mc <- file.path(tempdir(), "adslmc.xpt")
adsl_in |>
  xportr_metadata(adsl_mc, "ADSL") |>
  xportr_type()   |>
  xportr_length() |>
  xportr_label()  |>
  xportr_order()  |>
  xportr_format() |>
  xportr_df_label(adsl_mc) |>
  xportr_write(out_mc, strict_checks = FALSE)

file.exists(out_mc)
[1] TRUE

The metadata now flows from one object into both the transport file and — in the Define-XML metadata lesson that follows in this series — the define.xml. That is the payoff of a spec-driven workflow: define each variable once, and every deliverable reads from the same definition. (A note on loading: metacore example files come in two flavours — load() an .rda, but load_metacore() a .rds; do not mix them.)

xportr vs haven::write_xpt()

Under the hood, xportr_write() calls haven::write_xpt(version = 5) — so both produce a genuine v5 transport file. The difference is everything that happens before the write:

haven::write_xpt(x, path, version = 5) xportr_write(x, path, strict_checks = TRUE)
Writes a v5 XPT Yes Yes (it calls haven internally)
Applies a metadata spec No — writes the frame as-is Yes — types, lengths, labels, formats, order
Checks the v5 limits No Yes — names ≤ 8, labels ≤ 40, values ≤ 200
Refuses a non-conformant file No Yes, with strict_checks = TRUE

Use haven::write_xpt() when you just need a v5 file from an already-correct frame. Use xportr when the file is a submission deliverable and must be proven conformant. The compliance layer is the reason xportr exists.

What xportr does not do

Be precise about xportr’s scope so you reach for the right tool at the right step:

  • It writes and checks the XPT — variable types, lengths, labels, formats, order, and the v5 constraints.
  • It does not generate Define-XML. It only consumes a metacore spec; producing the Define-XML metadata file is the job of metacore/metatools, covered in the Define-XML lesson that follows.
  • It does not perform full CDISC conformance. It checks the transport-format limits, not the hundreds of implementation-guide rules for SDTM (Study Data Tabulation Model) and ADaM (SDTMIG / ADaMIG), controlled terminology, or cross-dataset checks — that is Pinnacle 21 / CDISC CORE.
  • It does not derive or transform data. That is admiral/sdtm.oak. xportr is the last step — packaging analysis-ready data for transport.

Passing the xportr gate means your file is a well-formed, correctly-labelled v5 transport file. It does not mean the submission is CDISC-conformant end to end — that is a separate, later check.

🟢 With an AI agent

Ask Prova “I have a validated ADSL — write me the xportr pipeline to export it to a compliant XPT v5, with strict_checks on and the dates handled correctly.” — it answers grounded in this pillar’s lessons, with runnable pharmaverse code you can try on example data. The runtime is the judge. Ask Prova →

Common issues

A date variable comes out corrupted or errors on write. You set type = "date" in the spec for a real Date column. xportr treats type = "date" as a character type and breaks the underlying value. For any ADaM date or datetime, use type = "numeric" with a SAS display format (format = "DATE9.", "DATETIME20.", etc.); the value stays numeric and reads back as a Date.

xportr_write() errors with “file name must be 8 characters or less.” The .xpt member name — the file basename without the extension — is itself a v5 identifier and is capped at 8 characters. xportr_write("adsl_nodate.xpt") fails because adsl_nodate is 11 characters. Name the file for its dataset: adsl.xpt, adae.xpt, adtte.xpt.

A non-conformant file shipped without you noticing. You wrote with the default strict_checks = FALSE, so a length or label violation was only a warning and the file was written anyway. Always write submission datasets with strict_checks = TRUE so a violation is a hard error that stops the pipeline, and run xpt_validate() as a pre-flight if you want the findings without attempting a write.

Frequently asked questions

Use the xportr package for a submission dataset: df |> xportr_metadata(spec, "ADSL") |> xportr_type() |> xportr_length() |> xportr_label() |> xportr_order() |> xportr_write("adsl.xpt", strict_checks = TRUE). That attaches a metadata spec, applies each variable’s type, length, label, and order, checks the CDISC v5 constraints, and writes a compliant transport file. If you only need a raw v5 file from an already-correct frame, haven::write_xpt(df, "adsl.xpt", version = 5) does that without any conformance checking.

xportr_write() calls haven::write_xpt(version = 5) internally, so both produce a genuine v5 XPT. The difference is the compliance layer: xportr first applies a metadata spec (types, lengths, labels, formats, column order) and validates the CDISC v5 limits (variable names ≤ 8 characters, labels ≤ 40, character values ≤ 200), and with strict_checks = TRUE it refuses to write a non-conformant file. haven::write_xpt() writes the frame exactly as it is, with no metadata application and no conformance check. Use haven for a quick v5 file, xportr for a submission deliverable.

SAS Transport version 5 caps variable names at 8 characters, variable labels at 40 characters, and character values at 200 characters, with no compression. This is why SDTM and ADaM variable names are terse (USUBJID, AVAL, TRT01P). xportr flags any violation; the record layout is specified in SAS’s technical paper on the v5 transport format.

strict_checks is the argument to xportr_write() that decides how a failed conformance check is handled. It defaults to FALSE, which means a violation (a too-long name, label, or value) is only a warning and the file is written anyway — dangerous for a submission. Set strict_checks = TRUE and a violation becomes a hard error that aborts before writing. Always use TRUE for submission datasets. You can also run xpt_validate(df) on its own to get the findings as a character vector without attempting a write.

Do not set type = "date" in the spec — that corrupts a real Date column on write. An R Date is a number, and SAS stores dates the same way with a display format, so set type = "numeric" and format = "DATE9." (or "DATETIME20." for a datetime). Run xportr_format() in the pipeline to apply the format. The value stays numeric on disk and reads back as a proper Date with its display format attached.

Test your understanding

You are handed a small dataset to export. It has a variable named SUBJECTAGEGROUP (15 characters) labelled "Subject Age Group Category for the Pooled Analysis Population" (well over 40 characters). Write the code that (1) runs xpt_validate() to show the findings, (2) attempts a strict write and observes it abort, then (3) fixes the name to the CDISC variable AGEGR1 and shortens the label to "Pooled Age Group" so a strict write succeeds. Which two findings does the pre-flight report, and why does the strict write refuse the original?

The two v5 limits in play are names ≤ 8 characters and labels ≤ 40 charactersSUBJECTAGEGROUP and the long label break both. Build the metadata spec with dataset, variable, type, length, label, format, and order columns, pipe the data through xportr_metadata() |> xportr_type() |> xportr_length() |> xportr_label(), then call xpt_validate() on the result. For the fix, change both the column name in the data frame and the variable/label entries in the spec.

library(xportr)

df <- data.frame(
  USUBJID         = c("01-701-1015", "01-701-1023"),
  SUBJECTAGEGROUP = c(">64", "18-64"),
  stringsAsFactors = FALSE
)
spec <- data.frame(
  dataset  = "ADSL",
  variable = c("USUBJID", "SUBJECTAGEGROUP"),
  type     = c("character", "character"),
  length   = c(20, 20),
  label    = c("Unique Subject Identifier",
               "Subject Age Group Category for the Pooled Analysis Population"),
  format   = "",
  order    = 1:2,
  stringsAsFactors = FALSE
)

# (1) Pre-flight: two findings — name > 8 chars, label > 40 chars
prepped <- df |>
  xportr_metadata(spec, "ADSL") |> xportr_type() |>
  xportr_length(length_source = "metadata") |> xportr_label()
xpt_validate(prepped)

# (2) Strict write aborts (no file written)
prepped |> xportr_write(file.path(tempdir(), "bad.xpt"), strict_checks = TRUE)

# (3) Fix the name and label, then the strict write passes
good <- data.frame(
  USUBJID = c("01-701-1015", "01-701-1023"),
  AGEGR1  = c(">64", "18-64"),
  stringsAsFactors = FALSE
)
spec_good <- data.frame(
  dataset  = "ADSL", variable = c("USUBJID", "AGEGR1"),
  type     = c("character", "character"), length = c(20, 10),
  label    = c("Unique Subject Identifier", "Pooled Age Group"),
  format   = "", order = 1:2, stringsAsFactors = FALSE
)
good |>
  xportr_metadata(spec_good, "ADSL") |> xportr_type() |>
  xportr_length(length_source = "metadata") |> xportr_label() |> xportr_order() |>
  xportr_write(file.path(tempdir(), "adsl.xpt"), strict_checks = TRUE)

The pre-flight reports two findings: Variable SUBJECTAGEGROUP must be 8 characters or less and a label that must be 40 characters or less. With strict_checks = TRUE the write refuses because either violation is a hard error. Renaming to the 6-character CDISC name AGEGR1 and shortening the label to "Pooled Age Group" (16 characters) clears both, and the strict write succeeds.

A. haven::write_xpt(df, "adsl.xpt", version = 5) B. xportr_write(df, "adsl.xpt", strict_checks = FALSE) C. xportr_write(df, "adsl.xpt", strict_checks = TRUE) after the xportr_metadata()typelengthlabel pipeline

C. Only the full xportr pipeline with strict_checks = TRUE both applies the metadata spec and turns a v5 violation into a hard error, so a non-conformant dataset cannot be written. A writes a genuine v5 file but performs no conformance check — it will happily write an over-length name. B runs the check but, with strict_checks = FALSE, only warns and writes the file anyway. For a submission deliverable, always use C.

Conclusion

Exporting a submission dataset is a disciplined pipeline, not a single write call. You attach the metadata spec once, apply it variable by variable — type, length, label, format, order, and the dataset label — and write with strict_checks = TRUE so any breach of the v5 limits (names ≤ 8, labels ≤ 40, values ≤ 200) stops you cold instead of shipping a warning. When the gate flags a variable, the fix is a metadata change — the CDISC name and a conformant label — not a workaround. Handle dates as numeric with a DATE9. format, drive the spec from a metacore object so it feeds the Define-XML too, and remember xportr’s boundary: it proves the transport file is well-formed, not that the whole submission is CDISC-conformant. Get this step right and the .xpt that lands in analysis/adam/datasets/ is exactly what a reviewer expects to open.

Note

This lesson is reproducible: every result on this page was produced by the code shown — copy any block and run it to reproduce them. The runtime is the judge.

References

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Export an {XPT} {File} in {R} with Xportr: {The}
    {CDISC-Compliant} {Workflow}},
  date = {2026-07-01},
  url = {https://www.datanovia.com/learn/pharma-clinical/07-submission-packaging/xpt-export-xportr},
  langid = {en}
}
For attribution, please cite this work as:
“Export an XPT File in R with Xportr: The CDISC-Compliant Workflow.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/07-submission-packaging/xpt-export-xportr.