SDTM AE Domain in R with sdtm.oak: Map Adverse Events to CDISC SDTM

Map a raw adverse-event EDC extract into a CDISC-conformant SDTM AE domain in R with sdtm.oak — the Events observation class, controlled-terminology qualifiers, ISO 8601 dates, and the sequence variable

Pharma & Clinical

A complete, runnable tutorial for the SDTM AE (adverse events) domain in R. Learn what the CDISC SDTM Events observation class is (one record per event, keyed by the topic variable AETERM), how it differs from the Findings class, then map a raw adverse-event extract into a conformant AE domain with sdtm.oak: generate_oak_id_vars() for record lineage, assign_no_ct() for the verbatim term and the MedDRA-coded AEDECOD/AEBODSYS, assign_ct() for the controlled-terminology qualifiers (AESEV/AESER/AEREL/AEOUT), assign_datetime() for ISO 8601 AESTDTC/AEENDTC, and derive_seq() for AESEQ — every line runnable, feeding the downstream ADAE dataset.

Published

July 1, 2026

Modified

July 7, 2026

TipKey takeaways
  • AE is an Events domain, not a Findings one. The Findings class (vital signs, labs) records measurements keyed by a test code; the Events class records things that happened — one row per adverse event, keyed by the reported term AETERM.
  • The topic variable is AETERM (free text), and it carries no controlled terminology. assign_no_ct() copies the verbatim reported term as collected; the coded AEDECOD and AEBODSYS come from MedDRA coding done upstream, so they too are copied, not recoded.
  • The qualifiers are controlled. assign_ct() recodes the collected severity, seriousness, relationship, and outcome to their CDISC standard terms (AESEV, AESER, AEREL, AEOUT).
  • Timing is two ISO 8601 dates. assign_datetime() builds the start (AESTDTC) and end (AEENDTC) from the raw date/time fields — and safely returns a partial date when the raw day is unknown, rather than inventing one.
  • derive_seq() numbers the events. AESEQ is unique per subject, and the whole domain becomes the standard input the downstream ADAE analysis dataset reads from.

Introduction

Adverse events are the safety backbone of every clinical trial. Every study collects them, every regulator scrutinizes them, and the reviewer who decides whether a drug is safe reads them through one standardized dataset: the SDTM (Study Data Tabulation Model) AE (adverse events) domain. What the site actually records is messier — a CRF (case report form) page with a free-text reported term, a severity ticked as “Mild”, a start date typed however the coordinator typed it. The job here is to turn that raw extract into a conformant AE domain a regulator 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 is the Events counterpart to mapping a Findings domain (vital signs) with sdtm.oak: the same oak algorithms, a different observation class. SDTM is the upstream of the clinical data flow — raw → SDTM → ADaM → tables, listings, and figures — so a clean AE domain here is what makes the next step, building the ADAE adverse-events analysis dataset with admiral, straightforward.

Here is where we are headed — the conformant AE domain we will build, summarized by body system and severity straight off the mapped result:

Stacked horizontal bar chart of the mapped SDTM AE domain. Each bar is one system organ class (Nervous system disorders, Gastrointestinal disorders, General disorders, Skin and subcutaneous tissue disorders, Psychiatric disorders) on the y axis, its length the number of adverse events on the x axis, segmented by severity (mild in light blue, moderate in mid blue, severe in dark blue). Nervous system and gastrointestinal disorders each carry four events spanning mild to severe; the smaller body systems carry one or two. The point is that a raw adverse-event extract has become a tidy, standardized set of events that is immediately queryable by body system and severity.

Every segment there is one SDTM record — a reported event, coded to a body system, graded for severity. 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 Events 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 CM (con meds), EX (exposure) --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 companion vital-signs lesson mapped a Findings domain, where the topic variable is a test code (VSTESTCD = “what was measured”) and each record answers “what was measured and what was the result.” Events are different. An adverse event is not a measurement — it is an occurrence, so the topic variable is the reported term AETERM (“what happened”), and there is one record per event, not per assessment. That single difference — term instead of test code, event instead of measurement — is the whole distinction between the two classes.

