Skip to main content

1. Introduction: The “Tab Fatigue” Problem

A doctor needs to check if a new prescription will interact with the patient’s current medications. The drug interaction database wants RxNorm codes. The EHR has the medications stored with local formulary codes. The patient’s research record uses OMOP concept IDs. Three systems, three vocabularies, zero interoperability. The doctor opens another tab. This is “Tab Fatigue” or, as clinicians call it, “Death by a Thousand Clicks.” The insights exist: drug interaction alerts, care gap notifications, clinical trial eligibility signals. But they’re trapped in separate systems that don’t speak the same vocabulary language. Getting data out of an EHR for research is hard. Getting standardized insights back into the EHR at the point of care is the real “Last Mile” problem. The vocabulary translation layer is where OMOPHub fits. When an EHR-integrated application (like a SMART on FHIR app) needs to convert local or FHIR-native codes into standardized OMOP concepts, OMOPHub provides the lookup: search for a code, get back the OMOP concept ID, access its hierarchy and relationships. It’s one component in a larger integration pipeline, not the middleware itself, but the vocabulary resolution step that makes the rest possible. The full FHIR-to-OMOP pipeline looks like this:
  1. FHIR client reads patient data from the EHR (medications, conditions, labs)
  2. Your application code parses the FHIR resources and extracts coded elements
  3. OMOPHub resolves those codes to standardized OMOP concept IDs
  4. Your CDS / analytics engine runs logic against the standardized concepts
OMOPHub handles step 3. That’s a narrow but critical role: without it, every integration project maintains its own vocabulary mapping tables, which drift out of date and diverge across installations.

2. The Core Concept: Code Resolution at the Point of Care

EHR data arrives in many vocabulary flavors. A medication might be coded in RxNorm, NDC, a local formulary code, or just free text. A diagnosis might be ICD-10-CM in billing, SNOMED in the problem list, or a local shorthand in a quick note. A lab result could use LOINC, a vendor-specific code, or an institution-specific abbreviation. For any clinical decision support to work across EHR installations, these codes need to resolve to a common vocabulary. OMOP provides that common layer - and OMOPHub provides programmatic access to it. The resolution patterns:
  • Known standard code (e.g., RxNorm “1049502”) → search OMOPHub by vocabulary + code → get OMOP concept ID
  • Local display name (e.g., “Creatinine, Serum”) → search OMOPHub with fuzzy/semantic search → get best LOINC match
  • Free text (e.g., “Chest pain”) → search OMOPHub semantically → get candidate SNOMED concepts
  • Proprietary code (e.g., “MED_LOCAL_4827”) → OMOPHub can’t help directly; requires a pre-built local mapping table
Once you have OMOP concept IDs, you can reuse the same concept sets, phenotypes, and clinical logic across every EHR installation: no per-site mapping tables needed.

3. Use Case A: Real-Time Clinical Decision Support

A SMART on FHIR app needs to check a newly prescribed medication against the patient’s current conditions and medications: all standardized to OMOP concepts. The Scenario: A doctor prescribes Amoxicillin. The app extracts the medication from the FHIR MedicationRequest resource, resolves it to an OMOP concept via OMOPHub, and feeds it to a CDS engine that checks for interactions against the patient’s OMOP-standardized history.
pip install omophub
import omophub

client = omophub.OMOPHub()

# Sample FHIR MedicationRequest (simplified)
# In production, this comes from the EHR via FHIR API
fhir_medication_request = {
    "resourceType": "MedicationRequest",
    "id": "medrx001",
    "medicationCodeableConcept": {
        "coding": [
            {
                "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
                "code": "1049502",
                "display": "Amoxicillin 500 MG Oral Capsule",
            }
        ],
        "text": "Amoxicillin 500mg capsule",
    },
    "subject": {"reference": "Patient/example", "display": "Peter James Chalmers"},
}

print("Processing FHIR MedicationRequest for CDS...\n")

# --- Step 1: Parse the FHIR resource (this is YOUR code, not OMOPHub) ---
med_code = None
med_vocab = None
med_text = None

if "medicationCodeableConcept" in fhir_medication_request:
    concept = fhir_medication_request["medicationCodeableConcept"]
    for coding in concept.get("coding", []):
        if coding.get("system") == "http://www.nlm.nih.gov/research/umls/rxnorm":
            med_code = coding.get("code")
            med_vocab = "RxNorm"
            break
    med_text = concept.get("text")

