Auditable AI Workflows in Clinical Programming: The End-to-End Reproducible Audit Record

Assemble one AI-assisted derivation’s prompt, AI draft, QC gate, and human approval into a single reproducible, tamper-evident audit record — hashes plus environment plus a fixed timestamp — so an AI-drafted deliverable meets the same data-integrity and audit-trail expectations as human-authored code

Pharma & Clinical

The capstone of the agentic clinical programming series: the end-to-end, auditable workflow that ties the whole series together. For one AI-assisted BMI derivation, this lesson captures the prompt reference, the AI-drafted code, the independent QC gate result, the environment, and the human approval into a single reproducible provenance record — content hashes plus package versions plus a fixed timestamp — built with runnable base R (digest, diffdf). It shows both branches: a buggy AI draft the QC gate fails records REJECTED, and a corrected draft that passes records APPROVED; flipping one input value changes the content hash, so any tampering is evident. The record is then mapped to the ALCOA+ data-integrity principles. The honest boundary throughout: the audit trail records what happened, makes it reproducible, and makes changes tamper-evident — it does not make the AI output correct. Human review plus a passing gate is what qualifies it.

Published

July 2, 2026

Modified

July 7, 2026

TipKey takeaways
  • A submission needs the whole AI-assisted loop to be traceable. Across this series an AI copilot drafted derivations, tables, and metadata, and an independent gate caught its errors. The capstone assembles that loop — prompt → AI draft → validate → approve — into one audit trail: a durable, reproducible record of who did what, with which code, on which version, and who approved it.
  • The record is genuine computation, not paperwork. Content hashes (SHA-256 fingerprints) of the input data, the AI-drafted code, and the QC result — plus package versions and a fixed timestamp — assemble into a single provenance record with digest and diffdf. Run it twice and it reproduces byte-for-byte.
  • The record tells the truth, both ways. A buggy AI draft fails the QC gate and is logged REJECTED; the corrected draft passes and is logged APPROVED. The audit trail records what actually happened — it does not rubber-stamp the AI.
  • Tamper-evident, not a signature. Change one input value and the content hash changes, so any later alteration no longer matches the log. The hash is an integrity checksum, not a cryptographic signature — digest’s own documentation says it is not for cryptographic use.
  • Logged is necessary, not sufficient. The audit trail records, reproduces, and tamper-evidences the work; it does not make the AI output correct. Human review plus a passing gate is what qualifies it — an AI-drafted deliverable is not an AI-validated one.

Introduction

Across this series an AI copilot did real drafting work: it wrote an admiral time-to-event derivation, a tern adverse-event table, Define-XML metadata, and an independent QC re-derivation. Every time, the same discipline held — the AI output was an unqualified draft until an independent gate checked it, and a human owned the approval. Five lessons, five drafts, five gates.

A regulatory submission asks one more thing of that work: it must be traceable. Not just “the number is right,” but who produced it, from what prompt, with which code, on which software version, checked how, and approved by whom — and it must stay that way, verifiable years later by an inspector who was never in the room. That traceability is the audit trail: the durable, ordered record of what happened to a piece of data. This is a GxP requirement — the “good practice” quality regulations that govern clinical data — and it applies to AI-assisted code exactly as it applies to human-authored code.

This capstone builds that record. For one AI-assisted derivation it captures the loop — prompt → AI draft → validate → approve — as a single provenance record: a compact, reproducible description of an artifact’s origin and the steps that produced it. Everything here runs in base R. The AI prompt and draft are shown as illustration (drafted in your own tooling); the R that hashes the artifacts, runs the QC gate, and assembles the record is real and executes.

Note

This capstone is the record; the SOP is the policy. The governance procedure — the roles, the prompt-to-approval workflow, and the mapping to FDA and GAMP 5 expectations — lives in Validating AI-Generated Code in Clinical Programming: An Auditable SOP. The SOP says what the policy is; this lesson shows the tamper-evident record the policy produces. Read the SOP for the procedure; read this for the concrete, runnable artifact.

The loop, made into a record

The workflow every lesson in this series followed is four steps:

1 · Prompt A human writes the spec/prompt · references the SAP or ADaM spec

2 · AI draft The copilot returns R code · captured verbatim as text

3 · Validate An independent QC gate runs · diffdf PASS / FAIL

4 · Approve A qualified human reviews · signs off · APPROVED / REJECTED