An AE record is that topic term plus its qualifiers, identifiers, and timing:

Variable role AE example What it carries
Identifiers STUDYID, USUBJID, DOMAIN, AESEQ Study, subject, domain code (AE), and a unique sequence number per record.
Topic AETERM The verbatim reported term exactly as collected on the CRF.
Synonym / dictionary AEDECOD, AEBODSYS The MedDRA preferred term (PT) and system organ class (SOC) — the coded event.
Result qualifiers AESEV, AESER, AEREL, AEOUT Severity, seriousness, relationship to study drug, and outcome.
Timing AESTDTC, AEENDTC Start and end date/time in ISO 8601 (--DTC variables).

Two conventions carry the whole mapping. First, the topic variable (AETERM) names the observation and everything else qualifies it. Second, most qualifier columns are restricted to controlled terminology (CT) — CDISC-published codelists that fix the allowed values (AESEV must be MILD, not “mild” or “1”). 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, done for a different record shape.

The raw adverse-event 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 adverse events, so we build a small raw AE extract in the shape an EDC (electronic data capture) system exports one — one wide row per event, sponsor-chosen column names, dates in the format the form used.

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

# A raw adverse-event EDC extract: one row per event, as collected
ae_raw <- data.frame(
  PATNUM   = c("375","375","375","376","376","377","377","377","378","378","379","379"),
  AESPID   = c("AE01","AE02","AE03","AE01","AE02","AE01","AE02","AE03","AE01","AE02","AE01","AE02"),
  AETERM_R = c("HEADACHE","Nausea","Insomnia","Fatigue","MILD RASH","Dizziness",
               "Headache","Vomiting","Diarrhoea","Headache","Nausea","Pyrexia"),
  AEPT_R   = c("Headache","Nausea","Insomnia","Fatigue","Rash","Dizziness",
               "Headache","Vomiting","Diarrhoea","Headache","Nausea","Pyrexia"),
  AESOC_R  = c("Nervous system disorders","Gastrointestinal disorders","Psychiatric disorders",
               "General disorders","Skin and subcutaneous tissue disorders","Nervous system disorders",
               "Nervous system disorders","Gastrointestinal disorders","Gastrointestinal disorders",
               "Nervous system disorders","Gastrointestinal disorders","General disorders"),
  AESTDAT  = c("05 Jan 2023","07 Jan 2023","10 Jan 2023","12 Feb 2023","13 Feb 2023","02 Mar 2023",
               "04 Mar 2023","06 Mar 2023","09 Jan 2023","15 Jan 2023","21 Feb 2023","25 Feb 2023"),
  AESTTIM  = c("08:30","14:10","22:00","09:00","20:45","07:15","11:00","16:20","13:30","08:00","19:45","06:30"),
  AEENDAT  = c("06 Jan 2023","07 Jan 2023","12 Jan 2023","UN UNK 2023","20 Feb 2023","02 Mar 2023",
               "05 Mar 2023","06 Mar 2023","11 Jan 2023","16 Jan 2023","22 Feb 2023","27 Feb 2023"),
  AEENTIM  = c("10:00","18:30","07:00","","12:00","23:00","09:00","20:00","10:00","12:00","08:00","14:00"),
  SEV      = c("Mild","Moderate","Mild","Moderate","Mild","Severe","Moderate","Severe","Moderate","Mild","Moderate","Mild"),
  SER      = c("No","No","No","No","No","Yes","No","Yes","No","No","No","No"),
  REL      = c("Not related","Related","Not related","Not related","Related","Related",
               "Not related","Related","Related","Not related","Related","Not related"),
  OUT      = c("Recovered","Recovered","Recovered","Not recovered","Recovered","Recovering",
               "Recovered","Recovered","Recovered","Recovered","Recovered","Recovered"),
  stringsAsFactors = FALSE
)

