SDTM EX Domain in R with sdtm.oak: Map Drug Exposure to CDISC SDTM

Map a raw study-drug dosing extract into a CDISC-conformant SDTM EX domain in R with sdtm.oak — the Interventions observation class, the dose/route/frequency qualifiers, ISO 8601 dates, the sequence number, and study days

Pharma & Clinical

A complete, runnable tutorial for the SDTM EX (exposure) domain in R. Learn what the CDISC SDTM Interventions observation class is (one record per administration episode, keyed by the topic variable EXTRT), how it differs from the Findings and Events classes, then map a raw dosing extract into a conformant EX domain with sdtm.oak: generate_oak_id_vars() for record lineage, assign_no_ct() for the treatment and dose amount, assign_ct() for the controlled-terminology qualifiers (EXDOSU/EXDOSFRM/EXDOSFRQ/EXROUTE), assign_datetime() for ISO 8601 EXSTDTC/EXENDTC, derive_seq() for EXSEQ, and derive_study_day() for EXSTDY/EXENDY — every line runnable, the exposure record every efficacy and safety analysis reads.

Published

July 1, 2026

Modified

July 7, 2026

TipKey takeaways
  • EX is an Interventions domain — a third record shape. The Findings class (vital signs, labs) records measurements; the Events class (adverse events) records things that happened; the Interventions class records things given to the subject — one row per dosing episode, keyed by the treatment EXTRT.
  • The topic variable is EXTRT, the treatment given. assign_no_ct() copies the study-defined treatment name as collected; the dose amount EXDOSE is copied the same way.
  • The dose descriptors are controlled. assign_ct() recodes the collected unit, dose form, frequency, and route to their CDISC standard terms (EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE).
  • An exposure episode is bounded by two ISO 8601 dates. assign_datetime() builds the start (EXSTDTC) and end (EXENDTC) that fix when the subject was on drug.
  • derive_seq() numbers the episodes and derive_study_day() places them on the study clock. EXSEQ is unique per subject; EXSTDY/EXENDY count days from the reference start — and the whole domain becomes the input the treatment variables in ADSL read from.

Introduction

The subject took 54 mg of study drug transdermally from January to June, then was up-titrated to 81 mg. That administration record has one job: reach the regulator as the SDTM (Study Data Tabulation Model) EX (exposure) domain — the exposure every efficacy and safety analysis is measured against. What the site records is looser: a dosing CRF (case report form) page with a drug name, a number, a form ticked “Patch”, a start and stop date typed however the coordinator typed them. The job here is to turn that raw extract into a conformant EX domain a reviewer can open and read the same way for any study.

This lesson does that mapping end to end in R with sdtm.oak, the pharmaverse package for raw-to-SDTM transformation. It completes the observation-class trio: the Findings domain (vital signs) records what was measured, the Events domain (adverse events) records what happened, and EX records what was administered — the same oak algorithms, a third record shape. SDTM is the upstream of the clinical data flow — raw → SDTM → ADaM → tables, listings, and figures (TLF) — so a clean EX domain here is what lets the analysis layer derive the treatment start and end dates that anchor the subject-level ADSL dataset.

Here is where we are headed — the conformant EX domain we will build, drawn as the exposure timeline it encodes, straight off the mapped result:

Exposure-timeline (Gantt) chart of the mapped SDTM EX domain. Each horizontal segment is one dosing episode for a subject (six subjects on the y axis), running from its start date EXSTDTC to its end date EXENDTC on the x axis, coloured by dose: grey for the 0 mg placebo arm, light azure for 54 mg, dark azure for the 81 mg up-titration. Subjects on active drug show one to three consecutive segments — some stepping up in dose over months, some continuing at the same dose across a new dosing interval; the two placebo subjects show a single grey segment. The point is that a raw dosing extract has become a tidy, standardized set of exposure episodes, each with a treatment, a dose, a route, and a start and end date on the study clock.

Every segment there is one SDTM record — a treatment, a dose, a route, bounded by a start and an end. By the end of this lesson you will have produced all of them from a raw extract. New to the wider pipeline? Start with the clinical-trial data flow, which maps where SDTM sits, and the CDISC standards, which places SDTM next to CDASH and ADaM.

What the SDTM Interventions class is