A sign-off (or approval) is a named, qualified person attesting that the check was performed and passed, and answering for it in an audit. To make the loop auditable we capture each step as a value we can store and re-verify: the prompt reference, the AI-drafted code, the QC result, the environment, the fixed metadata — and, crucially, content hashes of the artifacts so any later change is detectable.

We use a small ADSL (Subject-Level Analysis Dataset — one row per subject) with height and weight, and the derivation under audit is a baseline BMI (body mass index — weight in kilograms over height in metres squared). Height is in centimetres, so it must be divided by 100 before squaring.

Assembling the provenance record

The block below is the whole capture, top to bottom, and every value in the final table is computed, not typed. It builds the input data, captures the AI-drafted derivation as text, runs an independent QC re-derivation from the same spec, gates the two with diffdf(), hashes each artifact, reads the environment, and assembles one record.

A few deterministic choices make this a durable log rather than a snapshot that changes every run. The input values are fixed literals. The environment is captured from version strings only (R.version.string, packageVersion()) — never a wall clock. The timestamp is a hard-coded illustrative string, not Sys.time(). As a result the record — and its hashes — reproduce byte-for-byte on every render, which is exactly what an audit trail must do. We hash with SHA-256, a fixed-length checksum (an integrity fingerprint) of the bytes it is given: identical input always yields the identical fingerprint, and any change yields a different one.

library(digest); library(diffdf)

# 1. Input data: a small ADSL, fixed in-block values => deterministic
adsl <- data.frame(
  USUBJID = sprintf("01-%03d", 1:6),
  HEIGHT  = c(170, 165, 180, 158, 175, 168),   # cm
  WEIGHT  = c( 72,  60,  85,  55,  90,  68),    # kg
  stringsAsFactors = FALSE)

# 2. The AI-drafted derivation, captured VERBATIM as text (illustrative -- drafted in your tooling).
#    This text string is itself an artifact we hash, so the log records exactly what code the AI returned.
ai_draft_code <- "ai_out$BMI <- round(ai_out$WEIGHT / (ai_out$HEIGHT / 100)^2, 1)"
ai_out <- adsl; eval(parse(text = ai_draft_code))

# 3. Independent QC re-derivation from the SAME spec (BMI = kg / m^2, one decimal place)
qc_out <- adsl; qc_out$BMI <- round(adsl$WEIGHT / (adsl$HEIGHT / 100)^2, 1)

# 4. The QC gate: diffdf compares the AI draft against the independent re-derivation on the subject key
qc_report <- capture.output(diffdf(ai_out, qc_out, keys = "USUBJID"))
gate_pass <- any(grepl("No issues were found", qc_report))

# 5. Content hashes -- tamper-evident, reproducible fingerprints of each artifact
h_input  <- digest(adsl,          algo = "sha256")
h_draft  <- digest(ai_draft_code, algo = "sha256", serialize = FALSE)  # a string => serialize = FALSE
h_result <- digest(ai_out,        algo = "sha256")

# 6. Environment capture -- version strings only, NEVER a wall clock (keeps the record deterministic)
env_r <- R.version.string

# 7. Assemble ONE provenance record (the timestamp is a FIXED illustrative string, not Sys.time())
record <- data.frame(
  Field = c("Spec / prompt ref", "AI model", "Prompt ID", "Input data (sha256)", "AI-draft code (sha256)",
            "QC result (sha256)", "QC gate (diffdf)", "R version", "digest / diffdf",
            "Timestamp (UTC, illustrative)", "Human reviewer", "Approval status"),
  Value = c("ADaM-BMI-derivation v1.2 / SAP §9.3", "claude-opus-x (illustrative)", "PRM-2026-014",
            substr(h_input, 1, 16), substr(h_draft, 1, 16), substr(h_result, 1, 16),
            ifelse(gate_pass, "PASS - zero differences", "FAIL"),
            env_r, paste(packageVersion("digest"), packageVersion("diffdf")),
            "2026-07-02T09:00:00Z", "A. Programmer (QC lead)",
            ifelse(gate_pass, "APPROVED", "REJECTED")),
  stringsAsFactors = FALSE)