ae_raw |>
  select(PATNUM, AETERM_R, AESOC_R, AESTDAT, SEV, SER, OUT) |>
  head()
  PATNUM  AETERM_R                                AESOC_R     AESTDAT      SEV
1    375  HEADACHE               Nervous system disorders 05 Jan 2023     Mild
2    375    Nausea             Gastrointestinal disorders 07 Jan 2023 Moderate
3    375  Insomnia                  Psychiatric disorders 10 Jan 2023     Mild
4    376   Fatigue                      General disorders 12 Feb 2023 Moderate
5    376 MILD RASH Skin and subcutaneous tissue disorders 13 Feb 2023     Mild
6    377 Dizziness               Nervous system disorders 02 Mar 2023   Severe
  SER           OUT
1  No     Recovered
2  No     Recovered
3  No     Recovered
4  No Not recovered
5  No     Recovered
6 Yes    Recovering

Each raw row is one reported event: a verbatim term (AETERM_R), a pre-coded preferred term and body system (AEPT_R, AESOC_R), a start and end date/time, and the collected severity, seriousness, and outcome. 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 AE codelists we need:

# Study controlled terminology: collected value -> CDISC standard term, by codelist
ae_ct <- data.frame(
  codelist_code = c("C66769","C66769","C66769", "C66742","C66742",
                    "C66768","C66768","C66768", "STUDYREL","STUDYREL"),
  term_code     = c("C41338","C41339","C41340", "C49488","C49487",
                    "C49498","C49496","C49494", "R01","R02"),
  term_value    = c("MILD","MODERATE","SEVERE",           # C66769  AESEV severity
                    "Y","N",                              # C66742  no/yes
                    "RECOVERED/RESOLVED","RECOVERING/RESOLVING","NOT RECOVERED/NOT RESOLVED",  # C66768 AEOUT
                    "NOT RELATED","RELATED"),             # STUDYREL  study relationship
  collected_value = c("Mild","Moderate","Severe", "Yes","No",
                      "Recovered","Recovering","Not recovered", "Not related","Related"),
  term_preferred_term = NA_character_, term_synonyms = NA_character_,
  stringsAsFactors = FALSE
)

ae_ct |> select(codelist_code, term_value, collected_value)
   codelist_code                 term_value collected_value
1         C66769                       MILD            Mild
2         C66769                   MODERATE        Moderate
3         C66769                     SEVERE          Severe
4         C66742                          Y             Yes
5         C66742                          N              No
6         C66768         RECOVERED/RESOLVED       Recovered
7         C66768       RECOVERING/RESOLVING      Recovering
8         C66768 NOT RECOVERED/NOT RESOLVED   Not recovered
9       STUDYREL                NOT RELATED     Not related
10      STUDYREL                    RELATED         Related

collected_value is what the CRF recorded; term_value is the CDISC-standard term sdtm.oak will substitute. The severity, seriousness, and outcome codelists (C66769, C66742, C66768) are CDISC standard; relationship to study drug has no single CDISC codelist, so studies define their own — here STUDYREL. 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.

ae_raw <- ae_raw |>
  generate_oak_id_vars(pat_var = "PATNUM", raw_src = "ae_raw")

ae_raw |>
  select(oak_id, raw_source, patient_number, AETERM_R) |>
  head()
  oak_id raw_source patient_number  AETERM_R
1      1     ae_raw            375  HEADACHE
2      2     ae_raw            375    Nausea
3      3     ae_raw            375  Insomnia
4      4     ae_raw            376   Fatigue
5      5     ae_raw            376 MILD RASH
6      6     ae_raw            377 Dizziness

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

An Events record begins with its topic — the reported term. Unlike a Findings test code, AETERM is free text under no controlled terminology, so we copy the collected value verbatim with assign_no_ct() (“assign”, because it copies a collected value; “no_ct”, because there is no codelist to recode through).

ae <- assign_no_ct(
  raw_dat = ae_raw,
  raw_var = "AETERM_R",
  tgt_var = "AETERM"
)

ae |>
  select(oak_id, patient_number, AETERM) |>
  head()