SDTM — the Study Data Tabulation Model — is the CDISC (Clinical Data Interchange Standards Consortium) standard for organizing collected trial data into fixed domains. It is published as the SDTM foundational standard and the SDTM Implementation Guide (SDTMIG). SDTM sorts every observation into one of three general observation classes, and the class decides the shape of a record:

Observation class What it records Example domains Topic variable
Interventions Things given to or done to the subject EX (exposure), CM (con meds), PR (procedures) --TRT
Events Things that happened to the subject AE (adverse events), MH (medical history), DS (disposition) --TERM
Findings Measurements and assessments VS (vital signs), LB (labs), EG (ECG) --TESTCD

The vital-signs lesson mapped a Findings domain, where the topic variable is a test code (VSTESTCD = “what was measured”); the adverse-events lesson mapped an Events domain, keyed by a reported term (AETERM = “what happened”). Interventions are the third shape. Study-drug exposure is neither a measurement nor an occurrence — it is a treatment administered, so the topic variable is the treatment EXTRT (“what was given”), and there is one record per constant dosing episode: a new record whenever the dose, route, or form changes — and also at a protocol dosing interval or visit boundary, so a subject can have two consecutive records at the same dose. That is the whole distinction — treatment instead of test code or term, episode instead of measurement or event.

An EX record is that topic treatment plus its dose qualifiers, identifiers, and timing:

Variable role EX example What it carries
Identifiers STUDYID, USUBJID, DOMAIN, EXSEQ Study, subject, domain code (EX), and a unique sequence number per record.
Topic EXTRT The name of the treatment given (the study-defined drug or PLACEBO).
Dose qualifiers EXDOSE, EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE Dose amount, unit, dosage form, frequency, and route of administration.
Timing EXSTDTC, EXENDTC, EXSTDY, EXENDY Episode start and end in ISO 8601 (--DTC), and the study day of each.

Two conventions carry the whole mapping. First, the topic variable (EXTRT) names the intervention and everything else qualifies it. Second, the dose descriptors are restricted to controlled terminology (CT) — CDISC-published codelists that fix the allowed values (EXROUTE must be TRANSDERMAL, not “transdermal” or “patch”). CDISC maintains them as SDTM Controlled Terminology. Mapping raw to SDTM is, in essence, filling the right qualifier columns with the right CT-valid values — the same job as for Findings and Events, done for a third record shape.

The raw dosing extract and its controlled terminology

Two inputs drive a mapping: the raw extract and the study controlled terminology. sdtm.oak ships raw examples for concomitant medications and vital signs but not exposure, so we build a small raw dosing extract in the shape an EDC (electronic data capture) system exports one — one wide row per dosing episode, sponsor-chosen column names, dates in the format the form used. It follows the public CDISC pilot: a transdermal study drug titrated between 54 mg and 81 mg against a placebo arm.

library(sdtm.oak)
library(dplyr, warn.conflicts = FALSE)

# A raw study-drug dosing extract: one row per dosing episode, as collected
ex_raw <- data.frame(
  SUBJ    = c("1015","1015","1015","1023","1028","1028","1033","1039","1039","1044","1044","1044"),
  EXSPID  = c("EX01","EX02","EX03","EX01","EX01","EX02","EX01","EX01","EX02","EX01","EX02","EX03"),
  DRUG    = c("Xanomeline","Xanomeline","Xanomeline","Placebo","Xanomeline","Xanomeline",
              "Placebo","Xanomeline","Xanomeline","Xanomeline","Xanomeline","Xanomeline"),
  DOSE    = c(54, 54, 81, 0, 54, 81, 0, 54, 54, 54, 81, 81),
  DOSEU   = "mg",
  FORM    = "Patch",
  FREQ    = "Once daily",
  ROUTE   = "Transdermal",
  EXSTDAT = c("02 Jan 2014","17 Jan 2014","19 Jun 2014","05 Aug 2012","11 Mar 2013","28 Mar 2013",
              "22 Sep 2013","03 Feb 2014","20 Feb 2014","09 Apr 2013","24 Apr 2013","10 Jun 2013"),
  EXENDAT = c("16 Jan 2014","18 Jun 2014","02 Jul 2014","27 Aug 2012","27 Mar 2013","30 May 2013",
              "18 Oct 2013","19 Feb 2014","30 Jun 2014","23 Apr 2013","09 Jun 2013","25 Jul 2013"),
  stringsAsFactors = FALSE
)