knitr::kable(record)
Field Value
Spec / prompt ref ADaM-BMI-derivation v1.2 / SAP §9.3
AI model claude-opus-x (illustrative)
Prompt ID PRM-2026-014
Input data (sha256) 8b7e4e496f9a608c
AI-draft code (sha256) ad432d6eaa47edca
QC result (sha256) 1561ec8b7c93e5c2
QC gate (diffdf) PASS - zero differences
R version R version 4.5.1 (2025-06-13)
digest / diffdf 0.6.39 1.1.2
Timestamp (UTC, illustrative) 2026-07-02T09:00:00Z
Human reviewer A. Programmer (QC lead)
Approval status APPROVED

Read the record top to bottom and it is the whole loop in one place. The prompt reference points to the SAP section that specified the derivation. The AI model and prompt ID say what produced the draft. The three hashes fingerprint the input data, the AI-drafted code, and the QC result — so a reviewer can later confirm they are looking at the exact same artifacts. The QC gate line is the independent check’s verdict. The environment fields pin the software. And because the gate passed, the approval status reads APPROVED, attributed to the named reviewer who owns the sign-off. One durable, reproducible object carries the provenance of an AI-assisted derivation.

The record tells the truth: the REJECTED branch

The approval line is not decoration, and it is not the AI’s to grant. It reflects the gate. Feed the same machinery an AI draft that is wrong — here the copilot forgets the centimetre-to-metre conversion and squares the raw HEIGHT — and the QC gate fails, so the record logs REJECTED:

library(digest); library(diffdf)

adsl <- data.frame(
  USUBJID = sprintf("01-%03d", 1:6),
  HEIGHT  = c(170, 165, 180, 158, 175, 168),   # cm
  WEIGHT  = c( 72,  60,  85,  55,  90,  68),    # kg
  stringsAsFactors = FALSE)

# A BUGGY AI draft: forgets the cm -> m conversion (no /100), so BMI is off by a factor of 10,000
ai_draft_bug <- "ai_out$BMI <- round(ai_out$WEIGHT / (ai_out$HEIGHT)^2, 1)"
ai_out <- adsl; eval(parse(text = ai_draft_bug))

# The independent QC re-derivation is still correct (converts cm -> m)
qc_out <- adsl; qc_out$BMI <- round(adsl$WEIGHT / (adsl$HEIGHT / 100)^2, 1)

# The gate now finds differences on every subject -> FAIL
qc_report <- capture.output(diffdf(ai_out, qc_out, keys = "USUBJID"))
gate_pass <- any(grepl("No issues were found", qc_report))

h_input  <- digest(adsl,         algo = "sha256")
h_draft  <- digest(ai_draft_bug, algo = "sha256", serialize = FALSE)
h_result <- digest(ai_out,       algo = "sha256")
env_r    <- R.version.string

record_rej <- data.frame(
  Field = c("Spec / prompt ref", "AI model", "Prompt ID", "Input data (sha256)", "AI-draft code (sha256)",
            "QC result (sha256)", "QC gate (diffdf)", "R version", "digest / diffdf",
            "Timestamp (UTC, illustrative)", "Human reviewer", "Approval status"),
  Value = c("ADaM-BMI-derivation v1.2 / SAP §9.3", "claude-opus-x (illustrative)", "PRM-2026-014",
            substr(h_input, 1, 16), substr(h_draft, 1, 16), substr(h_result, 1, 16),
            ifelse(gate_pass, "PASS - zero differences", "FAIL"),
            env_r, paste(packageVersion("digest"), packageVersion("diffdf")),
            "2026-07-02T09:00:00Z", "A. Programmer (QC lead)",
            ifelse(gate_pass, "APPROVED", "REJECTED")),
  stringsAsFactors = FALSE)

knitr::kable(record_rej)
Field Value
Spec / prompt ref ADaM-BMI-derivation v1.2 / SAP §9.3
AI model claude-opus-x (illustrative)
Prompt ID PRM-2026-014
Input data (sha256) 8b7e4e496f9a608c
AI-draft code (sha256) bc402c9428a591bc
QC result (sha256) 877b38aae368c871
QC gate (diffdf) FAIL
R version R version 4.5.1 (2025-06-13)
digest / diffdf 0.6.39 1.1.2
Timestamp (UTC, illustrative) 2026-07-02T09:00:00Z
Human reviewer A. Programmer (QC lead)
Approval status REJECTED