# A tibble: 6 × 3
  oak_id patient_number AETERM   
   <int> <chr>          <chr>    
1      1 375            HEADACHE 
2      2 375            Nausea   
3      3 375            Insomnia 
4      4 376            Fatigue  
5      5 376            MILD RASH
6      6 377            Dizziness

One SDTM column exists so far, one record per raw event. Notice AETERM keeps the verbatim casing (HEADACHE, MILD RASH) exactly as the site typed it — SDTM preserves the reported term unchanged; standardization happens in the coded columns, next.

Map the MedDRA-coded terms

AEDECOD (the preferred term) and AEBODSYS (the system organ class) are the coded view of the event. They come from MedDRA (the Medical Dictionary for Regulatory Activities, the global standard for coding adverse events — meddra.org): a medical coder maps each verbatim AETERM to a MedDRA PT (preferred term) and its SOC (system organ class). That coding happens upstream of this mapping, so in the raw extract the coded values already sit in AEPT_R and AESOC_R — we copy them as-is with assign_no_ct(); sdtm.oak maps the structure, it does not do the dictionary coding.

ae <- ae |>
  assign_no_ct(raw_dat = ae_raw, raw_var = "AEPT_R",
               tgt_var = "AEDECOD", id_vars = oak_id_vars()) |>
  assign_no_ct(raw_dat = ae_raw, raw_var = "AESOC_R",
               tgt_var = "AEBODSYS", id_vars = oak_id_vars())

ae |>
  select(patient_number, AETERM, AEDECOD, AEBODSYS) |>
  head()
# A tibble: 6 × 4
  patient_number AETERM    AEDECOD   AEBODSYS                              
  <chr>          <chr>     <chr>     <chr>                                 
1 375            HEADACHE  Headache  Nervous system disorders              
2 375            Nausea    Nausea    Gastrointestinal disorders            
3 375            Insomnia  Insomnia  Psychiatric disorders                 
4 376            Fatigue   Fatigue   General disorders                     
5 376            MILD RASH Rash      Skin and subcutaneous tissue disorders
6 377            Dizziness Dizziness Nervous system disorders              
Note

MedDRA coding is a separate step, not part of the mapping. Deriving AEDECOD/AEBODSYS requires a licensed MedDRA dictionary and medical judgment, and is done in dedicated coding software before SDTM programming. This lesson assumes the coding is complete (the raw extract carries the coded columns) and maps them into the SDTM structure — which is exactly how a real AE domain is built.

Map the controlled-terminology qualifiers

Now the columns that qualify each event: how bad it was, whether it was serious, whether it was related to the study drug, and how it resolved. Each is a collected value recoded through controlled terminology, so each uses assign_ct() with the codelist that governs it:

  • AESEV (severity) — codelist C66769: Mild/Moderate/SevereMILD/MODERATE/SEVERE.
  • AESER (serious) — the no/yes codelist C66742: No/YesN/Y.
  • AEREL (relationship to study drug) — the study codelist STUDYREL.
  • AEOUT (outcome) — codelist C66768: RecoveredRECOVERED/RESOLVED, and so on.
ae <- ae |>
  assign_ct(raw_dat = ae_raw, raw_var = "SEV", tgt_var = "AESEV",
            ct_spec = ae_ct, ct_clst = "C66769", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ae_raw, raw_var = "SER", tgt_var = "AESER",
            ct_spec = ae_ct, ct_clst = "C66742", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ae_raw, raw_var = "REL", tgt_var = "AEREL",
            ct_spec = ae_ct, ct_clst = "STUDYREL", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = ae_raw, raw_var = "OUT", tgt_var = "AEOUT",
            ct_spec = ae_ct, ct_clst = "C66768", id_vars = oak_id_vars())

ae |>
  select(patient_number, AETERM, AESEV, AESER, AEREL, AEOUT) |>
  head()
# A tibble: 6 × 6
  patient_number AETERM    AESEV    AESER AEREL       AEOUT                     
  <chr>          <chr>     <chr>    <chr> <chr>       <chr>                     
