OmniSelect FileSQL

Guides
← Back to App
Home › Guides › Reading SAS transport (.xpt) files

Inside an FDA Submission: Reading SAS Transport (.xpt) Files

Open a clinical study’s submission package and the data is all .xpt: dm.xpt, ae.xpt, adsl.xpt, one file per domain. They are SAS transport files, and the people who need to look inside them — reviewers, data managers, medical writers, clinical scientists — often have no SAS seat to do it with.

Why Everything Is .xpt

SAS transport (XPORT) is SAS’s interchange format. Unlike a .sas7bdat, its layout is published, which is why regulators chose it: the FDA requires study data sets — SDTM tabulations and ADaM analysis data — to be submitted as SAS XPORT version 5 files. Version 5 is old and strict: variable names of at most 8 characters, labels of at most 40, and character values of at most 200 bytes. That is why SDTM names look like USUBJID and AESTDTC.

ApproachWhat it costs
SASThe native route, and a licence per seat.
R (haven::read_xpt) or Python (pandas or pyreadstat)Reliable, once installed and set up — which a validated or locked-down workstation may not allow.
An online viewerPatient-level trial data sent to an unknown server. For unblinded data, not an option at all.

Doing It in the Browser

OmniSelect FileSQL reads .xpt files with ReadStat, the library behind R’s haven and Python’s pyreadstat, compiled to WebAssembly and run inside your browser tab. The files are never uploaded.

  1. Open the app.
  2. Drop dm.xpt and ae.xpt onto the File Select panel. Each gets a row, type XPT, and a table name in the Alias column from the first letter of its file name: D for DM, A for AE.
  3. Look at the demographics.
    SELECT * FROM D LIMIT 100
studyiddomainusubjidsiteidagesexarmcdarmrfstdtc
CV-301DMCV-301-01010154FPBOPlacebo2026-01-12
CV-301DMCV-301-01020161MTRTDrug 10 mg2026-01-14
CV-301DMCV-301-01030147FTRTDrug 10 mg2026-01-15
CV-301DMCV-301-01040169MPBOPlacebo2026-01-19

Column names are shown in lower case. SQL does not mind: USUBJID and usubjid are the same column, so write whichever your eyes are used to.

Quick Questions in Plain English

The plain-English box turns a question into SQL inside your browser, with no AI service. It works best on one domain at a time, naming its variables:

You typeIt writes
how many subjects per armSELECT arm, COUNT(*) AS count_rows FROM D GROUP BY arm
average age by sexSELECT sex, AVG(age) AS avg_age FROM D GROUP BY sex
count by aesevSELECT aesev, COUNT(*) AS count_rows FROM A GROUP BY aesev
aedecod where aeser is YSELECT aedecod FROM A WHERE aeser = 'Y'

A question that crosses domains is a join, and it is clearer written as SQL. Ask for count of adverse events by arm and you get a count of DM rows per arm, because adverse events is not a variable name. So always read the SQL before you trust the number — it is right there, beside the question.

Across Domains, in SQL

AE and DM share USUBJID, so a join puts each event next to the subject’s arm. Events and subjects with an event, per arm:

SELECT D.ARM, COUNT(*) AS events, COUNT(DISTINCT A.USUBJID) AS subjects_with_ae
FROM A
JOIN D ON A.USUBJID = D.USUBJID
GROUP BY D.ARM
ORDER BY D.ARM
armeventssubjects_with_ae
Drug 10 mg74
Placebo22

Severity by arm is one more column in the GROUP BY:

SELECT D.ARM, A.AESEV, COUNT(*) AS events
FROM A
JOIN D ON A.USUBJID = D.USUBJID
GROUP BY D.ARM, A.AESEV
ORDER BY D.ARM, A.AESEV
armaesevevents
Drug 10 mgMILD4
Drug 10 mgMODERATE2
Drug 10 mgSEVERE1
PlaceboMILD2

A listing of serious events with each subject’s demographics:

SELECT A.USUBJID, D.ARM, D.AGE, D.SEX, A.AEDECOD, A.AESEV
FROM A
JOIN D ON A.USUBJID = D.USUBJID
WHERE A.AESER = 'Y'
usubjidarmagesexaedecodaesev
CV-301-0203Drug 10 mg66FNauseaSEVERE

And the subjects with no adverse event at all — a LEFT JOIN that keeps DM rows with no match in AE:

SELECT D.USUBJID, D.ARM
FROM D
LEFT JOIN A ON A.USUBJID = D.USUBJID
WHERE A.USUBJID IS NULL
usubjidarm
CV-301-0101Placebo
CV-301-0202Placebo

Drop your .xpt files in and query them together.

Open the app →

A Whole Folder at Once

Submission data usually travels zipped. Add the .zip itself and every .xpt inside becomes its own table, shown with its path — sdtm.zip › m5/datasets/cv-301/tabulations/sdtm/dm.xpt — and ready to join. Nothing is extracted to your disk.

Two things to watch. Table names are single letters: when two files start with the same letter, the second takes the first free one, so after ae.xpt has A, adsl.xpt becomes B. Read the Alias column, or type a letter you prefer into it. And there are 26 letters: a package with more domains than that adds the first 26 and says which were left out, so remove the ones you do not need and add the rest.

Dates in SDTM and ADaM

The two standards store dates differently, and both come through as they should:

A demographics summary by arm, for a quick look at balance:

SELECT arm, COUNT(*) AS subjects, ROUND(AVG(age), 1) AS mean_age,
       MIN(age) AS min_age, MAX(age) AS max_age
FROM D
GROUP BY arm
armsubjectsmean_agemin_agemax_age
Placebo454.34369
Drug 10 mg4584766

What Comes Across

In the .xpt fileIn the table
XPORT version 5 (the FDA format) and version 8Both open
Character variablesText, including ISO 8601 dates
Numeric variablesNumbers
Numeric variables with a SAS date, datetime or time formatDates, date-times and times
Missing valuesEmpty — NULL in SQL
Data set and variable labelsNot shown: columns are named by the variable names

Any result exports to CSV, Excel, JSON or Parquet from the Export menu — a serious-event listing as a spreadsheet for a safety review, say. The export holds every row of the result.

Reasonable Questions

Is this a validated system?

No. It is for looking, checking and answering questions quickly. It does not replace the validated programs that produce submission data or its tables and listings, and results you report formally should come from those.

Is it safe for unblinded or patient-level data?

The files are read inside your browser and are not uploaded anywhere; after the page has loaded it makes no network requests at all. You can check that yourself in about a minute, and your organisation’s own rules still decide which machines may hold the data.

Can it write .xpt files?

No. Exports are CSV, JSON, Excel or Parquet.

How large a data set can it open?

Up to 50 MB and 1,000,000 rows per file. Large findings domains such as LB can exceed that in big studies; filter them down in SAS, R or Python first.

Does it read .sas7bdat too?

Yes — see opening a .sas7bdat file without SAS. Both kinds can be loaded and joined together.

Related Guides