AI Drafts an admiral ADTTE Derivation From a Plain-English Endpoint

Describe a time-to-event endpoint in one plain-English sentence, let an AI copilot draft the admiral ADTTE derivation in seconds — then run the one-line plausibility check that tells a correct result from a confident, wrong one

Pharma & Clinical

The signature agentic-programming demo. State a time-to-event endpoint in one plain-English sentence and an AI copilot drafts the admiral ADTTE derivation — event_source, censor_source, derive_param_tte — in seconds. Then the real work: validate the draft. A one-line plausibility check catches a confident, runnable derivation whose survival times are all negative, an independent re-derivation confirms the fix with diffdf, and the analysis population stays a human decision. AI drafts the structure fast; you own the endpoint definition and the accountability.

Published

July 2, 2026

Modified

July 7, 2026

TipKey takeaways
  • An AI copilot turns a plain-English endpoint into an admiral derivation in seconds. The scaffold — event_source(), censor_source(), derive_param_tte() — is exactly the repetitive, well-patterned code an assistant drafts well.
  • The draft is unqualified. A submission is a GxP deliverable (the “good practice” quality regulations: GCP, GMP, GLP), and “the code ran” was never the standard for putting a number in front of a regulator.
  • The signature catch: swap two dates and the derivation still runs, still returns 254 tidy rows — and 245 of them are negative survival times. A one-line AVAL >= 0 plausibility check flags it instantly; a glance at the tidy output does not.
  • The analysis population is a human decision. The treatment clock exists for the 254 treated subjects, not all 306 enrolled — the AI drafts the call, but which subjects belong to the endpoint is yours to own.
  • AI changes speed, not accountability. AI-drafted code is unqualified until it passes the same validation gates a human’s must — AI changes the speed of the first draft, never the accountability.

Introduction

A statistician hands you a time-to-event endpoint in one sentence and asks for the analysis dataset. An AI copilot — an AI coding assistant such as Claude, ChatGPT, or GitHub Copilot — will draft the admiral derivation faster than you can open the package documentation. That is genuinely useful: building an ADTTE (the time-to-event analysis dataset, one row per subject) is repetitive, well-patterned work, and the assistant has seen thousands of examples of it.

The trap is just as real. A time-to-event (TTE) endpoint is a precise clinical question encoded as a data structure — when does the clock start, what event stops it, what happens to subjects with no event — and an AI that gets the censoring rule subtly wrong hands you a dataset that runs clean and is quietly nonsense. This lesson walks the whole loop: a plain-English endpoint, the AI-drafted admiral derivation it produces, and — the point of the whole series — the validation that tells a correct result from a confident, wrong one. It is the companion to the capability map that opens this series and to the ADTTE time-to-event build, the from-scratch tutorial an assistant is, in effect, drafting for you here. We work on public pharmaverse example data, so every validation line runs as-is.

The endpoint, in one sentence

Here is the endpoint the statistician wrote, in plain English:

Time from first study-drug exposure (TRTSDT, the treatment start date) to the first treatment-emergent dermatologic adverse event — MedDRA (the Medical Dictionary for Regulatory Activities) system organ class (SOC) "SKIN AND SUBCUTANEOUS TISSUE DISORDERS". Subjects with no such event are censored at their last known alive date (LSTALVDT).

Read it like a QC reviewer, because every clause is a decision the derivation must encode exactly:

  • The origin — when the clock starts (TRTSDT, first exposure).
  • The event — what stops the clock (first treatment-emergent dermatologic adverse event).
  • The censoring rule — what happens to subjects who never have the event (censored at LSTALVDT).
  • The population — who is even eligible for a treatment-anchored clock (treated subjects).

An AI copilot maps that sentence to admiral’s declarative API cleanly. Whether it maps every clause correctly is the question the rest of this lesson answers.

The AI-drafted derivation