ex_raw |>
  select(SUBJ, DRUG, DOSE, DOSEU, ROUTE, EXSTDAT, EXENDAT) |>
  head()
  SUBJ       DRUG DOSE DOSEU       ROUTE     EXSTDAT     EXENDAT
1 1015 Xanomeline   54    mg Transdermal 02 Jan 2014 16 Jan 2014
2 1015 Xanomeline   54    mg Transdermal 17 Jan 2014 18 Jun 2014
3 1015 Xanomeline   81    mg Transdermal 19 Jun 2014 02 Jul 2014
4 1023    Placebo    0    mg Transdermal 05 Aug 2012 27 Aug 2012
5 1028 Xanomeline   54    mg Transdermal 11 Mar 2013 27 Mar 2013
6 1028 Xanomeline   81    mg Transdermal 28 Mar 2013 30 May 2013

Each raw row is one dosing episode: the treatment (DRUG), the dose amount and unit (DOSE, DOSEU), the dose form, frequency, and route, and the episode’s start and end date. A subject on active drug has several consecutive rows — as the dose steps up, or as a new dosing interval begins at the same dose. The controlled terminology maps each collected value to its standard CDISC term within a codelist. A real study CT ships as part of the SDTM specification; here we define the small set of dose-descriptor codelists we need:

# Study controlled terminology: collected value -> CDISC standard term, by codelist
ex_ct <- data.frame(
  codelist_code   = c("C71620",  "C66726",  "C71113",     "C66729"),
  term_value      = c("mg",      "PATCH",   "QD",         "TRANSDERMAL"),  # CDISC standard term
  collected_value = c("mg",      "Patch",   "Once daily", "Transdermal"),  # as the CRF recorded it
  term_preferred_term = NA_character_, term_synonyms = NA_character_,
  stringsAsFactors = FALSE
)

ex_ct |> select(codelist_code, collected_value, term_value)
  codelist_code collected_value  term_value
1        C71620              mg          mg
2        C66726           Patch       PATCH
3        C71113      Once daily          QD
4        C66729     Transdermal TRANSDERMAL

collected_value is what the CRF recorded; term_value is the CDISC-standard term sdtm.oak will substitute. The codelists are CDISC standard: C71620 is the Unit codelist (EXDOSU), C66726 the Pharmaceutical Dosage Form codelist (EXDOSFRM), C71113 the Frequency codelist (EXDOSFRQ, where “Once daily” → QD), and C66729 the Route of Administration codelist (EXROUTE). The ct_clst argument of every CT-aware function points at one of these codelist_code values.

Stamp record lineage with oak_id_vars

Because we map one raw field at a time and stitch the pieces together, sdtm.oak needs a stable key on every raw row. generate_oak_id_vars() adds three: oak_id (the row number), raw_source (which extract it came from), and patient_number.

ex_raw <- ex_raw |>
  generate_oak_id_vars(pat_var = "SUBJ", raw_src = "ex_raw")

ex_raw |>
  select(oak_id, raw_source, patient_number, DRUG, DOSE) |>
  head()
  oak_id raw_source patient_number       DRUG DOSE
1      1     ex_raw           1015 Xanomeline   54
2      2     ex_raw           1015 Xanomeline   54
3      3     ex_raw           1015 Xanomeline   81
4      4     ex_raw           1023    Placebo    0
5      5     ex_raw           1028 Xanomeline   54
6      6     ex_raw           1028 Xanomeline   81

These three columns are the oak_id_vars. Every qualifier map below passes id_vars = oak_id_vars(), which tells sdtm.oak to join the new column back onto the right record by this key — the mechanism that lets partial maps combine without duplicating rows.

Map the topic variable and the dose amount

An Interventions record begins with its topic — the treatment given. Unlike a Findings test code, EXTRT is a study-defined treatment name copied as collected, so we use assign_no_ct() (“assign”, because it copies a collected value; “no_ct”, because we take the study’s own drug name verbatim). The dose amount EXDOSE is likewise a collected number, copied the same way.

ex <- assign_no_ct(raw_dat = ex_raw, raw_var = "DRUG", tgt_var = "EXTRT") |>
  assign_no_ct(raw_dat = ex_raw, raw_var = "DOSE", tgt_var = "EXDOSE",
               id_vars = oak_id_vars())