# --- Step 2: Resolve to OMOP concept via OMOPHub ---
omop_drug_id = None

if med_code and med_vocab:
    print(f"  FHIR code: {med_code} ({med_vocab})")
    try:
        # Search for the RxNorm code directly
        results = client.search.basic(
            med_code,
            vocabulary_ids=[med_vocab],
            page_size=1,
        )
        candidates = results.get("concepts", []) if results else []

        if candidates:
            drug_concept = candidates[0]
            omop_drug_id = drug_concept["concept_id"]
            print(f"  -> OMOP: {drug_concept.get('concept_name')} (ID: {omop_drug_id})")
        else:
            print(f"  -> No OMOP match for {med_vocab} code {med_code}")
    except omophub.APIError as e:
        print(f"  -> API error: {e.message}")

elif med_text:
    # Fallback: search by display text if no standard code available
    print(f"  FHIR text (no standard code): '{med_text}'")
    try:
        results = client.search.fuzzy(med_text)
        candidates = (
            results if isinstance(results, list)
            else results.get("concepts", [])
        ) if results else []

        # Filter to RxNorm Drug concepts client-side
        drug_matches = [
            c for c in candidates
            if c.get("vocabulary_id") == "RxNorm" and c.get("domain_id") == "Drug"
        ]

        if drug_matches:
            drug_concept = drug_matches[0]
            omop_drug_id = drug_concept["concept_id"]
            print(f"  -> OMOP (fuzzy): {drug_concept.get('concept_name')} (ID: {omop_drug_id})")
        else:
            print(f"  -> No RxNorm match found via fuzzy search")
    except omophub.APIError as e:
        print(f"  -> API error: {e.message}")

else:
    print("  No medication code or text found in FHIR resource.")

# --- Step 3: Use the OMOP concept ID for CDS ---
if omop_drug_id:
    print(f"\n  Ready for CDS: OMOP Drug Concept ID {omop_drug_id}")
    print(f"  -> Pass to interaction checker, allergy checker, or trial matcher")
    print(f"  -> Compare against patient's OMOP-standardized medication/condition history")

    # Optionally: get the drug ingredient via hierarchy for ingredient-level checks
    try:
        ancestors = client.hierarchy.ancestors(omop_drug_id, max_levels=3, relationship_types=["Is a"])
        anc_list = (
            ancestors if isinstance(ancestors, list)
            else ancestors.get("concepts", [])
        ) if ancestors else []
        ingredients = [a for a in anc_list if a.get("concept_class_id") == "Ingredient"]
        if ingredients:
            print(f"  -> Ingredient: {ingredients[0].get('concept_name')} (ID: {ingredients[0]['concept_id']})")
    except omophub.APIError:
        pass
else:
    print("\n  Could not resolve medication - CDS check cannot proceed.")
The Key Insight: The FHIR parsing is your code. The CDS logic is your engine. OMOPHub’s role is the vocabulary resolution in between - turning the RxNorm code from the FHIR resource into an OMOP concept ID that your CDS engine can work with. That’s a narrow role, but without it, every CDS deployment needs custom mapping tables per EHR installation. With it, the same concept IDs and clinical logic work everywhere.

4. Use Case B: Normalizing Local Lab Codes for a Sidecar App

EHR-embedded “sidecar” apps (SMART on FHIR apps for specialty workflows) often need to display standardized trends from lab data that arrives in 50 different local code variants. The Scenario: A CKD management app needs to show creatinine trends. The EHR uses “Cr_Serum,” “Creat_Blood,” and “Kidney_Fx_Creat” - all meaning the same thing. The app needs a single LOINC concept to unify them.
import omophub

client = omophub.OMOPHub()

# Lab results from EHR (via FHIR Observation resources, parsed by your code)
local_labs = [
    {"local_code": "Cr_Serum", "display": "Creatinine, Serum", "value": 1.2, "unit": "mg/dL"},
    {"local_code": "Creat_Blood", "display": "Blood Creatinine", "value": 106, "unit": "umol/L"},
    {"local_code": "Kidney_Fx_Creat", "display": "Creatinine Level", "value": 0.9, "unit": "mg/dL"},
]

print("Normalizing local lab codes for sidecar app...\n")

standardized = []