Prompted with the endpoint above and told “draft the admiral ADTTE derivation,” an assistant produces the block below. The shape is idiomatic admiral, and it mirrors the canonical BDS time-to-event pattern — the BDS (Basic Data Structure, one row per subject per parameter) layout that the CDISC ADaM (Analysis Data Model) standard defines for survival endpoints.

Three source objects do the work, and it is worth naming each before the code runs:

  • event_source() declares what counts as an event — here, a treatment-emergent adverse event in the dermatologic SOC, dated by its start date ASTDT. It sets CNSR (the censoring flag) to 0.
  • censor_source() declares what counts as censoring — the last-known-alive date for subjects with no event. It sets CNSR to 1.
  • derive_param_tte() takes the subject-level dataset plus those sources and, per subject, picks the earliest event (or, failing that, the censor date), producing the BDS skeleton — STARTDT, ADT (the analysis date the clock stops on), CNSR, PARAMCD, PARAM. A final derive_vars_duration() computes AVAL (the analysis value — here the survival time in days) from STARTDT to ADT.

The endpoint restricts the clock to treated subjects, so the draft filters ADSL (the subject-level analysis dataset) to rows with a treatment start date, and pulls the events from ADAE (the adverse-events analysis dataset). The blocks that follow build on the adsl, base, and adtte objects this one creates.

library(admiral)
library(pharmaverseadam)
library(dplyr)
data("adsl", package = "pharmaverseadam")
data("adae", package = "pharmaverseadam")

# Analysis population = treated subjects (a treatment start date exists)
adsl <- adsl %>%
  filter(!is.na(TRTSDT)) %>%
  select(STUDYID, USUBJID, TRTSDT, LSTALVDT, TRT01A)
adae <- adae %>%
  select(STUDYID, USUBJID, ASTDT, AEBODSYS, TRTEMFL)

# What counts as an EVENT: first treatment-emergent dermatologic AE
derm_ae <- event_source(
  dataset_name  = "adae",
  filter        = AEBODSYS == "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" & TRTEMFL == "Y",
  date          = ASTDT,
  set_values_to = exprs(EVNTDESC = "First dermatologic AE", SRCDOM = "ADAE", SRCVAR = "ASTDT")
)

# What counts as CENSORING: last known alive date
lstalv <- censor_source(
  dataset_name  = "adsl",
  date          = LSTALVDT,
  set_values_to = exprs(EVNTDESC = "Last known alive", SRCDOM = "ADSL", SRCVAR = "LSTALVDT")
)

# The BDS skeleton: one row per subject, earliest event or the censor date
base <- derive_param_tte(
  dataset_adsl      = adsl,
  source_datasets   = list(adsl = adsl, adae = adae),
  start_date        = TRTSDT,
  event_conditions  = list(derm_ae),
  censor_conditions = list(lstalv),
  set_values_to     = exprs(PARAMCD = "TTDERMAE", PARAM = "Time to First Dermatologic AE")
)

# AVAL = duration from the start date to the analysis date, in days
adtte <- base %>%
  derive_vars_duration(new_var = AVAL, start_date = STARTDT, end_date = ADT, out_unit = "days")

head(adtte[, c("USUBJID", "STARTDT", "ADT", "CNSR", "AVAL")])
# A tibble: 6 × 5
  USUBJID     STARTDT    ADT         CNSR  AVAL
  <chr>       <date>     <date>     <int> <dbl>
1 01-701-1015 2014-01-02 2014-07-02     1   182
2 01-701-1023 2012-08-05 2012-08-07     0     3
3 01-701-1028 2013-07-19 2014-01-14     1   180
4 01-701-1033 2014-03-18 2014-04-14     1    28
5 01-701-1034 2014-07-01 2014-12-30     1   183
6 01-701-1047 2013-02-12 2013-04-07     1    55

The structure is right: one row per subject, CNSR following the CDISC convention (0 = the event happened, 1 = censored), AVAL carrying the time in days. Summarize what came out:

cat("Rows (one per subject):    ", nrow(adtte), "\n")
Rows (one per subject):     254 
cat("Events   (CNSR == 0):      ", sum(adtte$CNSR == 0), "\n")
Events   (CNSR == 0):       98 
cat("Censored (CNSR == 1):      ", sum(adtte$CNSR == 1), "\n")
Censored (CNSR == 1):       156 
cat("Median time to event (d):  ", median(adtte$AVAL[adtte$CNSR == 0]), "\n")
Median time to event (d):   29.5 

98 events, 156 censored, a median time to the first dermatologic event of 29.5 days. Clean, plausible, submission-shaped. Now do not trust a line of it until it clears the gate.

Validate: does the population add up?

The cheapest validation is a plausibility check, and the first one is arithmetic: in a one-row-per-subject TTE dataset, every subject is either an event or a censor, so events plus censored must equal the analysis population.

N     <- nrow(adsl)
n_evt <- sum(adtte$CNSR == 0)
n_cns <- sum(adtte$CNSR == 1)

cat("Treated subjects (N):    ", N, "\n")
Treated subjects (N):     254 
cat("Events + censored:       ", n_evt + n_cns, "\n")
Events + censored:        254 
cat("Reconciles:              ", n_evt + n_cns == N, "\n")
Reconciles:               TRUE 

It reconciles — against 254. But the study enrolled more than that, and the gap is not a bug; it is a decision the AI quietly made on your behalf. Look at the full enrolled set:

adsl_enrolled <- pharmaverseadam::adsl   # the full enrolled set, untouched

c(enrolled    = nrow(adsl_enrolled),
  never_dosed = sum(is.na(adsl_enrolled$TRTSDT)),
  treated     = sum(!is.na(adsl_enrolled$TRTSDT)))
   enrolled never_dosed     treated 
        306          52         254 

306 enrolled, of whom 52 have no treatment start date — screen failures who were never dosed. A clock anchored at first study-drug exposure legitimately has no start for them, so a treatment-emergent endpoint has nothing to measure. The draft’s filter(!is.na(TRTSDT)) handled this correctly, but that is the point worth pausing on: the analysis population is a scientific decision the programmer owns, not a line the AI gets to settle silently. Drop the filter and the same code would derive on 306 subjects and quietly fail this reconciliation — a “runs fine” derivation with the wrong denominator behind every downstream survival estimate. The AI can draft the call either way; deciding which subjects belong in the endpoint is yours.

Validate: the plausibility check that catches a confident bug

Now the derivation itself. derive_vars_duration() takes a start_date and an end_date, and an AI copilot — like a tired human — sometimes swaps them. Here is a draft that did, and it is the whole lesson in one output: it runs without a single error or warning.

# The AI-drafted duration line, with start and end dates SWAPPED
adtte_bug <- base %>%
  derive_vars_duration(new_var = AVAL, start_date = ADT, end_date = STARTDT, out_unit = "days")

# One-line plausibility gate: a survival time can never be negative
all(adtte_bug$AVAL >= 0)   # the QC red flag
[1] FALSE
sum(adtte_bug$AVAL < 0)    # how many rows are impossible
[1] 245
min(adtte_bug$AVAL)        # the worst offender, in days
[1] -197

all(adtte_bug$AVAL >= 0) returns FALSE. 245 of the 254 rows are negative, the worst a survival time of −197 days — a subject who had their event nearly seven months before they started treatment. That is physically impossible, and yet the code produced it confidently, in the right shape, with no complaint. A glance at a tidy 254-row table would not catch it; a one-line assertion — quality control (QC) reduced to its essence — catches it in a single FALSE.

Fix the argument order and re-derive:

# The corrected duration line: the clock runs from the start date to the analysis date
adtte_fix <- base %>%
  derive_vars_duration(new_var = AVAL, start_date = STARTDT, end_date = ADT, out_unit = "days")