ex |>
  select(oak_id, patient_number, EXTRT, EXDOSE) |>
  head()
# A tibble: 6 × 4
  oak_id patient_number EXTRT      EXDOSE
   <int> <chr>          <chr>       <dbl>
1      1 1015           Xanomeline     54
2      2 1015           Xanomeline     54
3      3 1015           Xanomeline     81
4      4 1023           Placebo         0
5      5 1028           Xanomeline     54
6      6 1028           Xanomeline     81

Two SDTM columns exist so far, one record per raw episode: the treatment and its dose amount. The 0-mg rows are the placebo arm — a real EX record, dose and all, because SDTM tabulates what was administered, and “placebo, 0 mg” is an administration.

Map the controlled-terminology dose qualifiers

Now the columns that describe the dose: its unit, dosage form, how often it was given, and by what route. Each is a collected value recoded through controlled terminology, so each uses assign_ct() with the codelist that governs it:

  • EXDOSU (dose unit) — the Unit codelist C71620: mg stays mg.
  • EXDOSFRM (dosage form) — the Pharmaceutical Dosage Form codelist C66726: PatchPATCH.
  • EXDOSFRQ (frequency) — the Frequency codelist C71113: Once dailyQD.
  • EXROUTE (route) — the Route of Administration codelist C66729: TransdermalTRANSDERMAL.
ex <- ex |>
  assign_ct(raw_dat = ex_raw, raw_var = "DOSEU", tgt_var = "EXDOSU",
            ct_spec = ex_ct, ct_clst = "C71620", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ex_raw, raw_var = "FORM", tgt_var = "EXDOSFRM",
            ct_spec = ex_ct, ct_clst = "C66726", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ex_raw, raw_var = "FREQ", tgt_var = "EXDOSFRQ",
            ct_spec = ex_ct, ct_clst = "C71113", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ex_raw, raw_var = "ROUTE", tgt_var = "EXROUTE",
            ct_spec = ex_ct, ct_clst = "C66729", id_vars = oak_id_vars())

ex |>
  select(patient_number, EXTRT, EXDOSE, EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE) |>
  head()
# A tibble: 6 × 7
  patient_number EXTRT      EXDOSE EXDOSU EXDOSFRM EXDOSFRQ EXROUTE    
  <chr>          <chr>       <dbl> <chr>  <chr>    <chr>    <chr>      
1 1015           Xanomeline     54 mg     PATCH    QD       TRANSDERMAL
2 1015           Xanomeline     54 mg     PATCH    QD       TRANSDERMAL
3 1015           Xanomeline     81 mg     PATCH    QD       TRANSDERMAL
4 1023           Placebo         0 mg     PATCH    QD       TRANSDERMAL
5 1028           Xanomeline     54 mg     PATCH    QD       TRANSDERMAL
6 1028           Xanomeline     81 mg     PATCH    QD       TRANSDERMAL

Every collected value is now a standard CDISC term: the form is PATCH, the frequency QD, the route TRANSDERMAL. That is the point of controlled terminology — a reviewer, or an FDA validation tool, reads the same coded values across every study, regardless of how each site phrased them on the CRF.

Note

When a descriptor is the same for every record, hardcode_ct() is the tighter tool. Here every episode is a once-daily transdermal patch, so EXDOSFRM/EXDOSFRQ/EXROUTE could each be hardcoded to their fixed CT value with hardcode_ct() (as the vital-signs lesson does for VSORRESU = "mmHg") instead of recoded from a raw column. We use assign_ct() because a real study collects these per record and can carry more than one form or route — recoding the collected value is the general pattern.

Map the timing variables

A dosing episode has a start and an end, each an ISO 8601 date (--DTC variables). ISO 8601 is the international date standard (2014-01-02) that makes dates sortable and comparable across studies. The raw dates sit in two fields, in a non-standard format; assign_datetime() parses them and writes the compliant column.

ex <- ex |>
  assign_datetime(raw_dat = ex_raw, raw_var = "EXSTDAT", tgt_var = "EXSTDTC",
                  raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars()) |>
  assign_datetime(raw_dat = ex_raw, raw_var = "EXENDAT", tgt_var = "EXENDTC",
                  raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars())

