← Back to list

CTOMOP: Extending OMOP to Be Comprehensive Transactional and Longitudinal Patient Record

We have discussed earlier a vision for centralized patient database repositories. One of the many use cases for a truly comprehensive…

Adam Blum in CancerBot · 2026-04-19 14:14 · 0 claps · 7.5 min read
#omop #omop-cdm #medical-informatics #fhir #patient-data
Open on Medium ↗

CTOMOP: Extending OMOP to Be Comprehensive Transactional and Longitudinal Patient Record

We have discussed earlier a vision for centralized patient database repositories. One of the many use cases for a truly comprehensive common patient record is doing the precision trial matching that CancerBot performs. So we built CTOMOP, which builds on the solid foundation of OHDSI OMOP’s relational database schema but adds things necessary for true precision clinical trial matching, and starts to add many of the other things needed by other use cases such as evaluating standard of care options and capturing a truly global picture of the patient. The “CT” in CTOMOP stands either for “Comprehensive Transactional” or “Clinical Trials” — you decide. This blog post presents a summary of the architecture of CTOMOP and describes how it can be used as solid underlying patient data store for other use cases you may have in mind.

Why Start With OMOP?

The Observational Medical Outcomes Partnership (OMOP) Common Data Model, stewarded by OHDSI, is the de facto standard for normalizing observational health data. It gives us a battle-tested relational schema, a rich set of standardized vocabularies (LOINC, SNOMED CT, RxNorm, ICD-O-3), and an enormous ecosystem of analytical tooling. Starting from OMOP means we inherit interoperability with that entire ecosystem for free — any cohort analysis, phenotyping, or population-level study that runs on OMOP runs on CTOMOP.

But OMOP was designed primarily for retrospective, population-scale research. It does a beautiful job of answering questions like “how do patients on drug X compare to patients on drug Y over time?” What it doesn’t do quite so gracefully is answer the question CancerBot has to answer every day: “Given everything we know about this specific patient, right now, which clinical trials could they qualify for, and which would be best for them?”

That question is transactional (it has to be answered in seconds, not hours), longitudinal (it depends on the complete arc of the patient’s disease and treatment), and precision-oriented (it turns on dozens of specific biomarker, staging, and behavioral attributes that OMOP doesn’t surface directly). CTOMOP is our attempt to make OMOP answer that question without breaking its compatibility with the broader OMOP world.

The Architectural Idea

CTOMOP is organized around a simple principle: all clinical data lives in standard OMOP tables, using standard vocabularies. We don’t create a parallel universe of custom tables for biomarkers, treatment lines, or social factors. Instead:

  • Biomarkers (ER, PR, HER2, PD-L1, Ki-67, genetic mutations) go in the standard Measurement table, keyed by LOINC concepts.

  • Lab values (hemoglobin, creatinine, calcium, AST/ALT, and the rest) go in Measurement, also keyed by LOINC.

  • Vital signs and anthropometrics (height, weight, BMI, blood pressure, heart rate, temperature, oxygen saturation) go in Measurement.

  • Social determinants and health behaviors (employment, insurance, tobacco use) go in Observation, keyed by SNOMED CT.

  • Infection status (HIV, Hep B, Hep C serologies) goes in Measurement with LOINC concepts.

  • Cancer staging — T, N, M, stage group, grade, primary site, histology — goes in Observation with ICD-O-3 and SNOMED concepts, using the CDM v6.0 observation_event_id to link staging observations back to the underlying ConditionOccurrence.

  • Treatment response (complete, partial, stable, progressive) goes in Observation with SNOMED concepts.

  • Treatments go in DrugExposure, and treatment lines are derived from drug exposure patterns rather than stored as a separate denormalized entity.

  • Cancer episodes use the official OMOP Oncology Extension: Episode, EpisodeEvent, CancerModifier, Histology, StemTable.

The net effect is that every clinical fact in CTOMOP is readable by any off-the-shelf OMOP tool, and any dataset that is already in OMOP can be loaded into CTOMOP without transformation. We take OMOP’s compliance commitment seriously — the project has been explicitly refactored to remove earlier non-standard extension tables (like bespoke BiomarkerMeasurement and TreatmentLine models) in favor of standard tables with the right vocabulary concepts.

