Skip to main content

1. 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.
Steps 2 and 3 collapse into one call. OMOPHub’s FHIR Resolver (POST /v1/fhir/resolve) takes a FHIR Coding or CodeableConcept and returns a standard OMOP concept plus the CDM target table in a single request - no hand-rolled parser, no URI mapping table, no Maps to traversal. The examples below use the Resolver as the default path. You can still drop to the raw /v1/concepts/* primitives when you need custom hierarchy walks or relationship queries the Resolver doesn’t cover.

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”) → POST /v1/fhir/resolve with system + code → standard OMOP concept, CDM target table, mapping type
  • Local display name (e.g., “Creatinine, Serum”) → POST /v1/fhir/resolve with display only → Resolver falls back to semantic search scoped by resource_type
  • Free text (e.g., “Chest pain”) → same as above, no code / system
  • Proprietary code (e.g., “MED_LOCAL_4827”) → OMOPHub can’t help directly; requires a pre-built local mapping table
The first three are the Resolver’s three native modes. The fourth remains the irreducible problem: local codes that aren’t coded against any published vocabulary need a mapping table maintained by the site’s data team. 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.
Python
import requests, 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"},
}

# --- Resolve the whole CodeableConcept in one call ---
# The CodeableConcept endpoint accepts the full `coding` array and applies
# OHDSI vocabulary preference when multiple codings are present. If no
# standard code is found it falls back to semantic search on `text`.
resp = requests.post(
    "https://api.omophub.com/v1/fhir/resolve/codeable-concept",
    headers={"Authorization": "Bearer oh_your_api_key"},
    json={
        "coding": fhir_medication_request["medicationCodeableConcept"]["coding"],
        "text": fhir_medication_request["medicationCodeableConcept"].get("text"),
        "resource_type": "MedicationRequest",
    },
)
best = resp.json()["data"]["best_match"]["resolution"]

omop_drug_id = best["standard_concept"]["concept_id"]
omop_drug_name = best["standard_concept"]["concept_name"]
target_table = best["target_table"]              # "drug_exposure"
mapping_type = best["mapping_type"]              # "direct" - RxNorm is already standard

print(f"Resolved: {omop_drug_name} (OMOP {omop_drug_id})")
print(f"CDM destination: {target_table}")
print(f"Ready for CDS: pass {omop_drug_id} to your interaction checker")

# --- Optional: ingredient lookup for class-level DDI rules ---
# "Has ingredient" is a non-hierarchical relationship, not an ancestor,
# so this leg stays on the raw primitives rather than the Resolver.
client = omophub.OMOPHub()
rels = client.concepts.relationships(omop_drug_id)
ingredients = [
    r for r in rels.get("relationships", [])
    if "ingredient" in r.get("relationship_id", "").lower()
    or r.get("concept_class_id") == "Ingredient"
]
if ingredients:
    ing = ingredients[0]
    print(f"Ingredient: {ing['concept_name']} ({ing['concept_id']})")
The Key Insight: The Resolver collapses the three-step “parse FHIR URI → find concept → traverse Maps to” chain into a single call and tells you (via target_table) which OMOP CDM table to write to. The ingredient-level step stays separate because Has ingredient is a non-hierarchical relationship - the right tool is concepts.relationships(), not a hierarchy walk. The Resolver for the ETL leg, the raw primitives for the domain-specific enrichment: two tools for two different shapes of problem. See also: FHIR Integration guide for the full Resolver response shape (source vs standard concept, mapping_type, alternative_standard_concepts, mapping_quality).

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.
Python
import requests

# 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"},
]

# Batch-resolve all three local variants in a single call.
# The Resolver's semantic-fallback path uses the `display` text scoped
# to the `resource_type` domain - no manual basic-then-semantic chain.
resp = requests.post(
    "https://api.omophub.com/v1/fhir/resolve/batch",
    headers={"Authorization": "Bearer oh_your_api_key"},
    json={
        "codings": [
            {
                "display": lab["display"],
                "resource_type": "Observation",
                # no system/code - tells the Resolver to go straight to
                # semantic search over the display text
            }
            for lab in local_labs
        ]
    },
)
items = resp.json()["data"]["items"]

standardized = []
for lab, item in zip(local_labs, items):
    res = item.get("resolution") or {}
    std = res.get("standard_concept")
    if not std:
        print(f"  {lab['local_code']}: no match (may need local mapping)")
        continue
    standardized.append({
        "original_code": lab["local_code"],
        "value": lab["value"],
        "unit": lab["unit"],
        "loinc_concept_id": std["concept_id"],
        "loinc_name": std["concept_name"],
        "mapping_type": res["mapping_type"],     # "semantic_match"
        "similarity_score": res.get("similarity_score"),
    })

# All three local codes should collapse to the same LOINC creatinine concept
unique_loinc = {s["loinc_concept_id"] for s in standardized}
print(f"Standardized {len(standardized)}/{len(local_labs)} local codes")
print(f"Unique LOINC concepts: {len(unique_loinc)}")
for s in standardized:
    print(f"  {s['loinc_name']}: {s['value']} {s['unit']} (from '{s['original_code']}')")
The Key Insight: POST /v1/fhir/resolve/batch is the natural fit for “N local variants, need the canonical LOINC”. Each display-only input tells the Resolver “I don’t have a coded system - use semantic search scoped to the Observation domain.” The response’s similarity_score and mapping_type: "semantic_match" let you decide which matches to trust (high-similarity: auto-accept; low: surface to the site’s data team for review). Batch counts as N API calls, same as N single resolves, so there’s no pricing penalty for bundling. Caveat: This only works when the local display name is descriptive enough for semantic search to match. Highly abbreviated codes (like “Cr_S” or “KFC_001”) won’t match anything useful. Those need a pre-built local mapping maintained by the site’s data team - the target_table and mapping_quality: "manual_review" fields in the Resolver response help you route them to the right workflow.

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.

7. Next Steps

FHIR Integration

Full reference for the FHIR Resolver (single, batch, CodeableConcept) and the OMOPHub FHIR Terminology Service - $lookup, $validate-code, $translate, $expand, $subsumes, batch Bundles.

EHRbase / openEHR

Terminology-validated openEHR capture: wire EHRbase’s composition validator at OMOPHub via the FHIR Terminology Service so coded fields in openEHR templates are checked at commit time.

HAPI FHIR

Point a HAPI FHIR server at OMOPHub as a remote terminology backend. Covers both the HAPI JPA Starter (reverse-proxy auth pattern) and custom Spring Boot HAPI builds.