ex |>
  select(patient_number, EXTRT, EXDOSE, EXSTDTC, EXENDTC) |>
  head()
# A tibble: 6 × 5
  patient_number EXTRT      EXDOSE EXSTDTC    EXENDTC   
  <chr>          <chr>       <dbl> <iso8601>  <iso8601> 
1 1015           Xanomeline     54 2014-01-02 2014-01-16
2 1015           Xanomeline     54 2014-01-17 2014-06-18
3 1015           Xanomeline     81 2014-06-19 2014-07-02
4 1023           Placebo         0 2012-08-05 2012-08-27
5 1028           Xanomeline     54 2013-03-11 2013-03-27
6 1028           Xanomeline     81 2013-03-28 2013-05-30

raw_fmt tells sdtm.oak how the raw date was written (02 Jan 2014); it returns the standard 2014-01-02 form. Exposure dates are usually collected to the day, so these are date-only --DTC values — the same assign_datetime() also handles a paired time field and safely returns a partial date when a raw day is unknown, rather than inventing one. The date-handling vignette documents the full set of supported formats and partial-date rules.

Add the identifiers, sequence, and study days

A conformant record needs its identifiers, a sequence number, and its place on the study clock. STUDYID, DOMAIN, and USUBJID are simple assignments; derive_seq() assigns EXSEQ — a unique sequence number per record within a subject; and derive_study_day() computes EXSTDY/ EXENDY, the study day of each date relative to the subject’s reference start date RFSTDTC (carried on the DM demographics domain). Study day 1 is the reference date itself — there is no day 0.

# The reference start date per subject lives on DM; here is the minimal slice derive_study_day needs
dm <- data.frame(
  USUBJID = paste0("CDISCPILOT01-", c("1015","1023","1028","1033","1039","1044")),
  RFSTDTC = c("2014-01-02","2012-08-05","2013-03-11","2013-09-22","2014-02-03","2013-04-09"),
  stringsAsFactors = FALSE
)

ex <- ex |>
  mutate(
    STUDYID = "CDISCPILOT01",
    DOMAIN = "EX",
    USUBJID = paste0(STUDYID, "-", patient_number)
  ) |>
  derive_seq(tgt_var = "EXSEQ", rec_vars = c("USUBJID", "EXTRT", "EXSTDTC")) |>
  derive_study_day(dm_domain = dm, tgdt = "EXSTDTC", refdt = "RFSTDTC", study_day_var = "EXSTDY") |>
  derive_study_day(dm_domain = dm, tgdt = "EXENDTC", refdt = "RFSTDTC", study_day_var = "EXENDY") |>
  select(STUDYID, DOMAIN, USUBJID, EXSEQ, EXTRT, EXDOSE, EXDOSU, EXDOSFRM, EXDOSFRQ,
         EXROUTE, EXSTDTC, EXENDTC, EXSTDY, EXENDY)

head(ex)
# A tibble: 6 × 14
  STUDYID     DOMAIN USUBJID EXSEQ EXTRT EXDOSE EXDOSU EXDOSFRM EXDOSFRQ EXROUTE
  <chr>       <chr>  <chr>   <int> <chr>  <dbl> <chr>  <chr>    <chr>    <chr>  
1 CDISCPILOT… EX     CDISCP…     1 Xano…     54 mg     PATCH    QD       TRANSD…
2 CDISCPILOT… EX     CDISCP…     2 Xano…     54 mg     PATCH    QD       TRANSD…
3 CDISCPILOT… EX     CDISCP…     3 Xano…     81 mg     PATCH    QD       TRANSD…
4 CDISCPILOT… EX     CDISCP…     1 Plac…      0 mg     PATCH    QD       TRANSD…
5 CDISCPILOT… EX     CDISCP…     1 Xano…     54 mg     PATCH    QD       TRANSD…
6 CDISCPILOT… EX     CDISCP…     2 Xano…     81 mg     PATCH    QD       TRANSD…
# ℹ 4 more variables: EXSTDTC <date>, EXENDTC <date>, EXSTDY <int>,
#   EXENDY <int>