1 375            HEADACHE  MILD     N     NOT RELATED RECOVERED/RESOLVED        
2 375            Nausea    MODERATE N     RELATED     RECOVERED/RESOLVED        
3 375            Insomnia  MILD     N     NOT RELATED RECOVERED/RESOLVED        
4 376            Fatigue   MODERATE N     NOT RELATED NOT RECOVERED/NOT RESOLVED
5 376            MILD RASH MILD     N     RELATED     RECOVERED/RESOLVED        
6 377            Dizziness SEVERE   Y     RELATED     RECOVERING/RESOLVING      

Every collected value is now a standard CDISC term: AESEV is MILD/MODERATE/SEVERE, AESER is N/Y. 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.

Map the timing variables

An event has a start and an end, each an ISO 8601 date/time (--DTC variables). ISO 8601 is the international date standard (2023-01-05T08:30) that makes dates sortable and comparable across studies. The raw date and time sit in two fields, in a non-standard format; assign_datetime() parses them and writes the compliant column.

ae <- ae |>
  assign_datetime(
    raw_dat = ae_raw,
    raw_var = c("AESTDAT", "AESTTIM"),
    tgt_var = "AESTDTC",
    raw_fmt = c(list(c("d-m-y", "dd mmm yyyy")), "H:M")
  ) |>
  assign_datetime(
    raw_dat = ae_raw,
    raw_var = c("AEENDAT", "AEENTIM"),
    tgt_var = "AEENDTC",
    raw_fmt = c(list(c("d-m-y", "dd mmm yyyy")), "H:M")
  )

ae |>
  select(patient_number, AETERM, AESTDTC, AEENDTC) |>
  head()
# A tibble: 6 × 4
  patient_number AETERM    AESTDTC          AEENDTC         
  <chr>          <chr>     <iso8601>        <iso8601>       
1 375            HEADACHE  2023-01-05T08:30 2023-01-06T10:00
2 375            Nausea    2023-01-07T14:10 2023-01-07T18:30
3 375            Insomnia  2023-01-10T22:00 2023-01-12T07:00
4 376            Fatigue   2023-02-12T09:00 2023            
5 376            MILD RASH 2023-02-13T20:45 2023-02-20T12:00
6 377            Dizziness 2023-03-02T07:15 2023-03-02T23:00

raw_fmt tells sdtm.oak how the raw date and time were written (05 Jan 2023, 08:30); it returns the standard 2023-01-05T08:30 form. Look at the fourth row: its raw end date was UN UNK 2023 (day and month unknown), and AEENDTC becomes just 2023 — a valid partial ISO 8601 date. This is the correct behaviour: SDTM records the precision that was actually collected and never invents a day. The date-handling vignette documents the full set of supported formats and partial-date rules.

Add the identifiers and the sequence number

A conformant record needs its identifiers and a sequence number. STUDYID, DOMAIN, and USUBJID are simple assignments; derive_seq() assigns AESEQ — a unique sequence number per record within a subject, an SDTM requirement.

ae <- ae |>
  mutate(
    STUDYID = "CDISC01",
    DOMAIN = "AE",
    USUBJID = paste0(STUDYID, "-", patient_number)
  ) |>
  derive_seq(
    tgt_var = "AESEQ",
    rec_vars = c("USUBJID", "AETERM", "AESTDTC")
  ) |>
  select(STUDYID, DOMAIN, USUBJID, AESEQ, AETERM, AEDECOD, AEBODSYS,
         AESEV, AESER, AEREL, AEOUT, AESTDTC, AEENDTC)

head(ae)
# A tibble: 6 × 13
  STUDYID DOMAIN USUBJID   AESEQ AETERM AEDECOD AEBODSYS AESEV AESER AEREL AEOUT
  <chr>   <chr>  <chr>     <int> <chr>  <chr>   <chr>    <chr> <chr> <chr> <chr>