for lab in local_labs:
    display = lab["display"]
    print(f"  '{lab['local_code']}' ({display})")

    # Search OMOPHub using the human-readable display name
    # (OMOPHub cannot resolve proprietary local codes directly)
    try:
        # Try basic search first
        results = client.search.basic(
            display,
            vocabulary_ids=["LOINC"],
            domain_ids=["Measurement"],
            page_size=3,
        )
        candidates = results.get("concepts", []) if results else []

        # If basic search misses, try fuzzy search
        if not candidates:
            fuzzy_results = client.search.fuzzy(display)
            fuzzy_list = (
                fuzzy_results if isinstance(fuzzy_results, list)
                else fuzzy_results.get("concepts", [])
            ) if fuzzy_results else []
            candidates = [
                c for c in fuzzy_list
                if c.get("vocabulary_id") == "LOINC" and c.get("domain_id") == "Measurement"
            ][:3]

        if candidates:
            best = candidates[0]
            loinc_id = best["concept_id"]
            loinc_name = best.get("concept_name", "Unknown")
            loinc_code = best.get("concept_code", "N/A")
            print(f"    -> {loinc_name} (OMOP: {loinc_id}, LOINC: {loinc_code})")

            standardized.append({
                "original_code": lab["local_code"],
                "value": lab["value"],
                "unit": lab["unit"],
                "loinc_concept_id": loinc_id,
                "loinc_name": loinc_name,
            })
        else:
            print(f"    -> No LOINC match found (may need manual mapping)")

    except omophub.APIError as e:
        print(f"    -> API error: {e.message}")

# All three local codes should now map to the same LOINC creatinine concept
print(f"\n--- Standardized for App Display ---")
unique_loinc = set()
for s in standardized:
    unique_loinc.add(s["loinc_concept_id"])
    print(f"  {s['loinc_name']}: {s['value']} {s['unit']} (from '{s['original_code']}')")

print(f"\nUnique LOINC concepts: {len(unique_loinc)}")
if len(unique_loinc) == 1:
    print("All local codes resolved to the same LOINC concept - ready for trend display.")
else:
    print("Multiple LOINC concepts found - review mappings for consistency.")
The Key Insight: The sidecar app doesn’t need to maintain a mapping table for every EHR installation’s local lab codes. It sends the human-readable display names to OMOPHub, gets back LOINC concept IDs, and all three “Creatinine” variants collapse to a single concept for trend display. This pattern scales: deploy the same app at 50 hospitals, and OMOPHub handles the vocabulary differences at each one. Caveat: This only works when the local display name is descriptive enough for search to match. Highly abbreviated codes (like “Cr_S” or “KFC_001”) won’t match anything. Those need a pre-built local mapping maintained by the site’s data team.

5. The “Feedback Loop”: Writing Back to the EHR

True integration is bidirectional. When your OMOPHub-powered app identifies something - a more precise diagnosis code, a care gap, a trial match - that information should flow back to the EHR. Example: A clinician types “chest pain” in a free-text field. Your app searches OMOPHub and suggests “Angina pectoris” (a specific SNOMED condition concept) with its concept ID and code. The clinician confirms, and the app writes the structured SNOMED code back to the EHR via a FHIR Condition resource. The mechanics of the write-back are handled by the EHR’s FHIR write API - OMOPHub doesn’t write to EHRs. But OMOPHub provides the vocabulary backbone: the standardized concept ID, name, and code that get written back. This closes the loop: messy data comes out of the EHR → OMOPHub standardizes it → your app adds clinical intelligence → clean, structured data goes back in. Over time, this feedback loop improves the EHR data itself - more precise codes, fewer local abbreviations, better downstream analytics. The vocabulary standardization that started as a research need becomes a data quality improvement engine.

6. Conclusion: Making Data Invisible (and Useful)

The best clinical technology is invisible. The clinician doesn’t see “FHIR-to-OMOP transformation” or “vocabulary resolution” - they see a drug interaction alert that fires at the right moment, a lab trend that makes sense across visits, a trial match that arrives without a separate login. OMOPHub’s role in this is specific: it resolves codes from EHR-native vocabularies to standardized OMOP concepts via API. It doesn’t parse FHIR resources, run CDS logic, or write back to EHRs. But it provides the vocabulary resolution layer that every other component depends on. And it does so without requiring per-site mapping tables or local vocabulary databases. If you’re building SMART on FHIR apps or EHR-integrated clinical tools, start with the vocabulary problem. Take a FHIR MedicationRequest from your test environment, extract the coded medication, and resolve it through OMOPHub. That single API call - billing code in, standard concept out - is the foundation everything else builds on. The data complexity becomes invisible. The clinical insight becomes useful.