The Two Deliberate Extensions

We made exactly two additions beyond vanilla OMOP, and each earns its place.

1. PersonLanguageSkill Clinical trials care, in a legally and ethically serious way, about what language a patient can give informed consent in, whether they can read the consent form versus only speak the language, and what their primary language is. OMOP’s Person table has a single language concept, which isn’t enough. PersonLanguageSkill is a small join table linking a person to one or more language concepts (themselves drawn from the standard OMOP Concept table — we don’t invent new vocabulary), with a skill_level of speak/understand, read/write, or both, and an is_primary flag. This is a genuine data model extension, but it piggybacks on standard OMOP concepts and is trivial to ignore if you’re running a query that doesn’t care.

2. PatientInfo This one is more interesting, and it’s really the workhorse of CTOMOP for transactional use cases. PatientInfo is a wide, denormalized, single-row-per-patient view — over 100 fields — that aggregates the information a trial-matching engine actually needs to reason about a patient, pulled from the standard OMOP tables underneath. It is explicitly not a new place to store clinical data. Nothing lives only in PatientInfo. It is a materialized, research-friendly projection of the OMOP tables, populated by a management command (populate_patient_info) that walks Person, Measurement, Observation, DrugExposure, ConditionOccurrence, and the oncology extension tables and hydrates the denormalized row.

Why bother? Because the alternative — joining across six or seven OMOP tables and resolving LOINC/SNOMED/ICD-O-3 concepts every time CancerBot needs to evaluate a patient against a trial’s eligibility criteria — is slow and query-heavy. When a patient loads their CancerBot dashboard and we want to score them against thousands of candidate trials in real time, we need single-row, indexed access to their clinical profile. PatientInfo gives us that, without sacrificing the source-of-truth status of the OMOP tables. If the OMOP data changes, you re-run the populate command (or, in a production setup, trigger incremental updates), and PatientInfo reflects the new state.

What’s Actually In There

If you open the front-end patient portal that ships with CTOMOP, you see the PatientInfo shape expressed as a set of tabs:

  • General — demographics, geography, language, disease, stage, performance status (Karnofsky and ECOG), comorbidity flags.

  • Disease-specific tabs (e.g., Multiple Myeloma, Follicular Lymphoma) — cytogenetic markers, disease-specific staging, CRAB/SLiM criteria, progression markers. CTOMOP is designed so that new disease-specific views can be added without schema churn.

  • Treatment— prior therapy, first-line / second-line / later-line therapies with dates and outcomes, supportive therapies, planned therapies, stem cell transplant history, refractory status, relapse count.

  • Blood / Liver / Labs — the full complement of lab values with proper units (g/dL, mg/dL, cells/µL, etc.), computed derived values like eGFR, and disease-relevant composites like serum free light chains.

  • Behavior — consent capability, caregiver availability, contraceptive use, pregnancy/lactation status, mental health flags, tobacco and recreational drug use, occupational and environmental exposure risk, language skills.

Every one of those fields is, underneath, an aggregation over standard OMOP rows with standard concept codes. That’s the whole architectural trick.

Getting Data In: FHIR

OMOP is great for analysis but it’s not the format in which patient data typically arrives. Modern EHRs and health information exchanges speak FHIR R4. CTOMOP ships with a FHIR Bundle upload endpoint: you POST a FHIR R4 Bundle, the loader walks the resources (Patient, Observation, Condition, MedicationStatement, MedicationAdministration, DiagnosticReport, etc.), maps each to the appropriate OMOP table, resolves or creates the right concept IDs, and then triggers a refresh of the patient’s PatientInfo row. There’s also a synthetic FHIR patient generator for testing, which produces realistic oncology bundles including biomarker panels and treatment histories.

This matters for use cases beyond CancerBot. If you’re standing up any system that needs to consume FHIR from the outside world and reason about it in a structured, queryable way, CTOMOP gives you the ingestion pipeline, the canonical storage, and the denormalized projection — all in one.