Same pipeline, opposite outcome. The AI-draft code (sha256) fingerprint differs from the approved record above because the code text is different, the QC gate reads FAIL, and the Approval status reads REJECTED. Nothing about the record flattered the AI. Correct the draft — restore the /100 conversion — and the identical machinery flips the gate to PASS and the status back to APPROVED, reproducing the first record exactly. That is the point: the audit trail records the truth of what the gate and the reviewer decided. A logged workflow is necessary; it is not sufficient. What qualifies the derivation is the human review plus the passing gate — not the fact that it was logged.

Tamper-evidence: change one value, break the hash

Content hashes give the record a second property: any later alteration of an audited artifact is tamper-evident — detectable, because the stored fingerprint no longer matches. Recompute the input hash after changing a single weight from 72 to 73:

library(digest)

adsl <- data.frame(
  USUBJID = sprintf("01-%03d", 1:6),
  HEIGHT  = c(170, 165, 180, 158, 175, 168),   # cm
  WEIGHT  = c( 72,  60,  85,  55,  90,  68),    # kg
  stringsAsFactors = FALSE)

# The logged input fingerprint
h_original <- digest(adsl, algo = "sha256")

# Someone edits ONE value after the fact: subject 01-001's weight 72 -> 73
adsl_tampered <- adsl
adsl_tampered$WEIGHT[1] <- 73
h_tampered <- digest(adsl_tampered, algo = "sha256")

data.frame(
  version = c("logged input", "one weight changed 72 -> 73"),
  sha256  = c(substr(h_original, 1, 16), substr(h_tampered, 1, 16)),
  matches_log = c(TRUE, h_tampered == h_original))
                      version           sha256 matches_log
1                logged input 8b7e4e496f9a608c        TRUE
2 one weight changed 72 -> 73 4c2a655f8baa417d       FALSE

The fingerprint changes completely from a single-digit edit, so the tampered data no longer matches the value stored in the audit record — the alteration cannot pass silently. This is what “content-addressed” buys: the log points at the content, not at a filename, so changing the content invalidates the pointer.

One honest caveat, stated plainly: this is tamper-evidence, not security. A SHA-256 checksum proves the bytes are unchanged; it is not a cryptographic signature and does not prove who made a change or stop a determined actor from recomputing the hash after editing both the data and the log. digest’s documentation states outright that it “is not meant to be used for cryptographic purposes.” Call it an integrity checksum, not a signature — it is the right tool for reproducibility and change-detection inside a controlled, human-owned process, and only that.

Reading the record as an ALCOA+ audit record

Regulators evaluate data integrity against a small set of principles known as ALCOA+. The MHRA GxP data integrity guidance defines ALCOA as Attributable, Legible, Contemporaneous, Original, and Accurate, with the “+” adding Complete, Consistent, Enduring, and Available — and notes there is no difference in expectation regardless of which acronym is used. Data integrity is the degree to which data is complete, consistent, and accurate throughout its lifecycle; the same FDA data-integrity guidance that governs human-authored records governs AI-assisted ones. The provenance record maps onto every ALCOA+ principle:

ALCOA+ principle How the record supports it
Attributable Human reviewer + AI model + Prompt ID say who and what produced and approved each artifact
Legible Plain, human-readable fields — not opaque internal state
Contemporaneous The timestamp field records when the step happened (here an illustrative fixed string)
Original The input/draft/result hashes content-address the originals; a true copy is verifiable against them
Accurate The QC gate (diffdf) status is the evidence the derivation was independently checked
Complete Prompt ref → draft → QC result → approval: the whole loop is captured, nothing dropped
Consistent The deterministic hashes reproduce identically, so the record is internally consistent across regenerations
Enduring A durable artifact committed and frozen with the analysis, not transient console state
Available It sits beside the code and the commit, retrievable years later by a reviewer or inspector

ALCOA+ is applied here as a principle the record is built to satisfy — not a certification this lesson confers. The R Validation Hub frames the same idea for the tools underneath: assurance comes from documented, human-owned validation, not from a tool’s own say-so.

The honest boundary

Line up what the record does and does not do, because the distinction is the whole point of the capstone.

The audit trail records what happened, reproduces it byte-for-byte, and makes any change to the audited artifacts tamper-evident. Those are real, valuable properties, and they are exactly what a submission needs from an AI-assisted deliverable.

The audit trail does not make the AI output correct. It is not a cryptographic signature. It does not certify the model, and it does not validate the derivation. Correctness came from the independent QC gate and the human review — the REJECTED branch is the proof: the same record machinery logged a wrong answer as wrong. A logged AI workflow is necessary for a defensible submission; it is not sufficient. Put plainly: AI-drafted is not AI-validated, and a record of the loop is trustworthy only because a qualified human owned the approval at the end of it.