1 CDISC01 AE     CDISC01-…     1 HEADA… Headac… Nervous… MILD  N     NOT … RECO…
2 CDISC01 AE     CDISC01-…     2 Insom… Insomn… Psychia… MILD  N     NOT … RECO…
3 CDISC01 AE     CDISC01-…     3 Nausea Nausea  Gastroi… MODE… N     RELA… RECO…
4 CDISC01 AE     CDISC01-…     1 Fatig… Fatigue General… MODE… N     NOT … NOT …
5 CDISC01 AE     CDISC01-…     2 MILD … Rash    Skin an… MILD  N     RELA… RECO…
6 CDISC01 AE     CDISC01-…     1 Dizzi… Dizzin… Nervous… SEVE… Y     RELA… RECO…
# ℹ 2 more variables: AESTDTC <iso8601>, AEENDTC <iso8601>

That is a conformant AE domain: the SDTM identifier columns first, then the topic (AETERM), the coded terms, the qualifiers, and the timing. derive_seq() orders the records by rec_vars and numbers them 1, 2, 3… within each subject, so AESEQ is unique per USUBJID — the key every downstream join relies on. A quick structural check confirms the SDTM contract:

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

Three TRUEs: the domain is structurally sound. 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 AE domain in hand, the next stage reads straight from it: building ADAE, the adverse-events analysis dataset, with admiral adds the treatment-emergent flag and occurrence flags on top of exactly these columns.

🟢 With an AI agent

Ask Prova “how do I add the AEACN action-taken and AEACNOTH qualifiers to this SDTM AE domain, and which controlled-terminology codelist governs AEACN?” — it answers grounded in this pillar’s lessons, with runnable sdtm.oak code you can try on the example data. The runtime is the judge. Ask Prova →

Common issues

You reach for a test code and there is none — the AE has no --TESTCD. That is the Events-class trap: you are thinking in Findings terms. An adverse event is an occurrence, not a measurement, so its topic variable is the reported term AETERM, mapped with assign_no_ct() (free text, no CT) — there is no AETESTCD. If you find yourself looking for a test-code codelist for AE, you have the wrong observation class in mind; map the term, not a test.

A partial or missing date makes assign_datetime() return a short string like 2023 — and you assume it failed. It did not. When the raw day or month is unknown (UN UNK 2023), the correct ISO 8601 result is the truncated 2023 (or 2023-01) — SDTM records only the precision collected and never fabricates a day. Do not “fix” it by padding with 01; a partial --DTC is conformant, and the downstream analysis dataset imputes analysis dates from it deliberately.

assign_ct() warns that 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. sdtm.oak passes the raw value through unchanged and tells you, so you fix the terminology, not the data. Check the codelist with subset(ae_ct, codelist_code == "<code>") and confirm its collected_value column actually contains the raw value you are mapping (a common cause is a case or spelling mismatch, e.g. "YES" vs "Yes").

Frequently asked questions

The SDTM AE domain is the CDISC SDTM dataset that holds a study’s adverse events — one standardized record per event, in the Events observation class. Each record carries the reported term AETERM, the MedDRA-coded AEDECOD and AEBODSYS, controlled-terminology qualifiers (AESEV, AESER, AEREL, AEOUT), ISO 8601 start/end dates (AESTDTC, AEENDTC), and identifiers (USUBJID, AESEQ). It is a required domain in a regulatory submission and the source for the ADAE analysis dataset.

Findings records a measurement (a lab value, a blood pressure) — its topic variable is a test code (--TESTCD), and each record answers “what was measured and what was the result.” Events records something that happened (an adverse event, a medical-history condition) — its topic variable is a term (--TERM, e.g. AETERM), with one record per event. Same sdtm.oak algorithms, different record shape: for AE you map a reported term, not a test-code-plus-result.

AETERM is the verbatim reported term — the adverse event exactly as the investigator wrote it on the CRF (“MILD RASH”). AEDECOD is the MedDRA preferred term (PT) it was coded to (“Rash”) — the standardized dictionary term that lets you group and count events consistently across sites and studies. AETERM is copied as-is with assign_no_ct(); AEDECOD (and the system organ class AEBODSYS) come from MedDRA coding done before SDTM programming.

