← All white papers

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.

Interactive Slideshow

Structural Compliance in Life Sciences

Slide 1 of 7
Slide 1
Companion Podcast Episode

Hard-coding Compliance into Life Sciences

Listen to our podcast episode covering Chain of Custody, Attribution, and deviation alerting in LIMS and Clinical Trials.

Visual Architecture & Compliance Models

Life Sciences Structural Compliance Infographic
Figure 1: Life Sciences Structural Compliance Infographic. (Click image to open full resolution)
Structural Compliance for Life Sciences Map
Figure 2: Architectural Mapping for Structural Compliance in Life Sciences. (Click image to open full resolution)

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}'