How CTOMOP Supports Precision Trial Matching

The payoff for CancerBot is that eligibility evaluation becomes tractable. A typical oncology trial eligibility rule — “ECOG ≤ 1, hemoglobin ≥ 9 g/dL, adequate renal function (creatinine clearance ≥ 60 mL/min), HER2-positive, no active Hep B, at least one prior line of therapy, able to consent in English or Spanish” — translates to a query over a handful of indexed fields on PatientInfo. We can score thousands of trials against a patient in real time, present the results, and let the patient and their care team explore why they matched or didn’t match each one. When we need the audit trail — “where did this HER2 status come from, exactly?” — we drop back to the OMOP tables underneath and pull the source Measurement row with its date, provider, and concept.

And because the matching logic operates on semantically meaningful, vocabulary-coded fields rather than free text, the match is precision-grade in the sense that matters: it actually reflects the patient’s molecular, staging, and treatment reality rather than approximating it from notes.

Beyond Trial Matching

Trial matching is the forcing function, but the architecture generalizes. A few use cases we think CTOMOP serves well out of the box:

  • Standard-of-care evaluation. Given a complete, coded treatment history and disease state, you can check a patient against evidence-based guideline trees (NCCN, ESMO) in the same way you check them against trial criteria. The required data is the same; only the rule base differs.

  • Longitudinal patient journey visualization. Because episodes, drug exposures, measurements, and observations all carry dates, you can reconstruct the full arc of a patient’s disease and treatment for display, summarization, or input to a language model.

  • Cohort discovery and phenotyping. This is OMOP’s native strength, and CTOMOP inherits it — any existing OHDSI phenotype library or cohort tool works against the standard tables.

  • Clinical decision support. The coded, denormalized PatientInfo is a natural feature vector for ML models and a natural input for retrieval-augmented LLM workflows.

  • Patient-facing summaries. Because the data is coded rather than just transcribed, you can generate grounded, non-hallucinated patient summaries where every claim traces back to a structured source row.A Few Practical Notes

CTOMOP is a Django 5 + Django REST Framework backend with a React/TypeScript front-end, backed by PostgreSQL, with Docker and Render deployment configurations included. The codebase is organized into three Django apps:

  • omop_core — standard OMOP CDM core tables plus the PatientInfo integration model and PersonLanguageSkill.

  • omop_oncology — the standard OMOP Oncology Extension models.

  • omop_genomics — a placeholder for future genomic-extension work; genomic data currently lives in Measurement and Observation with LOINC concepts, which handles the cases CancerBot needs today.

Migrations use the SeparateDatabaseAndState pattern with idempotent IF NOT EXISTS SQL, which has proven necessary when managing a production database that drifts from Django’s migration state — a realistic concern for any long-lived health data system. The project is open source and available at github.com/cancerbot-org/ctomop.

Closing Thought

A lot of what goes wrong in health data tooling is the temptation to throw away the standard and build something bespoke because the standard doesn’t fit your specific use case perfectly. CTOMOP is our argument for the opposite move: keep the standard, use its vocabularies religiously, add the minimum extension surface your use case genuinely requires, and solve the performance problem with a well-defined denormalized projection rather than a parallel schema. The result is a patient record that is simultaneously a first-class OMOP citizen and a fast, transactional substrate for precision applications like CancerBot — and, we hope, for whatever you want to build on top of it next.


메타데이터
post_id
fb90e7b6bd5f
slug
ctomop-extending-omop-to-be-comprehensive-transactional-and-longitudinal-patient-record-fb90e7b6bd5f
url
https://blog.cancerbot.org/ctomop-extending-omop-to-be-comprehensive-transactional-and-longitudinal-patient-record-fb90e7b6bd5f
canonical_url
https://blog.cancerbot.org/ctomop-extending-omop-to-be-comprehensive-transactional-and-longitudinal-patient-record-fb90e7b6bd5f
author_url
https://medium.com/@adamsblum
status
ok
fetched_at
2026-06-09 14:34:10