Life Sciences & Healthcare
ADL v0.3 for Life Sciences & Healthcare
Clinical-trial, drug-safety / pharmacovigilance, lab-notebook, and LIMS ADL domains for regulated life-sciences operations, built to the ADL v0.3 specification.
Regulated life-sciences work runs on attributable, timely records: a lab result tied to instrument, method, and operator; a protocol deviation captured when it happens; a serious adverse event reported inside its regulatory clock. This paper works four such domains in the Application Definition Language (ADL) as complete, runnable definitions: LIMS, clinical-trial protocol deviations, drug-safety pharmacovigilance, and the electronic lab notebook.
Each domain below shows how attribution, validation, and deadline-driven action are declared as data and enforced by the Kalos runtime, so the guarantees a regulated environment depends on are structural rather than procedural.
Visual Architecture & Compliance Models
Domain 1: LIMS (lims)
domain:
name: lims
version: "1.0.0"
description: >
Laboratory Information Management. Chain of custody, instrument/method/operator
attribution, and per-sample temperature limits. Deviation alerting and result
attribution are modeled fields, not free text.
channels:
email:
provider: smtp
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
from: "lims-noreply@example.com"
# PHI lives here (patientId). §33 privacy is required for the CLIA/HIPAA claims.
privacy:
enabled: true
classifications:
phi: { description: "Protected Health Information (HIPAA)", retention: 6 years }
subjectIdentifier: { primary: patientId }
anonymization:
method: pseudonymize
rules:
default: "Redacted"
patientId: "$hash6"
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
SampleType:
values:
blood: { label: "Blood" }
serum: { label: "Serum" }
plasma: { label: "Plasma" }
urine: { label: "Urine" }
tissue: { label: "Tissue" }
swab: { label: "Swab" }
SampleStatus:
values:
registered: { label: "Registered", color: "#94a3b8" }
received: { label: "Received", color: "#3b82f6" }
accessioned: { label: "Accessioned", color: "#6366f1" }
in_testing: { label: "In Testing", color: "#f59e0b" }
tested: { label: "Tested", color: "#8b5cf6" }
reviewed: { label: "Reviewed", color: "#06b6d4" }
approved: { label: "Approved", color: "#10b981" }
reported: { label: "Reported", color: "#059669", final: true }
failed: { label: "Failed", color: "#ef4444" }
SampleCondition:
values:
acceptable: { label: "Acceptable" }
hemolyzed: { label: "Hemolyzed" }
clotted: { label: "Clotted" }
insufficient: { label: "Insufficient Quantity" }
contaminated: { label: "Contaminated" }
resources:
Sample:
object:
id: { type: uuid, primaryKey: true }
accessionNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
sampleType: { $ref: '#/enums/SampleType', required: true }
collectionSite: { type: string, required: true, maxLength: 120 }
collectionDate: { type: datetime, required: true }
collectorId: { type: string, required: true, immutable: true, maxLength: 100 }
collectorName: { type: string, required: true, maxLength: 120 }
patientId: { type: string, maxLength: 100,
privacy: { classification: phi, phi: true, subjectIdentifier: true } }
status: { $ref: '#/enums/SampleStatus', default: registered }
# Storage + per-sample temperature envelope (labs run −80 / −20 / 2–8 / RT)
storageLocation: { type: string, maxLength: 80 }
storageTemp: { type: float }
minTempC: { type: float, default: 2 }
maxTempC: { type: float, default: 8 }
condition: { $ref: '#/enums/SampleCondition' }
# Instrument / method / operator attribution — mandatory per CLIA/CAP
instrumentId: { type: string, maxLength: 80 }
instrumentName: { type: string, maxLength: 120 }
testMethod: { type: string, maxLength: 120 }
rawResult: { type: string, maxLength: 255 }
resultUnit: { type: string, maxLength: 40 }
# Review / QC / disposition fields the transitions require
reviewerComments: { type: text }
failureReason: { type: text }
retestReason: { type: text }
rejectionReason: { type: text }
reportedAt: { type: datetime }
qcFlag: { type: boolean, default: false }
qcReason: { type: text }
$merge: { $ref: '#/objects/Timestamp' }
indexes:
- { fields: [status] }
- { fields: [collectorId] }
- { fields: [patientId] }
- { fields: [qcFlag] }
stateMachine:
field: status
initial: registered
states:
registered:
transitions:
receive:
to: received
guards: [labTech, admin]
requiredFields: [storageLocation, storageTemp]
received:
transitions:
accession:
to: accessioned
guards: [labTech, admin]
accessioned:
transitions:
startTesting:
to: in_testing
guards: [labTech, analyst, admin]
requiredFields: [instrumentId, testMethod]
in_testing:
transitions:
completeTest:
to: tested
guards: [analyst, admin]
requiredFields: [rawResult, resultUnit]
fail:
to: failed
guards: [analyst, admin]
requiredFields: [failureReason]
tested:
transitions:
review:
to: reviewed
guards: [supervisor, admin]
requiredFields: [reviewerComments]
retest:
to: accessioned
guards: [supervisor, admin]
requiredFields: [retestReason]
reviewed:
transitions:
approve:
to: approved
guards: [labDirector, admin]
reject:
to: tested
guards: [labDirector, admin]
requiredFields: [rejectionReason]
approved:
transitions:
report:
to: reported
guards: [labDirector, admin]
sets:
reportedAt: "now()"
failed:
transitions:
retest:
to: accessioned
guards: [supervisor, admin]
requiredFields: [retestReason]
reported:
final: true
validation:
tempWindowSane:
rule: "minTempC == null || maxTempC == null || maxTempC > minTempC"
message: "Maximum storage temperature must exceed the minimum"
features:
audit: true
search: true
audit:
exclude: [updatedAt]
history:
events:
registered:
on: create
capture: [accessionNumber, sampleType, collectionSite, collectorName]
label: "Sample registered — {accessionNumber}"
detail: "{sampleType} collected at {collectionSite} by {collectorName}"
icon: lucide:plus-circle
severity: info
received:
on: transition
transition: receive
capture: [storageLocation, storageTemp, condition]
label: "Received — stored at {storageLocation}"
detail: "Condition: {condition}, Temp: {storageTemp}°C"
icon: lucide:package
severity: info
accessioned:
on: transition
transition: accession
label: "Accessioned and ready for testing"
icon: lucide:clipboard-check
severity: info
testingStarted:
on: transition
transition: startTesting
capture: [instrumentId, instrumentName, testMethod]
label: "Testing started on {instrumentName}"
detail: "Method: {testMethod}"
icon: lucide:flask-conical
severity: info
testCompleted:
on: transition
transition: completeTest
capture: [rawResult, resultUnit, instrumentName]
label: "Testing complete — {rawResult} {resultUnit}"
detail: "Instrument: {instrumentName}"
icon: lucide:check
severity: info
qcFlagged:
on: fieldChange
field: qcFlag
condition: "qcFlag == true"
capture: [qcReason]
label: "QC flag raised"
detail: "{qcReason}"
icon: lucide:alert-triangle
severity: error
reviewed:
on: transition
transition: review
capture: [reviewerComments]
label: "Reviewed"
detail: "{reviewerComments}"
icon: lucide:eye
severity: info
approved:
on: transition
transition: approve
label: "Results approved"
icon: lucide:check-circle
severity: success
resultReported:
on: transition
transition: report
capture: [reportedAt]
label: "Results reported"
icon: lucide:file-text
severity: success
retestOrdered:
on: transition
transition: retest
capture: [retestReason]
label: "Retest ordered"
detail: "{retestReason}"
icon: lucide:rotate-ccw
severity: warning
storageChanged:
on: fieldChange
field: storageLocation
capture: [storageLocation, storageTemp]
label: "Moved to {storageLocation}"
detail: "Temperature: {storageTemp}°C"
icon: lucide:thermometer
severity: info
temperatureDeviation:
# Legitimate fieldChange: storageTemp genuinely changes on write.
# Threshold is now per-sample, not hardcoded 2–8.
on: fieldChange
field: storageTemp
condition: "storageTemp > maxTempC || storageTemp < minTempC"
capture: [storageTemp, storageLocation, minTempC, maxTempC]
label: "TEMPERATURE DEVIATION — {storageTemp}°C"
detail: "Allowed range: {minTempC}–{maxTempC}°C at {storageLocation}"
icon: lucide:alert-octagon
severity: error
permissions:
list: [authenticated]
get: [authenticated]
create: [labTech, admin]
update: [labTech, analyst, supervisor, labDirector, admin]
delete: [admin]
fields:
rawResult: { write: [analyst, admin] }
instrumentId: { write: [analyst, labTech, admin] }
testMethod: { write: [analyst, labTech, admin] }
qcFlag: { write: [supervisor, labDirector, admin] }
reviewerComments: { write: [supervisor, labDirector, admin] }
patientId: { read: [labTech, analyst, supervisor, labDirector, admin] }
Domain 2: Clinical Trials / Protocol Deviations (clinicaltrial)
domain:
name: clinicaltrial
version: "1.0.0"
description: >
Protocol deviation lifecycle with CAPA and IRB/sponsor notification. Subject
data carries §33 privacy classification for GDPR/HIPAA. Notification events
record who notified and when as controlled transitions, not free booleans.
channels:
email:
provider: smtp
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
from: "cta-noreply@example.com"
privacy:
enabled: true
classifications:
phi: { description: "Protected Health Information", retention: 25 years }
personalData: { description: "Standard personal data (GDPR)", retention: 25 years }
subjectIdentifier: { primary: subjectId }
anonymization:
method: pseudonymize
rules:
default: "Redacted"
subjectId: "$hash6"
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
DeviationType:
values:
eligibility: { label: "Eligibility Criteria" }
visit_window: { label: "Visit Window" }
dosing: { label: "Dosing" }
procedure: { label: "Procedure" }
consent: { label: "Informed Consent" }
ae_reporting: { label: "AE Reporting" }
DeviationSeverity:
values:
minor: { label: "Minor", color: "#94a3b8" }
major: { label: "Major", color: "#f59e0b" }
critical: { label: "Critical", color: "#ef4444" }
DeviationStatus:
values:
identified: { label: "Identified", color: "#3b82f6" }
under_assessment: { label: "Under Assessment", color: "#8b5cf6" }
classified: { label: "Classified", color: "#6366f1" }
capa_required: { label: "CAPA Required", color: "#f59e0b" }
capa_completed: { label: "CAPA Completed", color: "#06b6d4" }
closed: { label: "Closed", color: "#059669", final: true }
resources:
Deviation:
object:
id: { type: uuid, primaryKey: true }
deviationNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
studyId: { type: string, required: true, immutable: true, maxLength: 60 }
siteId: { type: string, required: true, maxLength: 60 }
subjectId: { type: string, required: true, maxLength: 60,
privacy: { classification: phi, phi: true, subjectIdentifier: true } }
deviationType: { $ref: '#/enums/DeviationType', required: true }
severity: { $ref: '#/enums/DeviationSeverity' }
description: { type: text, required: true }
discoveredDate: { type: date, required: true }
discoveredBy: { type: string, required: true, maxLength: 120 }
status: { $ref: '#/enums/DeviationStatus', default: identified }
capaRequired: { type: boolean, default: false }
capaDescription: { type: text }
capaCompletedAt: { type: datetime }
impactAssessment: { type: text }
# Notification is a controlled transition; these record the outcome.
irbNotifiedDate: { type: date }
irbNotifiedById: { type: string, maxLength: 100 }
sponsorNotifiedDate: { type: date }
sponsorNotifiedById: { type: string, maxLength: 100 }
createdById: { type: string, required: true, immutable: true, maxLength: 100 }
$merge: { $ref: '#/objects/Timestamp' }
indexes:
- { fields: [status] }
- { fields: [studyId] }
- { fields: [siteId] }
- { fields: [severity] }
stateMachine:
field: status
initial: identified
states:
identified:
transitions:
assess:
to: under_assessment
guards: [clinicalMonitor, admin]
under_assessment:
transitions:
classify:
to: classified
guards: [clinicalMonitor, admin]
requiredFields: [impactAssessment, severity]
classified:
transitions:
requireCapa:
to: capa_required
guards: [qualityManager, admin]
sets:
capaRequired: true
close:
to: closed
guards: [qualityManager, admin]
requiredFields: [impactAssessment]
notifyIrb:
to: classified
guards: [qualityManager, admin]
sets:
irbNotifiedDate: "now()"
irbNotifiedById: "$user.id"
notifySponsor:
to: classified
guards: [qualityManager, admin]
sets:
sponsorNotifiedDate: "now()"
sponsorNotifiedById: "$user.id"
capa_required:
transitions:
completeCapa:
to: capa_completed
guards: [siteCoordinator, qualityManager, admin]
requiredFields: [capaDescription, capaCompletedAt]
capa_completed:
transitions:
verifyCapa:
to: closed
# SoD: CAPA verifier is not the person who completed it.
guard: "hasRole('qualityManager') && field('createdById') != userId()"
closed:
final: true
validation:
criticalNeedsIrb:
rule: "$op != 'update' || severity != 'critical' || status != 'closed' || irbNotifiedDate != null"
message: "A critical deviation cannot be closed before IRB notification is recorded"
features:
audit: true
history:
events:
identified:
on: create
capture: [studyId, siteId, subjectId, deviationType, discoveredBy, description]
label: "Deviation identified at site {siteId}"
detail: "Subject {subjectId}: {description}"
icon: lucide:alert-triangle
severity: warning
assessed:
on: transition
transition: assess
label: "Assessment started"
icon: lucide:search
severity: info
classified:
on: transition
transition: classify
capture: [severity, impactAssessment]
label: "Classified as {severity}"
detail: "Impact: {impactAssessment}"
icon: lucide:tag
severity: info
capaInitiated:
on: transition
transition: requireCapa
label: "CAPA required"
icon: lucide:clipboard-list
severity: warning
capaCompleted:
on: transition
transition: completeCapa
capture: [capaDescription, capaCompletedAt]
label: "CAPA completed"
detail: "{capaDescription}"
icon: lucide:check
severity: success
capaVerified:
on: transition
transition: verifyCapa
label: "CAPA verified and deviation closed"
icon: lucide:shield-check
severity: success
closed:
on: transition
transition: close
label: "Deviation closed"
icon: lucide:check-circle
severity: success
irbNotified:
on: transition
transition: notifyIrb
capture: [irbNotifiedDate]
label: "IRB notified"
detail: "Notification date: {irbNotifiedDate}"
icon: lucide:bell
severity: info
sponsorNotified:
on: transition
transition: notifySponsor
capture: [sponsorNotifiedDate]
label: "Sponsor notified"
detail: "Notification date: {sponsorNotifiedDate}"
icon: lucide:bell
severity: info
severityEscalated:
on: fieldChange
field: severity
condition: "severity == 'major' || severity == 'critical'"
capture: [severity]
label: "Severity escalated to {severity}"
icon: lucide:arrow-up
severity: error
permissions:
list: [authenticated]
get: [authenticated]
create: [siteCoordinator, clinicalMonitor, admin]
update: [siteCoordinator, clinicalMonitor, qualityManager, admin]
delete: [admin]
fields:
severity: { write: [clinicalMonitor, qualityManager, admin] }
impactAssessment: { write: [clinicalMonitor, qualityManager, admin] }
capaRequired: { write: [qualityManager, admin] }
subjectId: { read: [siteCoordinator, clinicalMonitor, qualityManager, admin] }
Domain 3: Drug Safety / Pharmacovigilance (drugsafety)
domain:
name: drugsafety
version: "1.0.0"
description: >
Individual Case Safety Report lifecycle. The regulatory deadline is COMPUTED
from receipt date and seriousness, and enforced by a SCHEDULE — not a
fieldChange event that never fires. FDA/EMA submission is an integration
transaction, not a self-attested boolean.
channels:
email:
provider: smtp
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
from: "pv-noreply@example.com"
privacy:
enabled: true
classifications:
phi: { description: "Patient health data in ICSR", retention: 10 years }
subjectIdentifier: { primary: caseNumber }
anonymization:
method: pseudonymize
rules:
default: "Redacted"
reporterName: "Redacted"
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
ReportSource:
values: [spontaneous, clinical_trial, literature, regulatory_authority, patient_support_program]
ReportType:
values: [initial, followup, correction]
Seriousness:
values:
non_serious: { label: "Non-Serious", color: "#94a3b8" }
serious: { label: "Serious", color: "#f59e0b" }
life_threatening: { label: "Life-Threatening", color: "#ef4444" }
fatal: { label: "Fatal", color: "#7f1d1d" }
Expectedness:
values: [expected, unexpected]
CausalityAssessment:
values: [not_related, unlikely, possible, probable, definite]
CaseStatus:
values:
received: { label: "Received", color: "#3b82f6" }
triaged: { label: "Triaged", color: "#6366f1" }
under_assessment: { label: "Under Assessment", color: "#8b5cf6" }
medically_reviewed: { label: "Medically Reviewed", color: "#06b6d4" }
approved_for_submission: { label: "Approved for Submission", color: "#10b981" }
submitted: { label: "Submitted", color: "#059669" }
on_hold: { label: "On Hold", color: "#f59e0b" }
closed: { label: "Closed", color: "#4b5563", final: true }
PatientSex:
values: [male, female, other, unknown]
ReporterQual:
values: [physician, pharmacist, nurse, other_hcp, consumer, lawyer]
resources:
Case:
object:
id: { type: uuid, primaryKey: true }
caseNumber: { type: string, required: true, unique: true, immutable: true, maxLength: 40 }
receiptDate: { type: datetime, required: true, immutable: true }
reportSource: { $ref: '#/enums/ReportSource', required: true }
reportType: { $ref: '#/enums/ReportType' }
seriousness: { $ref: '#/enums/Seriousness' }
expectedness: { $ref: '#/enums/Expectedness' }
causality: { $ref: '#/enums/CausalityAssessment' }
status: { $ref: '#/enums/CaseStatus', default: received }
productName: { type: string, required: true, maxLength: 200 }
eventDescription: { type: text, required: true }
meddraCode: { type: string, maxLength: 20 }
meddraPreferredTerm: { type: string, maxLength: 200 }
patientAge: { type: integer, min: 0, max: 150,
privacy: { classification: phi, phi: true } }
patientSex: { $ref: '#/enums/PatientSex',
privacy: { classification: phi, phi: true } }
reporterName: { type: string, maxLength: 120,
privacy: { classification: phi, pii: true } }
reporterQualification: { $ref: '#/enums/ReporterQual' }
narrativeSummary: { type: text }
# Submission state is set by the integration, not by hand.
fdaSubmissionId: { type: string, maxLength: 80 }
fdaSubmissionDate: { type: datetime }
emaSubmissionId: { type: string, maxLength: 80 }
emaSubmissionDate: { type: datetime }
$merge: { $ref: '#/objects/Timestamp' }
indexes:
- { fields: [status] }
- { fields: [seriousness] }
- { fields: [productName] }
- { fields: [receiptDate] }
computed:
# Expedited (15-day) reporting applies to serious/unexpected cases.
# Deadline is derived, stored, and filterable — the schedule reads it.
isExpedited:
type: boolean
formula: "(seriousness == 'serious' || seriousness == 'life_threatening' || seriousness == 'fatal') && expectedness == 'unexpected'"
stored: true
updateOn: [seriousness, expectedness]
regulatoryDeadline:
type: datetime
formula: "dateAdd(receiptDate, 15, 'days')"
stored: true
updateOn: [receiptDate]
stateMachine:
field: status
initial: received
states:
received:
transitions:
triage:
to: triaged
guards: [safetyAssociate, admin]
requiredFields: [seriousness, reportType]
triaged:
transitions:
assess:
to: under_assessment
guards: [safetyScientist, admin]
under_assessment:
transitions:
completeMedicalReview:
to: medically_reviewed
guards: [safetyPhysician, admin]
requiredFields: [causality, expectedness, meddraCode]
medically_reviewed:
transitions:
approve:
to: approved_for_submission
guards: [safetyManager, admin]
requiredFields: [narrativeSummary]
returnForReview:
to: under_assessment
guards: [safetyManager, admin]
approved_for_submission:
transitions:
submit:
to: submitted
guards: [regulatoryAffairs, admin]
hold:
to: on_hold
guards: [safetyManager, admin]
on_hold:
transitions:
release:
to: approved_for_submission
guards: [safetyManager, admin]
submitted:
transitions:
close:
to: closed
guards: [safetyManager, admin]
closed:
final: true
features:
audit: true
history:
events:
received:
on: create
capture: [caseNumber, productName, eventDescription, reportSource, receiptDate]
label: "Case {caseNumber} received"
detail: "{productName}: {eventDescription}"
icon: lucide:inbox
severity: info
triaged:
on: transition
transition: triage
capture: [seriousness, reportType]
label: "Triaged — {seriousness}, {reportType}"
icon: lucide:filter
severity: info
assessed:
on: transition
transition: assess
label: "Medical assessment started"
icon: lucide:stethoscope
severity: info
medicallyReviewed:
on: transition
transition: completeMedicalReview
capture: [causality, expectedness, meddraPreferredTerm]
label: "Medical review complete — {causality}"
detail: "MedDRA: {meddraPreferredTerm}. Expectedness: {expectedness}"
icon: lucide:clipboard-check
severity: info
approvedForSubmission:
on: transition
transition: approve
capture: [narrativeSummary]
label: "Approved for regulatory submission"
icon: lucide:check-circle
severity: success
returnedForReview:
on: transition
transition: returnForReview
label: "Returned for additional medical review"
icon: lucide:rotate-ccw
severity: warning
submittedToAuthorities:
on: transition
transition: submit
label: "Submitted to regulatory authorities"
icon: lucide:send
severity: success
onHold:
on: transition
transition: hold
label: "Case placed on hold"
icon: lucide:pause
severity: warning
released:
on: transition
transition: release
label: "Case released from hold"
icon: lucide:play
severity: info
seriousnessEscalated:
on: fieldChange
field: seriousness
condition: "seriousness == 'serious' || seriousness == 'life_threatening' || seriousness == 'fatal'"
capture: [seriousness]
label: "SERIOUSNESS ESCALATED — {seriousness}"
icon: lucide:alert-octagon
severity: error
permissions:
list: [authenticated]
get: [authenticated]
create: [safetyAssociate, safetyScientist, admin]
update: [safetyAssociate, safetyScientist, safetyPhysician, safetyManager, regulatoryAffairs, admin]
delete: [admin]
fields:
causality: { write: [safetyPhysician, safetyManager, admin] }
expectedness: { write: [safetyPhysician, safetyManager, admin] }
seriousness: { write: [safetyAssociate, safetyPhysician, safetyManager, admin] }
meddraCode: { write: [safetyScientist, safetyPhysician, admin] }
narrativeSummary: { write: [safetyScientist, safetyPhysician, safetyManager, admin] }
fdaSubmissionId: { write: [admin] } # set by the integration
emaSubmissionId: { write: [admin] }
patientAge: { read: [safetyScientist, safetyPhysician, safetyManager, admin] }
reporterName: { read: [safetyPhysician, safetyManager, admin] }
# THE FIX: time-based deadline enforcement lives here, not in a fieldChange event.
schedules:
expeditedDeadline:
description: "Escalate expedited (serious/unexpected) cases as the 15-day clock runs down"
type: relative
source:
resource: Case
filter: "isExpedited == true && status != 'submitted' && status != 'closed'"
relativeTo: regulatoryDeadline
tiers:
- daysUntil: 5
action: notify
tag: t5
channels: [email]
recipients: { roles: [safetyManager, regulatoryAffairs] }
template:
subject: "ICSR deadline in 5 days — {caseNumber}"
body: "{productName}. Deadline: {regulatoryDeadline}."
- daysUntil: 3
action: notify
tag: t3
channels: [email]
recipients: { roles: [safetyManager, regulatoryAffairs] }
template:
subject: "REGULATORY DEADLINE IN 3 DAYS — {caseNumber}"
body: "{productName}. Deadline: {regulatoryDeadline}. Case status: not yet submitted."
- daysUntil: 0
action: notify
tag: due
channels: [email]
recipients: { roles: [safetyManager, regulatoryAffairs, admin] }
template:
subject: "DEADLINE TODAY — {caseNumber}"
body: "{productName}. Expedited ICSR is due today and not yet submitted."
# Submission is a gateway transaction, not a boolean the user flips.
integrations:
fdaFaers:
type: rest
baseUrl: "https://api.fda.example/esg"
auth: { type: mtls }
retry: { maxAttempts: 3, backoff: exponential, baseDelay: 2s, maxDelay: 60s, retryOn: [500, 502, 503, 504, 429] }
circuitBreaker: { enabled: true, failureThreshold: 5, successThreshold: 2, timeout: 120s }
deadLetter: { enabled: true, maxRetries: 5, notifyOn: [integration_failure] }
operations:
submitIcsr:
trigger: { resource: Case, event: transition, transition: submit }
request:
method: POST
path: "/icsr/e2b"
body:
case_number: $record.caseNumber
product: $record.productName
seriousness: $record.seriousness
narrative: $record.narrativeSummary
onSuccess:
action: update
resource: Case
target: $record.id
input: { fdaSubmissionId: $response.acknowledgment_id, fdaSubmissionDate: $now() }
history: { event: fdaAcknowledged, label: "FDA acknowledged — {fdaSubmissionId}" }
onError:
action: transition
resource: Case
target: $record.id
transition: hold
Domain 4: Electronic Lab Notebook (labnotebook)
domain:
name: labnotebook
version: "1.0.0"
description: >
Experiment records with author signing, independent witnessing, and post-sign
immutability. NOTE: "signing" here is a gated approval workflow with a full
audit trail. A true 21 CFR Part 11 e-signature (§11.200 re-authentication,
§11.50 signature manifestation with meaning) is applied via the eSignature
integration challenge below — the state transition alone is NOT a Part 11
signature and must not be marketed as one without that challenge in place.
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
enums:
ExperimentStatus:
values:
draft: { label: "Draft", color: "#94a3b8" }
signed: { label: "Signed by Author", color: "#3b82f6" }
witnessed: { label: "Witnessed", color: "#10b981" }
revision_requested: { label: "Revision Requested", color: "#f59e0b" }
locked: { label: "Locked", color: "#059669", final: true }
resources:
Experiment:
object:
id: { type: uuid, primaryKey: true }
notebookId: { type: uuid, required: true }
title: { type: string, required: true, maxLength: 200 }
objective: { type: markdown, required: true }
methodology: { type: markdown }
observations: { type: markdown }
results: { type: markdown }
conclusion: { type: markdown }
status: { $ref: '#/enums/ExperimentStatus', default: draft }
authorId: { type: string, required: true, immutable: true, maxLength: 100 }
authorName: { type: string, required: true, maxLength: 120 }
# Witness fields are set by the transition, never client-supplied.
witnessId: { type: string, maxLength: 100 }
witnessName: { type: string, maxLength: 120 }
witnessedAt: { type: datetime }
signedAt: { type: datetime }
# Signature manifestation record (§11.50): meaning + method.
signatureMeaning: { type: string, maxLength: 120 }
signatureMethod: { type: string, maxLength: 60 }
projectCode: { type: string, maxLength: 40 }
tags: { type: json }
$merge: { $ref: '#/objects/Timestamp' }
relationships:
notebook: { type: belongsTo, target: Notebook, foreignKey: notebookId, onDelete: restrict }
indexes:
- { fields: [notebookId] }
- { fields: [status] }
- { fields: [authorId] }
- { fields: [projectCode] }
stateMachine:
field: status
initial: draft
states:
draft:
transitions:
sign:
to: signed
# Only the author signs. The eSignature integration (below)
# intercepts this transition to force re-authentication before
# the state actually advances.
guard: "isOwner()"
requiredFields: [objective, observations, signatureMeaning]
sets:
signedAt: "now()"
signatureMethod: "password_reauth"
signed:
transitions:
witness:
to: witnessed
# Independent witness: a scientist who is NOT the author.
guard: "hasRole('scientist') && field('authorId') != userId()"
sets:
witnessId: "$user.id"
witnessName: "$user.username"
witnessedAt: "now()"
requestRevision:
to: revision_requested
guards: [supervisor, admin]
revision_requested:
transitions:
revise:
to: draft
guards: [owner]
witnessed:
transitions:
lock:
to: locked
guards: [supervisor, admin]
locked:
final: true
features:
audit: true
versioning: true # full pre-update snapshots — the real "every version preserved" mechanism
# Content is editable ONLY in draft. Once signed, witnessed, or locked,
# nobody (including owner and admin) can alter the record body. This is
# what makes "unalterable after signing" true rather than aspirational.
permissions:
list: [authenticated]
get: [authenticated]
create: [scientist, admin]
update: [owner, admin]
delete: [admin]
conditional:
update:
draft: [owner, admin]
revision_requested: [owner, admin]
signed: []
witnessed: []
locked: []
fields:
# Witness identity is transition-set only; block direct writes entirely.
witnessId: { write: [] }
witnessName: { write: [] }
witnessedAt: { write: [] }
signedAt: { write: [] }
history:
events:
created:
on: create
capture: [title, authorName, projectCode]
label: "Experiment created by {authorName}"
detail: "'{title}' — Project {projectCode}"
icon: lucide:flask-conical
severity: info
contentEdited:
on: update
fields: [objective, methodology, observations, results, conclusion]
capture: [title]
label: "Content updated"
icon: lucide:edit
severity: info
signed:
on: transition
transition: sign
capture: [signedAt, signatureMeaning]
label: "Signed by author"
detail: "Signed at {signedAt} — {signatureMeaning}"
icon: lucide:pen-tool
severity: success
witnessed:
on: transition
transition: witness
capture: [witnessName, witnessedAt]
label: "Witnessed by {witnessName}"
detail: "Witnessed at {witnessedAt}"
icon: lucide:eye
severity: success
revisionRequested:
on: transition
transition: requestRevision
label: "Revision requested"
icon: lucide:rotate-ccw
severity: warning
locked:
on: transition
transition: lock
label: "Experiment locked — record is now immutable"
icon: lucide:lock
severity: info
Notebook:
object:
id: { type: uuid, primaryKey: true }
title: { type: string, required: true, maxLength: 200 }
ownerId: { type: string, required: true, immutable: true, maxLength: 100 }
projectCode: { type: string, maxLength: 40 }
$merge: { $ref: '#/objects/Timestamp' }
relationships:
experiments: { type: hasMany, target: Experiment, foreignKey: notebookId }
indexes:
- { fields: [ownerId] }
features:
audit: true
permissions:
list: [authenticated]
get: [authenticated]
create: [scientist, admin]
update: [owner, admin]
delete: [admin]
# 21 CFR Part 11 §11.200: re-authentication at the moment of signing.
# The integration intercepts the `sign` transition and requires the signer to
# re-enter credentials before the state advances. Without this, "signed" is an
# authorization event, not a compliant electronic signature.
integrations:
eSignature:
type: rest
baseUrl: "${ESIGN_SERVICE_URL}"
auth: { type: bearer, token: "${ESIGN_TOKEN}" }
operations:
challengeSign:
trigger: { resource: Experiment, event: transition, transition: sign }
request:
method: POST
path: "/challenge"
body:
userId: $user.id
recordId: $record.id
meaning: $record.signatureMeaning
onError:
# Failed re-auth reverts the signing attempt.
action: transition
resource: Experiment
target: $record.id
transition: revise
Auditor & operator queries
These queries use the documented /api/v1/ prefix and id-based paths:
LIMS: every error-severity event on a sample
# Resolve accession number → id, then fetch the record timeline
curl -s "$BASE/api/v1/lims/samples?filter[accessionNumber]=ACC-2026-0042" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data.items[0].id'
curl -s "$BASE/api/v1/lims/samples/{id}/history?severity=error" \
-H "Authorization: Bearer $TOKEN"
Clinical trials: one deviation’s inspection artifact
curl -s "$BASE/api/v1/clinicaltrial/deviations?filter[deviationNumber]=PD-2026-042" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data.items[0].id'
curl -s "$BASE/api/v1/clinicaltrial/deviations/{id}/history" \
-H "Authorization: Bearer $TOKEN"
Drug safety: portfolio-wide escalations, and proof the deadline engine ran
# Cross-record event stream (documented history params: event, since→after)
curl -s "$BASE/api/v1/drugsafety/cases/history?event=seriousnessEscalated&after=2026-01-01" \
-H "Authorization: Bearer $TOKEN"
# Open expedited cases by deadline — a filtered list on stored computed fields
curl -s "$BASE/api/v1/drugsafety/cases?filter[isExpedited]=true&filter[status]=under_assessment&sort=regulatoryDeadline" \
-H "Authorization: Bearer $TOKEN"
# Evidence the deadline escalation actually fired (scheduler log)
curl -s "$BASE/api/v1/drugsafety/schedules/expeditedDeadline/log?since=2026-01-01" \
-H "Authorization: Bearer $TOKEN"
GDPR/HIPAA: subject rights (only real because §33 privacy is now declared)
curl -s "$BASE/api/v1/clinicaltrial/privacy/subject/S-1042" \
-H "Authorization: Bearer $TOKEN" # Art. 15 access
curl -s -X POST "$BASE/api/v1/clinicaltrial/privacy/subject/S-1042/erase" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"method":"pseudonymize","reason":"GDPR Art. 17","retainAudit":true,"dryRun":true}'