🟢 With an AI agent

Ask Prova “How do I turn my prompt → AI draft → QC → approval loop into a reproducible, tamper-evident audit record in R, and what does that record actually prove versus not prove?” — it answers grounded in this series’ validation lessons, with the runnable digest + diffdf provenance capture you can adapt to your own derivation. The runtime is the judge. Ask Prova →

Common issues

A record that changes every render — usually Sys.time(). If the audit record or its hashes differ between two renders, something non-deterministic leaked in: a wall-clock call (Sys.time()/Sys.Date()), a random draw, or an environment value that varies. A committed audit trail must reproduce byte-for-byte, so capture time as a fixed metadata string, capture the environment from version strings only, and fix every input as a literal. Determinism is what lets a reviewer regenerate and confirm the log years later.

digest() on a code string returns the “wrong” hash. By default digest() serialises its input before hashing, which is right for a data frame but not for a plain string — two identical strings can hash differently across sessions. When you are fingerprinting captured code text, pass serialize = FALSE so digest() hashes the characters directly (as the AI-draft-code hash does here).

Treating the hash as a signature. A SHA-256 checksum proves the bytes are unchanged; it does not prove who changed them and is not cryptographically secure — digest’s documentation says so explicitly. Use it for reproducibility and tamper-evidence inside a controlled process, and rely on your version-control history and access controls (not the hash) for attribution and authenticity.

Reading “it’s logged” as “it’s validated.” The audit trail records and reproduces the workflow; it does not qualify the AI output. Never let a complete, reproducible record substitute for the independent QC gate and the human sign-off — the record is evidence that validation happened, not the validation itself.

Frequently asked questions

No. An audit trail records what happened — the prompt, the AI draft, the QC result, the approval — and makes it reproducible and tamper-evident. It does not make the code correct. Compliance comes from the same gates any code must pass (independent QC, human review) plus a qualified human’s sign-off; the audit record is the evidence that those steps happened, not a substitute for them. A logged AI workflow is necessary, not sufficient.

No. A SHA-256 hash is an integrity checksum: it detects whether bytes changed, so it makes a record tamper-evident. It does not identify who made a change and is not cryptographically secure — digest’s own documentation states it “is not meant to be used for cryptographic purposes.” For authorship and authenticity, rely on version control and access controls; use the hash for reproducibility and change-detection.

ALCOA+ is the set of data-integrity principles regulators expect records to satisfy: Attributable, Legible, Contemporaneous, Original, Accurate, plus Complete, Consistent, Enduring, and Available (defined in the MHRA GxP data integrity guidance). They apply to AI-assisted code exactly as to human-authored code: the provenance record makes an AI-drafted derivation attributable (who/what produced and approved it), original and consistent (content hashes), accurate (an independent QC gate), and complete/enduring/available (the whole loop, committed with the analysis).

Remove every source of non-determinism from the record: fix inputs as literals, capture the environment from version strings (R.version.string, packageVersion()) rather than a wall clock, and store the timestamp as fixed metadata instead of Sys.time(). Fingerprint the artifacts with digest() (use serialize = FALSE for code strings) and gate the derivation with diffdf(). Committed and frozen with the analysis, the record then regenerates byte-for-byte on any machine — which is what “reproducible” means for an audit trail.

The auditable AI SOP is the policy: the roles, the prompt-to-approval procedure, and the mapping to FDA and GAMP 5 expectations. This capstone is the artifact the policy produces: a concrete, runnable, content-hashed provenance record for one derivation, shown across its REJECTED and APPROVED branches and mapped to ALCOA+. The SOP tells you what the workflow must be; this shows you the tamper-evident record it generates.

Test your understanding

You have a two-subject ADSL and an AI-drafted derivation captured as a string. Build a small provenance record and demonstrate its integrity properties.

  1. In prose: explain why the record must use a fixed timestamp string and version-string environment capture rather than Sys.time() and a wall clock — and what property would break if it did not.
  2. In R: hash a small input data frame and an AI-draft code string with digest() (remember serialize = FALSE for the string). Then change one input value, recompute the input hash, and show it no longer matches the original — the tamper-evidence property.