That is a conformant EX domain: the SDTM identifier columns first, then the topic (EXTRT), the dose qualifiers, the timing, and the study days. derive_seq() orders the records by rec_vars and numbers them 1, 2, 3… within each subject, so EXSEQ is unique per USUBJID; derive_study_day() places each episode on the study clock (subject -1015’s first patch starts on EXSTDY 1 and the up-titration lands months later), which is what the analysis layer uses to compute exposure duration. A quick structural check confirms the SDTM contract:

required <- c("STUDYID", "DOMAIN", "USUBJID", "EXSEQ", "EXTRT", "EXDOSE")
all(required %in% names(ex))                  # required identifiers + topic present?
[1] TRUE
length(unique(ex$DOMAIN)) == 1                # one domain code?
[1] TRUE
!any(duplicated(ex[c("USUBJID", "EXSEQ")]))   # EXSEQ unique within subject?
[1] TRUE

Three TRUEs: the domain is structurally sound. It also lines up with the real thing — the columns we built (EXTRT, EXDOSE, EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE, EXSTDTC, EXENDTC, EXSTDY, EXENDY) are exactly the exposure variables the published pharmaversesdtm ex reference dataset carries. In production the full check is a conformance tool (Pinnacle 21, or the pharmaverse sdtmchecks package, shown in the vital-signs lesson) that runs the same integrity rules a regulator would. With a conformant EX domain in hand, the analysis layer reads the treatment start and end dates straight from it to build the subject-level ADSL dataset.

🟢 With an AI agent

Ask Prova “how do I collapse consecutive EX dosing episodes to a subject-level total exposure and derive the treatment start and end dates (TRTSDTM/TRTEDTM) for ADSL?” — it answers grounded in this pillar’s lessons, with runnable code you can try on the example data. The runtime is the judge. Ask Prova →

Common issues

You put drug exposure in a Findings domain — reaching for a --TESTCD and a result. That is the class trap: exposure is not a measurement, so it has no test code and no result value. It is an Intervention — a treatment given — so its topic variable is EXTRT (the drug), mapped with assign_no_ct(), qualified by dose and route, not by a test-and-result pair. If you are looking for an EXTESTCD, you have the wrong observation class in mind; map the treatment, not a test.

assign_ct() warns a value “could not be mapped,” and the SDTM column keeps the raw text. The collected value is not in the codelist you pointed at — a real finding, not a bug. A frequent cause is a case or spelling mismatch: sdtm.oak matches collected_value exactly, so a raw "transdermal" will not match a codelist entry recorded as "Transdermal". Check the codelist with subset(ex_ct, codelist_code == "<code>") and confirm its collected_value column contains the raw value exactly as written; sdtm.oak passes the unmapped value through unchanged so you fix the terminology, not the data.

EXSEQ is not unique, or derive_seq() ties two rows. The rec_vars you gave do not uniquely order the records within a subject. For EX, a subject has several consecutive episodes, so order by the treatment plus the start date (c("USUBJID", "EXTRT", "EXSTDTC")); if two episodes could share a start date, add another distinguishing field so every record within a subject sorts to a distinct position.

Frequently asked questions

The SDTM EX (exposure) domain is the CDISC SDTM dataset that records a subject’s exposure to the study treatment — one standardized record per continuous dosing episode, in the Interventions observation class. Each record carries the treatment EXTRT, the dose qualifiers (EXDOSE, EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE), ISO 8601 start/end dates (EXSTDTC, EXENDTC) with their study days (EXSTDY, EXENDY), and identifiers (USUBJID, EXSEQ). It is a required domain in a regulatory submission and the source of the treatment dates and exposure variables in ADSL and the exposure analysis datasets.

They are SDTM’s three general observation classes, split by what a record represents. Interventions records something given to or done to the subject (study drug in EX, concomitant meds in CM) — topic variable --TRT. Events records something that happened (an adverse event in AE, a medical-history condition in MH) — topic variable --TERM. Findings records a measurement or assessment (a vital sign in VS, a lab in LB) — topic variable --TESTCD, with a result. Same sdtm.oak algorithms, three record shapes: for EX you map a treatment with dose qualifiers, not a test-and-result or a reported term.