all(adtte_fix$AVAL >= 0)                        # now TRUE
[1] TRUE
median(adtte_fix$AVAL[adtte_fix$CNSR == 0])     # median time to event, in days
[1] 29.5

TRUE, with a median event time back to a sensible 29.5 days. The bug never announced itself; the plausibility gate did. This is the entire argument of the series in one before-and-after: running is not the same as correct, and a cheap, explicit check is what tells them apart.

Validate: an independent re-derivation with diffdf

A plausibility check catches the impossible. It does not confirm the derivation is right — for that, the same discipline you would apply to any clinical dataset applies here: independent double programming. A second programmer re-derives the ADTTE from the same intent, and the two are compared key-by-key with diffdf(), the double-programming engine from earlier in this series.

library(diffdf)

# An independent re-derivation from the raw pharmaverse data (using :: to avoid clobbering)
adsl2 <- pharmaverseadam::adsl %>%
  filter(!is.na(TRTSDT)) %>%
  select(STUDYID, USUBJID, TRTSDT, LSTALVDT, TRT01A)
adae2 <- pharmaverseadam::adae %>%
  select(STUDYID, USUBJID, ASTDT, AEBODSYS, TRTEMFL)

evt2 <- event_source(
  dataset_name  = "adae",
  filter        = AEBODSYS == "SKIN AND SUBCUTANEOUS TISSUE DISORDERS" & TRTEMFL == "Y",
  date          = ASTDT,
  set_values_to = exprs(EVNTDESC = "First dermatologic AE", SRCDOM = "ADAE", SRCVAR = "ASTDT")
)
cns2 <- censor_source(
  dataset_name  = "adsl",
  date          = LSTALVDT,
  set_values_to = exprs(EVNTDESC = "Last known alive", SRCDOM = "ADSL", SRCVAR = "LSTALVDT")
)
adtte2 <- derive_param_tte(
  dataset_adsl      = adsl2,
  source_datasets   = list(adsl = adsl2, adae = adae2),
  start_date        = TRTSDT,
  event_conditions  = list(evt2),
  censor_conditions = list(cns2),
  set_values_to     = exprs(PARAMCD = "TTDERMAE", PARAM = "Time to First Dermatologic AE")
) %>%
  derive_vars_duration(new_var = AVAL, start_date = STARTDT, end_date = ADT, out_unit = "days")

# The gate: compare the fixed derivation against the independent one, subject by subject
diffdf(adtte, adtte2, keys = "USUBJID")
No issues were found!

“No issues were found!” — the two independent derivations agree exactly. Only now, with the population decided, the plausibility check green, and an independent re-derivation matching, is the ADTTE qualified. The pattern is the one from double programming with diffdf: trust is earned at the gate, the same way it is for any code — regardless of who or what drafted it.

Who owns what

Line up what the AI did and what you did, and the division is exact. The assistant drafted the structure fast — the event_source() / censor_source() / derive_param_tte() scaffold, the BDS variables, the idiomatic admiral call. That is real time saved. But two things it produced were plausible-and-wrong (a swapped-date derivation, and a population it would have decided silently), and a human plus a QC gate caught both.

So the accountability never moved. You own the endpoint definition — is the censoring rule right, is the population right, does the clock start where the protocol says — and you own the validation that proves the derivation implements it. The AI drafts; the sponsor and the qualified programmer remain answerable for every derived value. To put it in the one line this series keeps returning to: AI-drafted code is unqualified until it passes the same validation gates a human’s must — AI changes the speed of the first draft, never the accountability. A submission is a GxP deliverable, and accountability never transfers to the tool.

🟢 With an AI agent

Ask Prova “An AI copilot drafted an admiral ADTTE derivation for a time-to-event endpoint — how do I QC it so a swapped date or the wrong analysis population can’t reach a survival table?” — it answers grounded in this series’ validation lessons, with the runnable plausibility checks and diffdf() double-programming you can try on your own draft. The runtime is the judge. Ask Prova →