A committed audit trail must reproduce byte-for-byte so a reviewer can regenerate and confirm it later; any wall-clock or random value would make it change every render and break reproducibility (the ALCOA+ “Consistent”/“Enduring” properties). For the hash, digest(df, algo = "sha256") fingerprints a data frame; digest(code_string, algo = "sha256", serialize = FALSE) fingerprints text. Changing any input byte yields a different fingerprint.

Step 1. The record has to regenerate identically on any machine, any day, so an inspector can reproduce it and confirm nothing changed. Sys.time() and other wall-clock or random values would make the record — and its hashes — differ on every render, so the log could never be reproduced or verified. Capturing time as a fixed metadata string and the environment from version strings keeps the record deterministic (the ALCOA+ Consistent and Enduring properties); the timestamp is illustrative of when the step happened, recorded as data, not read live.

library(digest)

adsl <- data.frame(
  USUBJID = c("01-001", "01-002"),
  HEIGHT  = c(170, 165),   # cm
  WEIGHT  = c(72, 60),     # kg
  stringsAsFactors = FALSE)

ai_draft_code <- "ai_out$BMI <- round(ai_out$WEIGHT / (ai_out$HEIGHT / 100)^2, 1)"

# Fingerprints: default serialisation for the data frame, serialize = FALSE for the code string
h_input <- digest(adsl,          algo = "sha256")
h_draft <- digest(ai_draft_code, algo = "sha256", serialize = FALSE)

# Tamper-evidence: change one value, recompute, and show it no longer matches
adsl2 <- adsl; adsl2$WEIGHT[1] <- 73
h_input2 <- digest(adsl2, algo = "sha256")

identical(h_input, h_input2)   # FALSE -> the edit is detectable; the log no longer matches

identical() returns FALSE: a single changed value produces a different fingerprint, so the alteration cannot pass unnoticed. The hash makes the record tamper-evident — while remaining a checksum, not a signature.

A. Yes — a complete, reproducible, tamper-evident audit record is what validation means. B. Not by the record alone — the audit trail records and reproduces the workflow and makes it tamper-evident, but correctness comes from the independent QC gate passing and a qualified human owning the approval; a logged workflow is necessary, not sufficient. C. Yes, as long as the hashes reproduce byte-for-byte across runs.

B. The record is evidence that the loop happened and lets anyone reproduce and verify it, but it does not make the AI output correct — that is why the REJECTED branch exists (the same machinery logs a wrong answer as wrong). Validation is the passing independent gate plus the human sign-off; the record documents them. Reproducible hashes (C) give tamper-evidence and consistency, not correctness, and a complete record (A) is necessary but not sufficient.

Conclusion

This capstone closes the series by assembling its work into one auditable object. Each earlier lesson produced an AI draft and put it through an independent gate — an admiral ADTTE derivation, a tern table, Define-XML metadata, a QC double-programming check. Here that loop becomes a single reproducible provenance record: content hashes of the input, the AI-drafted code, and the QC result, plus the environment, a fixed timestamp, the gate verdict, and the human approval. It reproduces byte-for-byte, it is tamper-evident, and it maps cleanly onto ALCOA+. And it tells the truth both ways — a failing draft is logged REJECTED, a passing one APPROVED. What the record never does is make the AI output correct; the human review and the passing gate do that. AI changes the speed of the first draft. It changes nothing about the accountability for the number, or the discipline that makes an AI-assisted submission defensible. The runtime is the judge.

Note

This lesson is reproducible: the provenance record, the REJECTED and APPROVED branches, the tamper-evidence hash change, and the ALCOA+ mapping all reproduce here with fixed inputs, so the hashes are identical on every render. Copy any block and run it to reproduce them. The AI drafting step happens in your own tooling; the R that hashes the artifacts, runs the QC gate, and assembles the record is real and executes. The runtime is the judge.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Auditable {AI} {Workflows} in {Clinical} {Programming:} {The}
    {End-to-End} {Reproducible} {Audit} {Record}},
  date = {2026-07-02},
  url = {https://www.datanovia.com/learn/pharma-clinical/08-agentic-clinical-programming/auditable-ai-workflow-end-to-end},
  langid = {en}
}
For attribution, please cite this work as:
“Auditable AI Workflows in Clinical Programming: The End-to-End Reproducible Audit Record.” 2026. July 2. https://www.datanovia.com/learn/pharma-clinical/08-agentic-clinical-programming/auditable-ai-workflow-end-to-end.