Visit is the operational visit-occurrence — a Participant attended a real visit at a StudySite on a real date. Per first-principles.md, the foundation carries one universal pattern: Visit → Activity → Task, where Activity is the universal work unit (Vitals, MRI, ePRO, biopsy, IP administration — same shape) and Task is the universal leaf data-capture (BP, MRI Study reference, PHQ-9 item, drug dose — same shape, polymorphic taskValue). Specialization is content (biomedicalConceptCode + taskValueType + URI references), never entity shape. Visit-occurrence is distinct from VisitDefinition (the protocol template, USDM-ingest-derivable). Per the temporal+PROV native commitment: every entity is a typed PROV node (Visit and Activity are prov:Activity, VisitDefinition is prov:Entity); status attributes are NGSI-LD temporal properties; Task captures observedAt as Property metadata. The foundation IS a W3C PROV graph by construction.
Three structural commitments, all applied:
Anchor pattern: Visit.forParticipant (1..1) + Visit.forStudySite (1..1) + Visit.definedBy → VisitDefinition (0..1). The 0..1 cardinality on definedBy lets UNSCHEDULED visits exist (urgent care, AE workup, sponsor-requested) without a corresponding template. SHACL invariant #30 enforces cross-entity consistency: Visit.forParticipant.forStudySite must match Visit.forStudySite.
The OOUX correction tracked since the Site spec is applied here: Visit → 1 Site (OOUX) is corrected to Visit → 1 StudySite (TOP) — Visits anchor at the per-Study site role, not the institutional Site.
Per the universal-foundation posture in FIRST-PRINCIPLES, Visit's sub-objects accommodate any assessment without modeling its specifics. The same TOP entity shape handles vitals, MRI, ePRO, biopsy, IP administration, lab draws, ECGs, sequencing — anything.
| Activity | activityName | activityType | biomedicalConceptCode | Task example | Task.taskValueType |
|---|---|---|---|---|---|
| Vital Signs | "Vital Signs" | ASSESSMENT | NCIt:C28267 | Systolic BP = 128 mmHg | NUMERIC |
| MRI Brain | "MRI Brain" | IMAGING | NCIt:C16809 | DICOM Study Instance UID = "urn:dicom:..." | URI_REFERENCE |
| PHQ-9 Questionnaire | "PHQ-9" | QUESTIONNAIRE | LOINC:44249-1 | Item 1 response = "2" | CODED |
| Drug X Administration | "Drug X Administration" | IP_ADMINISTRATION | NCIt:C172310 | {dose: 200, unit: "mg", route: "IV", lot: "..."} | STRUCTURED |
| Blood Draw | "Blood Draw — CBC" | SAMPLE_COLLECTION | NCIt:C25294 | Hemoglobin = 12.4 g/dL | NUMERIC |
| RECIST Lesion | "Tumor Measurement" | ASSESSMENT | NCIt:C147547 | {longestDiameter: 22, unit: "mm", lesionId: "L1"} | STRUCTURED |
| Genomic Sequencing | "WGS" | SAMPLE_COLLECTION | NCIt:C101294 | VCF reference URI | URI_REFERENCE |
The architectural moat: standards-up vendors model N entity types per therapeutic area (DICOMImagingStudy, PHQ9Response, LabResult, EProSession). TOP carries one universal pattern that handles all of them. External systems (DICOM PACS, lab LIS, ePRO platform, EHR, sequencing pipelines) hold implementation specifics; TOP holds the universal trial-conduct-realm reference; URI references point to wherever the specialized artifact actually lives.
Every Visit-related entity declares its PROV type via rdfs:subClassOf in the emitted shapes (per the v0.4.1 commitment):
| TOP entity | PROV type | Reasoning |
|---|---|---|
| Visit | prov:Activity | Occurs over time, acts on entities (Participant, Equipment); the occurrence IS an Activity in PROV terms |
| VisitDefinition | prov:Entity | A defined artifact (the protocol template) — Entity in PROV |
| Activity | prov:Activity | Occurs over time within a Visit; the universal work unit |
| Task | prov:Activity | The capture event itself is an Activity that generates a value Entity (the taskValue) |
| VisitObservation | prov:Activity | The recording event; if escalated, the resulting Event was wasInformedBy this Activity |
Relationship-level PROV semantics:
| TOP relationship | rdfs:subPropertyOf |
|---|---|
| Activity.performedBy → Person | prov:wasAssociatedWith |
| Activity.usedEquipment → Equipment | prov:used |
| Task.performedBy → Person | prov:wasAssociatedWith |
| VisitObservation.recordedBy → Person | prov:wasAssociatedWith |
| VisitObservation.escalatedTo → Event | prov:wasInformedBy |
| Visit.definedBy → VisitDefinition | prov:wasInformedBy |
NGSI-LD temporal-property semantics on lifecycle attributes (visitStatus, activityStatus, taskStatus): each value bracketed by validFrom / validUntil, queryable as a trajectory. Task.observedAt is canonical Property metadata for the value's measurement timestamp — NOT a flat performedDateTime attribute.
TOP's universal Visit → Activity → Task hierarchy projects cleanly to FHIR R5 Questionnaire (form template) and QuestionnaireResponse (captured data). This is the consuming view that turns TOP into a single-source-of-truth for clinical-trial forms across Epic, Cerner, SMART on FHIR apps, ePRO vendors.
| TOP | FHIR Questionnaire (form template) | FHIR QuestionnaireResponse (captured) |
|---|---|---|
| VisitDefinition | Questionnaire (root) | (framing context) |
| VisitDefinition.visitName | Questionnaire.title | — |
| Activity (template, when ActivityTemplate lifts) | Questionnaire.item[type=group] | QuestionnaireResponse.item[type=group] |
| Activity.activityName | group.text | (matched by linkId) |
| Task (template) | Questionnaire.item[type=decimal/string/coding/etc.] | QuestionnaireResponse.item.answer |
| Task.biomedicalConceptCode (NCIt → LOINC) | item.code (LOINC) | — |
| Task.taskValueType | item.type (decimal / coding / date / etc.) | (renders correct widget) |
| Task.taskUnit | item.extension[questionnaire-unit] (UCUM) | — |
| Task.taskValue (after capture) | — | item.answer.value[x] |
| Visit.actualStartDate/Time | — | QuestionnaireResponse.authored |
| Visit.forParticipant | — | QuestionnaireResponse.subject |
| Activity.performedBy | — | QuestionnaireResponse.author |
Force-multiplier story: today, sponsors maintain forms separately in 4-5 systems (CRF builder, SMART on FHIR app, ePRO vendor, EHR config). With TOP, the protocol → TOP VisitDefinition + ActivityTemplate + TaskTemplate is the single source; the FHIR Questionnaire adapter projects to canonical Questionnaire JSON; Epic / Cerner / ePRO devices all consume the same Questionnaire. Reverse: ePRO devices emit QuestionnaireResponse; the TOP ingester reads it and creates Visit + Activities + Tasks.
Adapter implementation deferred to v0.6+: tools/project_to_fhir_questionnaire.py takes a VisitDefinition (with linked Activity-template and Task-template metadata) and emits Questionnaire JSON. ~200-300 lines of Python. Bidirectional: also tools/ingest_fhir_questionnaire_response.py.
The foundation decisions in this lift enable per-Activity provenance views. Compliance vendors render these from hand-curated audit logs; TOP renders them from foundation facts in real time, against any Activity, by graph traversal. Example query for "show me the chain of custody for Maria's blood draw at C1D1":
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX top: <https://top.scientix.ai/onto/clinical/v1#>
SELECT ?activity ?actor ?equipment ?document ?observedAt WHERE {
?visit a top:Visit ;
top:forParticipant ?participant ;
top:hasActivity ?activity .
?participant top:firstName "Maria" .
?activity top:activityName "Blood Draw — CBC Panel" ;
prov:wasAssociatedWith ?actor ; # phlebotomist
prov:used ?equipment ; # centrifuge, dry shipper
top:governedBy ?document . # SOP
?activity top:hasTask / top:observedAt ?observedAt .
}
No audit-log table. No translation layer. The chain assembles from foundation triples. Every checkmark in a regulator's compliance view is one of these triples.
| Attribute | Type | Notes |
|---|---|---|
| visitId | NGSI-LD URI | Globally unique |
| visitNumber | string · optional | Maps to SDTM VISITNUM; matches definedBy.visitNumber when populated |
| visitName | string · optional | Maps to SDTM VISIT |
| visitMode | enum (6) — IN_PERSON_CLINIC / IN_PERSON_HOME / REMOTE_VIDEO / REMOTE_PHONE / REMOTE_ASYNC / HYBRID | ICH E6(R3) Annex 2 DCT framing |
| visitStatus | enum (9) — SCHEDULED / IN_PROGRESS / COMPLETED / PARTIALLY_COMPLETED / MISSED / RESCHEDULED / CANCELLED / OUT_OF_WINDOW / UNSCHEDULED | NGSI-LD temporal property |
| plannedStartDate | date · optional | What the SOA says |
| plannedEndDate | date · optional | |
| actualStartDate | date · optional | Maps to SDTM SVSTDTC |
| actualStartTime | time · optional | |
| actualEndDate | date · optional | Maps to SDTM SVENDTC |
| actualEndTime | time · optional | |
| visitDay | integer · optional | Days from study anchor; maps to SDTM VISITDY |
| protocolDeviationCode | string · optional | Required by SHACL when status=OUT_OF_WINDOW |
| unscheduledReason | string · optional | Required by SHACL when status=UNSCHEDULED |
| tags | string · optional | Comma-separated |
| validFrom | dateTime · optional | NGSI-LD temporal-property pattern |
| validUntil | dateTime · optional |
| Relationship | Target | Card. | Notes |
|---|---|---|---|
| forParticipant | Participant | 1..1 | The Participant whose visit this is |
| forStudySite | StudySite | 1..1 | The hosting StudySite (operational owner regardless of physical location) |
| definedBy | VisitDefinition | 0..1 | Protocol template; 0..1 because UNSCHEDULED visits have no template; provSemantics: wasInformedBy |
| hasActivity | Activity | 0..N | Activities performed at this Visit (universal sub-objects) |
| hasTask | Task | 0..N | Leaf data-capture units; each task.belongsToActivity links to a parent Activity |
| hasVisitObservation | VisitObservation | 0..N | Operator-narrative observations |
| partOf | Visit | 0..1 | Multi-part / follow-up linkage |
| hasAdverseEvent | Event | 0..N | Flagged-missing until v0.7+ |
| Sub-object | Role | provType | Key attrs |
|---|---|---|---|
| Activity | The universal work unit (vitals, MRI, ePRO, IP-admin, anything) | prov:Activity | activityName, activityType, activityStatus (temporal), biomedicalConceptCode, validFrom/Until |
| Task | The universal leaf data-capture unit; polymorphic taskValue | prov:Activity | taskName, taskValueType, taskValue, taskUnit, biomedicalConceptCode, taskStatus (temporal), observedAt, belongsToActivity |
| VisitObservation | Operator-narrative observation; may escalate to Event | prov:Activity | observationText, observationCategory, escalated, escalationDate |
| Attribute | Type | Notes |
|---|---|---|
| visitDefinitionId | NGSI-LD URI | Mints from USDM Encounter.id under the urn:top:study:{usdm_study_id}/visit-definition:{usdm_encounter_id} URI policy |
| visitNumber | string | Display number (V1, Visit 4, Day 28) |
| visitName | string | Operator-friendly name |
| visitDay | integer · optional | Days from study anchor |
| visitWindowMinDays | integer · optional | How early the visit can occur and still be in-window |
| visitWindowMaxDays | integer · optional | How late and still in-window |
| expectedDurationMinutes | integer · optional | |
| description | string · optional | |
| visitDefinitionStatus | enum — DRAFT / EFFECTIVE / SUPERSEDED | NGSI-LD temporal property |
| tags | string · optional |
VisitDefinition (Study sub-object) is USDM-ingest-derivable from StudyDesign.encounters[]; Visit (top-level) is the operational occurrence runtime-created by EDC/scheduling/eVisit systems. Linked by Visit.definedBy (0..1; UNSCHEDULED visits have no template). Per FIRST-PRINCIPLES (different operator vocabulary gets first-class entity): operators talk about "Visit 1" (template) and "Mary's Visit 1" (occurrence) as different operational objects. Two separate entities; clean USDM ingest mapping; clean SHACL invariants.
Visit.forParticipant (1..1) and Visit.forStudySite (1..1) — both direct edges, even though forStudySite is reachable via forParticipant.forStudySite traversal. The redundancy is deliberate (query convenience: "all visits at MSKCC across all participants" without a join). SHACL invariant #30 enforces consistency.
Captures operator-narrative observations during a visit that don't fit a structured Activity / Task. Has escalated flag and escalatedTo → Event for the Path C reportability handoff. PROV: prov:wasInformedBy on escalatedTo means a categorized Event was informed by this observation.
Per Bo's clarification: TOP must accommodate any assessment without modeling its specifics. The foundation carries Activity + Task as universal containers; specialization is content (biomedicalConceptCode + taskValueType + URI references), never entity shape. This eliminates the need for ImagingStudy / IPAdministration / QuestionnaireResponse / per-instrument horizontals. Implementation specifics live in external systems (DICOM PACS, lab LIS, ePRO platform); TOP carries the universal trial-conduct-realm reference. The architectural moat: standards-up vendors model N entity types per therapeutic area; TOP carries one.
IN_PERSON_CLINIC / IN_PERSON_HOME / REMOTE_VIDEO / REMOTE_PHONE / REMOTE_ASYNC / HYBRID. DCT modality is critical for trial-feasibility analytics, post-COVID DCT design, and twin-synthesis covariates (visit modality affects drop-out risk, data quality, and patient burden). FHIR Encounter.virtualService + Encounter.location are the projection targets.
SCHEDULED / IN_PROGRESS / COMPLETED / PARTIALLY_COMPLETED / MISSED / RESCHEDULED / CANCELLED / OUT_OF_WINDOW / UNSCHEDULED. NGSI-LD temporal property (validFrom/validUntil per value) for trajectory queryability — parallel to Participant.participantStatus from v0.4.0. Trade-off: more states → more SHACL state-machine invariants (4 of the 9 Visit invariants enforce status-driven completeness).
Three operationally-distinct deviation modes: OUT_OF_WINDOW (visit happened outside the protocol window — captured by status + protocolDeviationCode), MISSED (didn't happen, no make-up scheduled), UNSCHEDULED (extra visit — captured by status + unscheduledReason). A separate ProtocolDeviation sub-object can lift in v0.6+ if structured deviation modeling becomes operationally needed.
plannedStartDate / plannedEndDate are operator-grounded scheduling facts (what the SOA / scheduling system says). actualStartDate / actualStartTime / actualEndDate / actualEndTime are runtime facts (what actually happened). Both kept distinct; visit-window adherence analytics use both. NGSI-LD validFrom / validUntil on the entity record bracket operational validity at the entity level (separate concern from per-property-value temporal semantics on visitStatus).
Visit lifts the queryability surface for digital-twin synthesis significantly. Per-Participant time-series of Visits + per-Visit Activities + per-Activity Tasks + visit-window adherence as a covariate. The foundation decisions made in Participant v0.4 pay off here. Twin synthesis is downstream tooling (not in TOP); the foundation provides the contract.
| TOP attribute / relationship | FHIR R5 | SDTM SV | CDASH | OMOP | USDM |
|---|---|---|---|---|---|
| visitId (URI) | Encounter.identifier | (SUPPDM or implied) | — | visit_occurrence_id | — |
| visitNumber | Encounter.identifier (use=secondary) | VISITNUM | VISITNUM | — | (via VisitDefinition) |
| visitName | Encounter.type[].text | VISIT | VISIT | — | (via VisitDefinition) |
| visitMode | Encounter.virtualService + Encounter.location | (SUPPDM) | — | visit_concept_id (with mode-specific concepts) | Encounter.environmentalSettings + contactModes |
| visitStatus | Encounter.status | (computed from SVSTDTC presence) | — | (computed) | — |
| actualStartDate/Time | Encounter.actualPeriod.start | SVSTDTC | SVSTDTC | visit_start_datetime | — |
| actualEndDate/Time | Encounter.actualPeriod.end | SVENDTC | SVENDTC | visit_end_datetime | — |
| visitDay | (computed) | VISITDY | — | — | (via VisitDefinition relative-day) |
| unscheduledReason | Encounter.reason[].text | SVUPDES | — | visit_source_value | — |
| forParticipant → Participant | Encounter.subject (Ref → Patient via ResearchSubject) | USUBJID | USUBJID | person_id | — |
| forStudySite → StudySite | Encounter.serviceProvider + .location | SITEID | SITEID | care_site_id | — |
| definedBy → VisitDefinition | (via Encounter.appointment) | (implicit via VISITNUM) | — | — | Encounter (template) |
| partOf → Visit (parent) | Encounter.partOf | — | — | preceding_visit_occurrence_id | — |
| Activity content | FHIR R5 resource | SDTM domain | OMOP table |
|---|---|---|---|
| Vital Signs | Observation (category=vital-signs) | VS | measurement |
| Lab Tests | Observation (category=laboratory) | LB | measurement |
| Procedure | Procedure | PR | procedure_occurrence |
| IP Administration | MedicationAdministration | EX | drug_exposure |
| ECG | Observation + DiagnosticReport | EG | measurement |
| Imaging | ImagingStudy | (custom or CR) | — |
| Questionnaire | QuestionnaireResponse | QS | observation |
The same TOP Activity entity shape projects to different standards resources depending on its content (biomedicalConceptCode + activityType). The projection adapter at the deployment edge does the routing.
| taskValueType | FHIR Observation.value[x] | SDTM |
|---|---|---|
| NUMERIC | valueQuantity (with UCUM unit) | VSORRES (numeric, with VSORRESU unit) |
| TEXT | valueString | VSORRES (text) |
| CODED | valueCodeableConcept | VSORRES + VSSTRESC (controlled term) |
| URI_REFERENCE | valueReference (or extension) | (custom — reference to external system) |
| STRUCTURED | component[] (multi-component Observation) | SUPPDM or per-domain qualifier records |
| DATE | valueDateTime | (domain-specific DTC field) |
| IMAGE_REFERENCE | valueReference → ImagingStudy | (custom — DICOM Study Instance UID reference) |
| # | Severity | Constraint |
|---|---|---|
| 26 | Hard | Completed visit needs actual start. Visit with status in {COMPLETED, PARTIALLY_COMPLETED, OUT_OF_WINDOW} requires actualStartDate. |
| 27 | Hard | Completed visit needs actual end. Visit with status=COMPLETED requires actualEndDate. |
| 28 | Hard | Unscheduled visit needs reason. Visit with status=UNSCHEDULED requires unscheduledReason. |
| 29 | Hard | Out-of-window visit needs deviation code. Visit with status=OUT_OF_WINDOW requires protocolDeviationCode. |
| 30 | Hard | Visit-Participant-StudySite consistency. Visit.forParticipant.forStudySite must match Visit.forStudySite. |
| 31 | Hard | Activity NOT_PERFORMED needs reason. |
| 32 | Hard | Task NOT_PERFORMED needs reason. |
| 33 | Hard | Completed Task needs value. Task with taskStatus=COMPLETED requires non-empty taskValue. |
| 34 | Soft | Escalated VisitObservation needs link. escalated=true should have escalatedTo populated (when Event lifts). |
Maria attends Cycle 1 Day 1 at MSKCC on 2026-04-12. Vitals taken, blood drawn for CBC, drug administered.
Maria attends a Day 14 follow-up via video; PHQ-9 questionnaire administered remotely.
Maria's C2D1 visit was supposed to be on 2026-05-10 (Day 28, window ±3) but happened on 2026-05-15 (Day 33).
Maria reports nausea on Day 10; an unscheduled visit is opened for evaluation.
Maria's End-of-Treatment MRI scan on Day 90.
A regulator asks: "Show me the full chain of custody for Maria's blood draw at C1D1."