SNOMED to OMOP Concept: A Developer's Mapping Guide

You can get a SNOMED code to “work” in a test query and still break a cohort in production. That's the trap with SNOMED to OMOP concept mapping, the code looks tidy on the surface, then one release later a source term lands in the wrong bucket, or lands in two buckets, and the phenotype shifts without any obvious error.
The safe way to think about it is not as a vocabulary lookup, but as an ETL integration problem. OMOP's standard vocabulary model treats Conditions and Procedures differently from source codes, and SNOMED CT is the usual standard layer for those domains, while ICD-10-CM and other source vocabularies need translation before analysis. OHDSI's vocabulary guidance and OMOP basics both show that source codes flow through a mapping layer into standard concepts, not straight into analytics tables, which is why a “just search the code” shortcut is so often wrong OMOP vocabulary overview Understanding OMOP Basics.
Why SNOMED to OMOP Concept Mapping Is Harder Than It Looks
A developer usually finds the problem in a familiar way. A source SNOMED code appears to resolve cleanly, the lookup returns a destination concept, and the row loads without complaint. Weeks later, cohort counts drift because the source term was mapped to a broader concept than the clinical team expected, or to multiple standard concepts when the pipeline kept only the first row.
The lookup is not a single hop
OMOP vocabulary work starts by finding the source concept in CONCEPT, following CONCEPT_RELATIONSHIP where relationship_id = 'Maps to', then validating the destination in CONCEPT again. That path can still produce surprises when a source code maps uphill, meaning the destination is less granular than the source, or when one source term maps to more than one standard concept. The manual pattern is described in the OMOP vocabulary tutorial and the OHDSI vocabulary guidance.
OHDSI's vocabulary framework is explicit about standard concepts, nonstandard source codes, and the need to translate before analysis Understanding OMOP Basics. The practical consequence is simple. Your ETL is not just finding a concept, it is deciding how much clinical detail survives the trip.
Practical rule: if the target concept changes the phenotype boundary, treat the mapping as a governed transformation, not a passive lookup.
Why the wrong target breaks downstream logic
The dangerous part is that the wrong standard concept often looks plausible. A broad SNOMED condition may still satisfy a cohort definition, but it can pull in patients who should not qualify. A split mapping can be worse, because one source code becomes multiple standard rows and your logic either duplicates the event or drops part of it.
A proper pipeline has to guarantee more than “a code was found.” It has to guarantee which standard concept, how many mappings, what relationship type, and whether the result is active and valid for the vocabulary release you loaded. If those checks are missing, the bug usually shows up in analytics, not in the ETL logs.
Scale raises the stakes too. In a CPRD AURUM-to-OMOP conversion, a very large volume of rows was converted into the OMOP tables, which shows that SNOMED-centered normalization is a core engineering concern, not a terminology side issue. Once you are working at that size, a bad assumption does not just create a bad row, it can distort the population. The same practical problem is why teams often use OMOPHub's concept mapping API during development to short-circuit repeated manual lookups when they are debugging version drift or checking how a code resolves in the current vocabulary release.
The Canonical Lookup Pattern and the One-Call Shortcut
A source code that looks simple in the source system can still resolve to the wrong OMOP target if the lookup is done casually. The manual pattern is straightforward, but every step matters: locate the source concept by vocabulary_id and concept_code in CONCEPT, join to CONCEPT_RELATIONSHIP on relationship_id = 'Maps to', then read the destination standard concept from CONCEPT and verify that it is the concept you want OHDSI vocabulary guidance.
What the manual flow needs to return
If you are auditing an ETL, the return set needs to show more than a concept ID. It should include the source code, the source vocabulary, the mapped standard concept, the destination domain, and the mapping type so you can see whether the target is an exact match or a generalized one. That is the part people skip when they only test whether a row exists. The vocabulary tutorial also lays out the same two-table path through the OMOP vocabulary structures OMOP vocabulary tutorial.
A hosted resolver can shorten that check during development. OMOPHub exposes a FHIR resolve call that takes a system URI and code, then returns the standard concept and the CDM target table in one request. Its concept lookup tool is also useful when you want to spot-check a code before you wire it into a pipeline. The API walkthrough is described in the OMOP concept mapping API.
A simple SNOMED condition lookup looks like this:
curl -X POST "https://api.omophub.com/v1/fhir/resolve" \
-H "Authorization: Bearer oh_your_api_key" \
-H "Content-Type: application/json" \
-d '{"system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition"}'
That call collapses the manual walk into one round trip. It does not replace the need to understand the underlying mapping, but it does give you a fast way to confirm what the pipeline should do before you move the logic into production.