Common issues

“The derivation ran, so the ADTTE is fine.” This is the single most dangerous instinct with AI-drafted clinical code. A clean run means the syntax is valid — nothing more. The swapped-date derivation above ran perfectly and returned 254 tidy rows of impossible negative times. Treat a clean run as the start of validation (plausibility checks, an independent re-derivation, review), never the end of it.

Deriving on the enrolled population instead of the treated one. A treatment-anchored TTE endpoint has no clock for never-dosed screen failures. If you skip filter(!is.na(TRTSDT)), the code still runs but events + censored no longer equals your analysis population, and every downstream survival estimate carries the wrong denominator. Decide the population deliberately — it is a scientific choice, not a default the tool should make for you — and reconcile the count.

derive_param_tte() warns about “duplicate records.” This warning is benign: when a subject has several qualifying adverse events, admiral keeps the earliest one (the first event) as the TTE date, which is exactly what a “time to first event” endpoint wants. The default check_type = "warning" is correct; do not silence it with check_type = "none" — read it, confirm the earliest-event behaviour is what you intended, and move on.

Trying to call tte_source(). There is no tte_source() function to call. tte_source is the class that event_source() and censor_source() construct — reach for those two constructors, not a tte_source() that does not exist. (Fabricating a plausible-but-nonexistent function is a classic AI slip; verify every function against the package documentation before you run it.)

Frequently asked questions

As a drafting tool, yes — as a source of validated code, no. An AI copilot can draft an admiral ADTTE derivation (the event_source() / censor_source() / derive_param_tte() scaffold) far faster than you can type it, but that draft is unqualified: a qualified programmer must review every line, and the code must clear the same gates any code clears — plausibility checks, independent double programming with diffdf(), and review. Used that way it speeds up the drafting without weakening the validation.

Almost always a swapped start_date / end_date in derive_vars_duration(). If the analysis date is passed as the start and the treatment start as the end, AVAL is the negation of the real duration, so a survival time comes out negative — an impossible value the code computes without complaint. A one-line all(AVAL >= 0) plausibility check catches it immediately; the fix is to put the start date first and the analysis date second.

It should not, and you should not let it. The analysis population — here, treated subjects with a treatment start date, versus all enrolled subjects — is a scientific decision the programmer and statistician own. An AI copilot will apply a filter (or none) and produce a runnable dataset either way; deciding which subjects belong to a treatment-anchored endpoint, and reconciling events + censored == N, is your responsibility, not the tool’s.

CNSR is the censoring flag, and CDISC’s ADaM convention is the reverse of what many expect: CNSR = 0 means the event happened, and a positive integer (usually 1) means the subject was censored (no event; followed only up to a known date). In admiral, event_source() sets CNSR = 0 and censor_source() sets CNSR = 1. When you analyze the data, remember to convert — for example survival::Surv(AVAL, 1 - CNSR) — because most survival functions expect 1 = event.

Exactly as you validate any clinical code — the source of the draft is irrelevant to the gate. A qualified programmer reads and justifies every line against the endpoint definition, then the code clears plausibility checks (AVAL >= 0, events + censored == N, CNSR in {0, 1}, STARTDT <= ADT), independent double programming compared with diffdf(), and review. The plausibility checks catch the impossible; the independent re-derivation confirms the result is right, not merely runnable.

Test your understanding

An AI copilot drafts an ADTTE derivation and finishes the duration step with derive_vars_duration(new_var = AVAL, start_date = ADT, end_date = STARTDT, out_unit = "days"). The code runs without error and returns a tidy one-row-per-subject table.

  1. In prose: name the one-line plausibility check that catches this before it reaches a survival analysis, and explain why “the code ran” and “the table looks tidy” are not evidence the derivation is correct.
  2. In R: write a tiny two-subject example with a STARTDT and a later ADT, compute AVAL both the swapped way and the correct way, and use an all(AVAL >= 0) assertion to flag the swapped version.