The dose amount EXDOSE is a collected number, copied as-is with assign_no_ct(). The dose descriptors are under controlled terminology, so each is recoded with assign_ct() pointed at its codelist: the unit EXDOSU (Unit codelist C71620), the dosage form EXDOSFRM (C66726), the frequency EXDOSFRQ (C71113, e.g. “Once daily” → QD), and the route EXROUTE (Route of Administration codelist C66729, e.g. “Transdermal” → TRANSDERMAL). When a descriptor is the same for every record you can hardcode_ct() the fixed value instead. See SDTM Controlled Terminology for the published codelists.

Both are Interventions domains, but EX records the study treatment under investigation — the protocol-specified drug the subject is dosed with — while CM (concomitant medications) records other medications the subject takes alongside it (background therapy, rescue meds). The record shape is the same (a treatment topic variable plus dose qualifiers and timing), so the sdtm.oak mapping pattern in this lesson transfers directly to CM: map CMTRT with assign_no_ct(), the dose qualifiers with assign_ct(), and the dates with assign_datetime().

Use assign_no_ct() for values copied as-is (the treatment EXTRT and the dose amount EXDOSE); assign_ct() for collected values recoded through a codelist (EXDOSU, EXDOSFRM, EXDOSFRQ, EXROUTE) — or hardcode_ct() when that value is fixed for every record; assign_datetime() for the ISO 8601 dates (EXSTDTC, EXENDTC); derive_seq() for EXSEQ; and derive_study_day() for the study days EXSTDY/EXENDY. generate_oak_id_vars() first stamps the lineage key every map joins back on. The algorithms vignette lists the full set.

Test your understanding

Suppose the raw extract gained a rescue arm dosed as an oral tablet: a field FORM collected as Tablet and ROUTE collected as Oral. Sketch how you would extend the mapping so EXDOSFRM and EXROUTE come out as the CDISC standard terms — what changes in the controlled terminology, and which sdtm.oak function stays the same?

EXDOSFRM and EXROUTE are already mapped with assign_ct() — the function does not change. What must change is the study CT: the codelists must contain a collected_valueterm_value row for each new collected value, in the right codelist_code.

Add the new terms to ex_ct — the Pharmaceutical Dosage Form codelist C66726 and the Route of Administration codelist C66729 already govern these variables, so you extend those codelists, not the code:

# add rows to ex_ct: collected -> CDISC standard term
#   codelist_code  collected_value  term_value
#   C66726         "Tablet"         "TABLET"
#   C66729         "Oral"           "ORAL"

The assign_ct() calls for EXDOSFRM and EXROUTE are unchanged — they now recode TabletTABLET and OralORAL because the CT has the entries. That is exactly why sdtm.oak’s algorithms are reusable: the mapping code is fixed, and the study CT carries every allowed value.

A. Because EX is a Findings domain and EXTRT is its test code B. Because EX is an Interventions domain — it records a treatment given, keyed by the treatment name C. Because sdtm.oak requires every domain to use a --TRT topic variable

B. EX is in the Interventions observation class, which records things given to the subject. Its topic variable is therefore the treatment EXTRT (“what was given”), with one record per dosing episode — unlike the Findings class (vital signs, labs), whose topic is a test code --TESTCD (“what was measured”). A has the class wrong (EX is Interventions, not Findings), and C is false — the topic variable depends on the observation class, not a package rule.

Conclusion

Mapping the EX domain is the same one-field-at-a-time loop as any SDTM domain, applied to the Interventions class: fill the right SDTM column from the right raw field with the right algorithm. The topic variable EXTRT names the treatment (copied verbatim with assign_no_ct(), alongside the dose amount EXDOSE); assign_ct() recodes the unit, form, frequency, and route through controlled terminology; assign_datetime() builds the ISO 8601 start and end; derive_seq() numbers the episodes; and derive_study_day() places them on the study clock. Master the Interventions loop here and the concomitant-medications domain (CM) is the same loop with different fields. With a conformant SDTM EX in hand, the next step is the analysis layer, where the treatment start and end dates it carries anchor the subject-level ADSL dataset built with admiral.

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.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {SDTM {EX} {Domain} in {R} with Sdtm.oak: {Map} {Drug}
    {Exposure} to {CDISC} {SDTM}},
  date = {2026-07-01},
  url = {https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-ex-exposure-domain},
  langid = {en}
}
For attribution, please cite this work as:
“SDTM EX Domain in R with Sdtm.oak: Map Drug Exposure to CDISC SDTM.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-ex-exposure-domain.