Handling One-to-One, One-to-Many, and Uphill Mappings
A lot of ETL bugs come from assuming every source code has exactly one safe destination. That assumption holds often enough to lull teams into bad habits, then fails loudly when a source code is clinically richer than the vocabulary entry it maps to. OHDSI analysis of the OMOP concept_relationship table found 90,518 existing “Maps to” relationships as of 2022-09-10, with 67,377 (74.4%) being one-to-one mappings and 23.0% one-to-two mappings OMOP mapping statistics. That's a healthy one-to-one majority, but it still leaves enough ambiguity to break cohort logic.

Four cases you need to code for
One-to-one is the easy path. A source code maps to one standard SNOMED concept, and the clinical meaning stays intact. That's the case most demo scripts show, which is why teams think mapping is simple.
One-to-many is where the trouble starts. A single source term can point to multiple standard concepts when the vocabulary doesn't offer one exact answer. If your ETL picks the first row and moves on, you've turned a controlled vocabulary issue into a silent data-quality issue.
Many-to-one is common in rollups. Several source codes collapse into one SNOMED condition, often because the analysis wants a consistent standard layer across source systems. That can be right, but only if the cohort definition was built with that collapse in mind.
Uphill mapping means the source is more specific than the available standard concept. The target becomes broader, which is sometimes necessary, but it changes downstream counts in a way that needs governance. A narrow source phenotype and a broad standard concept are not interchangeable just because the codes share a family resemblance.
Rule of thumb: never assume the first
Maps torow is the right row, and never hide a non-one-to-one case from review.
What to log in production
At minimum, log the source code, the number of destination concepts, the mapping relationship, and whether the result was active. If your pipeline can't surface those fields, you won't know whether a change in counts came from the source file, the vocabulary release, or your own mapping logic.
The safest habit is to treat non-one-to-one mappings as exceptions until they've been reviewed. That doesn't mean they're invalid. It means they deserve explicit clinical sign-off instead of passive acceptance.
Traversing Hierarchies and Building Concept Sets
A concept that resolves correctly still leaves the harder job, phenotype expansion. A cohort definition rarely depends on one SNOMED node alone. It depends on descendants, related findings, and exclusions that keep the set from drifting into codes that do not belong in the analysis. OHDSI's standardized vocabulary model supports those layered relationships, and the practical task is deciding how far to walk the tree before the set becomes too broad.
Build the set, then freeze it
A usable concept set usually starts with a parent such as Diabetes mellitus, then expands through descendant concepts until you hit a controlled boundary. That same walk can include non-hierarchical relationships when the phenotype needs a tighter definition, but the expansion has to stay reproducible or you will never be able to explain why a cohort changed after a vocabulary refresh.
In practice, that means snapshotting the vocabulary version, deduplicating descendants, and excluding deprecated concepts during the walk. If you skip those steps, two analysts can build the “same” phenotype and still get different results because they expanded against different releases.
For teams wiring this into code, the OMOPHub hierarchy API walkthrough at OMOP concept hierarchy API is the pattern to study. A common approach is to expand in Python, then pass the resulting concept set into cohort logic, instead of rebuilding the tree every time a user opens the app. That keeps the behavior predictable and gives you one place to inspect version drift when a release changes the descendants under a parent concept.
A compact workflow that holds up
- Resolve the parent concept. Start with the standard SNOMED concept that names the phenotype anchor.
- Expand descendants carefully. Walk
Is arelationships, but cap depth and exclude inactive concepts. - Remove duplicates. Hierarchies can surface the same concept more than once through different paths.
- Persist the version. Save the vocabulary release alongside the concept set so the result is reproducible.
That sequence is plain, and plain is good here. Phenotype authoring breaks when the expansion logic gets clever, especially when a later vocabulary refresh changes the tree shape or exposes a concept that was not present in the earlier release. Keep the traversal deterministic, save the exact concept list you used, and make the expansion step something you can rerun and compare without guessing what changed.
Deprecated Versions, Coverage Gaps, and International Editions
The biggest mistake in vocabulary work is assuming SNOMED is complete enough that mapping is just maintenance. It isn't. OMOP can mark deprecated concepts with the invalid_reason flag, and those concepts need to be filtered out before any cohort expansion or event loading. If they slip through, your set can include codes that should already be retired.
Coverage gaps are real
SNOMED is the standard target in OMOP, but it's not a perfect superset of every clinically relevant term. In one kidney-transplant-related vocabulary coverage study, 1,981 concepts were not covered by the OMOP vocabulary, and 22.72% of those uncovered concepts were condition or diagnosis concepts coverage study summary. That's enough to matter for specialized programs, especially when your local clinical language is richer than the imported release.
For global teams, the same code can also behave differently depending on whether it comes from the SNOMED International Edition, the US Extension, or a country-specific release. The vocabulary graph changes across releases, so a pipeline that resolved cleanly in one version can become partial, broader, or unmapped in the next. That's why vocabulary versioning belongs in your deployment process, not just in your notes.
Version drift needs a change-control habit
If you compare vocabulary releases manually, you'll eventually miss something. A more reliable habit is to diff the release before promoting it, then review mapping changes that affect active cohorts or ETL rules. The OMOPHub FHIR terminology stack includes a $diff operation for release comparison, which fits naturally into that kind of controlled rollout.
Practical rule: pin the vocabulary release that produced a production cohort, and don't let a silent upgrade rewrite history.
That governance matters even more when you support more than one market. A local extension can make a term available in one region and absent in another, so “same source code” doesn't always mean “same OMOP behavior.” If your team ships internationally, keep a release ledger for every market you support.