A survival time is a duration from an earlier date to a later one, so it can never be negative. The cheapest gate is therefore all(adtte$AVAL >= 0) — if it returns FALSE, the clock is running backwards. “The code ran” only means the syntax is valid; the swapped-date version runs perfectly and is wrong for almost every subject.

Step 1. The gate is the plausibility assertion all(adtte$AVAL >= 0). A time-to-event value is a duration measured forward from the start date to the analysis date, so a negative AVAL is physically impossible — it means the derivation subtracted in the wrong direction. “The code ran” means only that the syntax is valid, and “the table looks tidy” only that the shape is right; neither says the numbers are correct. The swapped-date draft produces a full table of impossible negative times without any error.

# Two subjects, each with a start date earlier than the analysis date
df <- data.frame(
  USUBJID = c("01-001", "01-002"),
  STARTDT = as.Date(c("2014-01-02", "2014-03-10")),
  ADT     = as.Date(c("2014-07-02", "2014-04-07"))
)

# AI draft (WRONG): start and end swapped -> negative durations
df$AVAL_bug <- as.numeric(df$STARTDT - df$ADT)   # end - start, reversed
all(df$AVAL_bug >= 0)                            # FALSE -> the gate fires

# Correct: duration from the start date to the analysis date
df$AVAL_fix <- as.numeric(df$ADT - df$STARTDT)
all(df$AVAL_fix >= 0)                            # TRUE
df

The all(AVAL >= 0) check returns FALSE for the swapped version and TRUE after the fix — a one-line gate that turns a silent, confident error into an immediate, loud failure.

A. Validated — it ran without error and the table looks correct. B. Unqualified — until a human reviews it and it clears the QC gates (plausibility checks, independent re-derivation, review), it has no validation status. C. Validated, provided the assistant is a current, capable model.

B. Running cleanly and looking tidy are not evidence of correctness — the swapped-date derivation did both and returned 254 impossible negative survival times. AI output is an unqualified draft regardless of which model produced it (ruling out C); it earns validation only through human review plus the same QC gates any code must pass (ruling out A).

Conclusion

An AI copilot is a real accelerator for admiral work: it turns a plain-English time-to-event endpoint into an idiomatic ADTTE derivation in seconds, and the scaffold it produces is the repetitive part you were going to type anyway. But the draft arrives unqualified, and this lesson showed why that word matters — a swapped-date derivation that ran clean and returned a full table of impossible negative survival times, caught not by a careful reading but by a one-line AVAL >= 0 plausibility check, then confirmed with an independent re-derivation and diffdf(). The AI drafts the structure fast; you own the endpoint definition, the analysis population, and the validation that proves the derivation implements them. AI changes the speed of the first draft. It changes nothing about who answers for the result. The runtime is the judge.

Note

This lesson is reproducible: the AI-drafted ADTTE derivation, the swapped-date bug its plausibility check catches, the corrected derivation, and the independent diffdf() re-derivation all reproduce here on public pharmaverse data. Copy any block and run it to reproduce them. The AI drafting step happens in your own tooling; the R that validates the draft is real and executes. The runtime is the judge.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {AI {Drafts} an Admiral {ADTTE} {Derivation} {From} a
    {Plain-English} {Endpoint}},
  date = {2026-07-02},
  url = {https://www.datanovia.com/learn/pharma-clinical/08-agentic-clinical-programming/ai-drafts-admiral-adtte-from-endpoint},
  langid = {en}
}
For attribution, please cite this work as:
“AI Drafts an Admiral ADTTE Derivation From a Plain-English Endpoint.” 2026. July 2. https://www.datanovia.com/learn/pharma-clinical/08-agentic-clinical-programming/ai-drafts-admiral-adtte-from-endpoint.