No. sdtm.oak maps the structure — it moves the already-coded AEDECOD/AEBODSYS from the raw extract into the SDTM columns. MedDRA coding itself (assigning a preferred term and system organ class to each verbatim term) needs a licensed MedDRA dictionary and medical judgment, and is done in dedicated coding software upstream. By the time you map to SDTM, the coded columns already exist in the raw data.

Use assign_no_ct() for values copied as-is (the free-text AETERM, and the pre-coded AEDECOD/ AEBODSYS); assign_ct() for collected values recoded through a codelist (AESEV, AESER, AEREL, AEOUT); assign_datetime() for the ISO 8601 dates (AESTDTC, AEENDTC); and derive_seq() for AESEQ. generate_oak_id_vars() first stamps the lineage key every map joins back on. The algorithms vignette lists the full set.

Test your understanding

The raw extract does not yet carry action taken with study drug, but suppose it did, in a field ACN collected as Dose Not Changed / Drug Withdrawn / Dose Reduced. Sketch the mapping for the SDTM AEACN qualifier: which sdtm.oak function would you use, and what would the controlled-terminology step look like?

AEACN is a collected value that must be recoded to CDISC standard terms — so it is the same pattern as AESEV/AEOUT. You need the function that recodes through a codelist, plus a codelist entry mapping each collected value to its term_value.

Use assign_ct(), with an entry in the study CT (CDISC codelist C66767 governs AEACN) that maps each collected value to its standard term:

# add C66767 rows to ae_ct: collected -> standard
#   "Dose Not Changed" -> "DOSE NOT CHANGED"
#   "Drug Withdrawn"   -> "DRUG WITHDRAWN"
#   "Dose Reduced"     -> "DOSE REDUCED"
ae <- ae |>
  assign_ct(raw_dat = ae_raw, raw_var = "ACN", tgt_var = "AEACN",
            ct_spec = ae_ct, ct_clst = "C66767", id_vars = oak_id_vars())

It is the identical algorithm chain as the other qualifiers, with the raw field, target variable, and codelist swapped — which is exactly why sdtm.oak’s algorithms are reusable: every CT qualifier maps the same way.

A. Because AE is a Findings domain and AETERM is its test code B. Because AE is an Events domain — it records occurrences, keyed by the reported term C. Because sdtm.oak requires every domain to use a --TERM topic variable

B. AE is in the Events observation class, which records things that happened. Its topic variable is therefore the reported term AETERM (“what happened”), with one record per event — unlike the Findings class (vital signs, labs), whose topic is a test code --TESTCD (“what was measured”). A has the class wrong (AE is Events, not Findings), and C is false — the topic variable depends on the observation class, not a package rule.

Conclusion

Mapping the AE domain is the same one-field-at-a-time loop as any SDTM domain, applied to the Events class: fill the right SDTM column from the right raw field with the right algorithm. The topic variable AETERM names each event (copied verbatim with assign_no_ct()); the MedDRA-coded AEDECOD/ AEBODSYS are copied from upstream coding; assign_ct() recodes the severity, seriousness, relationship, and outcome qualifiers through controlled terminology; assign_datetime() builds the ISO 8601 start and end — honouring partial dates; and derive_seq() numbers the events. Master the Events loop here and every event-type domain — medical history, disposition — is the same loop with different fields. With a conformant SDTM AE in hand, the next step is the analysis layer: building the ADAE adverse-events dataset 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 {AE} {Domain} in {R} with Sdtm.oak: {Map} {Adverse}
    {Events} to {CDISC} {SDTM}},
  date = {2026-07-01},
  url = {https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-ae-adverse-events-domain},
  langid = {en}
}
For attribution, please cite this work as:
“SDTM AE Domain in R with Sdtm.oak: Map Adverse Events to CDISC SDTM.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-ae-adverse-events-domain.