SDK Examples in Python, R, and TypeScript
Teams don't all resolve vocabularies in the same place. Some do it in an ETL job, some inside an analysis notebook, and some at the application edge when a code arrives from FHIR. The surface should match the workflow, not the other way around.
Pick the SDK that matches the runtime
The Python SDK is usually the cleanest choice for ETL and orchestration work. The R SDK fits biostatistics teams that already work in dplyr and want the same concept resolution inside analysis code. TypeScript belongs in Node-based services or front-end workflows where code resolution happens close to the user.
| SDK | Language | Auth Header | Resolve Call |
|---|---|---|---|
| Python | Python | Authorization: Bearer oh_your_api_key | client.fhir.resolve(...) |
| R | R | Authorization: Bearer oh_your_api_key | client$fhir$resolve(...) |
| TypeScript | TypeScript | Authorization: Bearer oh_your_api_key | client.fhir.resolve(...) |
The package references are available in the OMOP vocabulary SDK guide, and the repos are published at omophub-python, omophub-R, and omophub-mcp. The MCP server is useful when you want AI tooling to stay grounded in the vocabulary source of truth instead of guessing codes.
Minimal usage in each language
Python:
from omophub import OMOPHubClient
client = OMOPHubClient(api_key="oh_your_api_key")
result = client.fhir.resolve(
system="http://snomed.info/sct",
code="44054006",
resource_type="Condition"
)
print(result["standard_concept_id"])
print(result["cdm_target_table"])
R:
library(omophub)
client <- OmophubClient(api_key = "oh_your_api_key")
result <- client$fhir$resolve(
system = "http://snomed.info/sct",
code = "44054006",
resource_type = "Condition"
)
print(result$standard_concept_id)
print(result$cdm_target_table)
TypeScript:
import { OMOPHubClient } from "omophub";
const client = new OMOPHubClient({ apiKey: "oh_your_api_key" });
const result = await client.fhir.resolve({
system: "http://snomed.info/sct",
code: "44054006",
resourceType: "Condition"
});
console.log(result.standard_concept_id);
console.log(result.cdm_target_table);
Store the key in a secrets manager, pin the SDK version, and don't log the resolved payload alongside any patient record. OMOPHub is a vocabulary service, so the lookup itself should stay separate from PHI-bearing systems.
Production QA, Deployment Patterns, and Practical Tips
A SNOMED-to-OMOP pipeline should fail in staging, not in analytics. The QA checklist is straightforward, but teams skip pieces because each one feels small in isolation. That's how mapping drift gets into production and survives long enough to distort results.
The checks that actually catch breakage
Start with a golden set of source codes and expected standard targets, then rerun it every time the vocabulary release changes. Add an assertion that each source code resolves to exactly one active standard concept unless the business rule explicitly allows multiplicity. Then monitor the distribution of mapping types so a sudden rise in ambiguous or unmapped rows gets noticed early.
Tip: treat every vocabulary version bump like a controlled software release, not a data refresh.
The deployment pattern that holds up best is hybrid. Develop and test against a hosted vocabulary API for speed, then cache the resolved results locally when you need air-gapped operation, lower latency, or stricter internal controls. That gives you the convenience of a live service during development without forcing production to depend on the network path forever.
Practical guardrails that keep teams out of trouble
- Spot-check ambiguous codes manually. Use the concept lookup interface at omophub.com/tools/concept-lookup when a code looks clinically odd.
- Prefer the resolver over hand-built joins. If your app already needs the standard concept and target table, use the resolve endpoint instead of reimplementing the
CONCEPTandCONCEPT_RELATIONSHIPwalk. - Keep a rollback plan. If a vocabulary release changes a critical mapping, you need a way to restore the prior release and reproduce the prior cohort.
- Watch unmapped rows. A growing unmapped share usually means either source drift or vocabulary drift, and both deserve investigation.
The implementation goal is stability, not cleverness. A pipeline that survives the next ATHENA refresh is more valuable than a pipeline that looks elegant in a notebook and fails under release pressure.

If you're building or hardening a SNOMED to OMOP concept pipeline, OMOPHub gives you a way to resolve codes, inspect mappings, and compare vocabulary behavior without standing up your own terminology stack. Visit OMOPHub to test a lookup, review the resolver flow, and see how the API fits into your ETL or FHIR integration work.


