SNOMED Semantic Query
Overview
SNOMED CT is unique among clinical terminologies: every fully-defined concept
carries explicit attribute relationships encoded as FHIR properties. These
let you answer questions like "all disorders of the cardiovascular system" or
"all conditions with infarct morphology" using structured queries rather than
keyword search.
This skill walks through the full workflow: find the pivot concept, inspect its
relationships, choose the right attribute type, and build a
filter that returns exactly the right set of codes.
Output
Every query produces two deliverables.
1. FHIR ValueSet JSON (always deliver this)
Return a complete
resource with
,
,
, and a
populated
. This is the primary artifact — useful whether or
not expansion succeeds.
json
{
"resourceType": "ValueSet",
"name": "AllInfarctDisorders",
"title": "All Infarct Disorders",
"status": "draft",
"compose": {
"include": [
{
"system": "http://snomed.info/sct",
"version": "<from list_available_codesystem_versions>",
"filter": [
{ "property": "116676008", "op": "=", "value": "55641003" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
]
}
}
2. Expansion (ask the user)
After delivering the ValueSet JSON, ask:
"Would you like me to expand this and show the matching codes?
I can format the results as a markdown table or CSV."
If the user says yes, attempt
once. On failure (see pi
limitation below), explain they can run the ValueSet JSON against any FHIR
terminology server or the ReasonHub API directly.
If expansion succeeds, check the response for a
count. The MCP
transport layer truncates returned rows regardless of the
parameter,
and
-based paging is unreliable.
If rows returned are fewer than
, label the output explicitly and stop:
⚠️ Partial result — {n} of {total} codes shown. The full set is defined
by the ValueSet JSON above; run it against any FHIR terminology server
for the complete expansion.
Do not retry with different
or
values — this will not
retrieve additional rows.
If expansion succeeds, use the requested format:
Markdown table (default):
| Code | Display |
|---|
| Myocardial infarction |
| Cerebral infarction |
CSV (when the user asks to download, import, or use in a spreadsheet):
csv
code,display
22298006,"Myocardial infarction"
432504007,"Cerebral infarction"
For SNOMED results, adding
as a third column is useful when
the expansion mixes disorders, findings, and procedures:
csv
code,display,semanticTag
22298006,"Myocardial infarction",disorder
432504007,"Cerebral infarction",disorder
SNOMED's Semantic Model
Common relationship attributes
This table lists frequently encountered attributes. It is
not exhaustive —
SNOMED CT defines hundreds of attribute types, and the exact set on any concept
depends on its definition. Always use
on a representative
concept to discover the actual attributes present (see
Discovering Attributes by Lookup below).
| Attribute typeId | Name | Applies to | Example |
|---|
| Finding site | Disorders, findings | Finding site = Heart structure () |
| Causative agent | Disorders, infections | Causative agent = Staphylococcus () |
| Associated morphology | Disorders, findings | Associated morphology = Infarct () |
| Procedure site - Direct | Procedures | The structure directly incised/excised. Kidney biopsy uses (Indirect) instead — look up first. |
| Procedure site - Indirect | Procedures | The target organ reached through another structure. Often used where might be expected. |
| Pathological process | Disorders | Pathological process = Inflammatory () |
| Associated with | Findings, disorders | Associated with = Hypertension () |
| Due to | Disorders, findings | Due to = Type 2 diabetes mellitus () — links complications to causal condition |
| Interprets | Findings | Interprets = Blood pressure () |
| Has interpretation | Findings | Has interpretation = Increased () |
| After | Procedures | After = General anaesthesia |
How the filter works
Relationship filters are outbound: they find concepts where a given
attribute points to a target concept.
concept --[363698007 Finding site]--> 80891009 Heart structure
So
filter: property=363698007, op==, value=80891009
returns all concepts
whose "finding site" attribute equals "Heart structure".
⚠️ is exact match, not subsumption
The
operator matches only concepts that store
exactly the specified
concept ID as their attribute value. It does
not apply subsumption to the
value side — filtering by
(respiratory tract) will
not automatically include concepts coded to
(lung structure) or
(lung parenchyma), even though both are subtypes of respiratory tract.
SNOMED concepts are coded to specific anatomical sites, not to tidy ancestor
concepts. For example:
| Concept | finding site coded to |
|---|
| Bacterial pneumonia () | Structure of parenchyma of lung |
| Bacterial respiratory infection () | Structure of respiratory system |
| Pneumoconiosis () | Lung structure |
A query for
(respiratory tract) matches
none of
these, because none are coded to that exact concept ID.
Practical rule: always call
on a few representative
concepts in your target clinical domain first. Read the actual value stored for
your attribute, then use that concept ID — or its closest common ancestor that
concepts in that domain actually share — as your filter value.
What you cannot do directly: reverse lookups ("find all concepts that
hypertension causes"). SNOMED doesn't have a
attribute.
Use the IS-A hierarchy or causative-agent filter instead.
Workflow
Step 1 — Identify the pivot concept
The pivot is the concept you want to filter by (the attribute value).
Use
with a descriptive query:
search_snomed(query="heart structure anatomy")
search_snomed(query="infarct morphology")
search_snomed(query="staphylococcus aureus organism")
Pick the best match. Body structures usually have semantic tag
,
organisms have
, morphologies have
(morphologic abnormality)
.
Step 2 — Confirm with
codesystem_lookup(code="80891009", system="http://snomed.info/sct")
Check:
- matches what you expect
- confirms the concept type
- is
Step 3 — Discover the attribute type via lookup
Do not rely solely on the table above. Always look up a representative
concept in the target domain to see which attribute typeIds are actually
present:
codesystem_lookup(code="22298006", system="http://snomed.info/sct")
# Returns properties including:
# 363698007 (Finding site) = 74281007 (Myocardium structure)
# 116676008 (Associated morphology) = 55641003 (Infarct)
Properties whose
is a bare numeric SNOMED ID are attribute
relationships. Named properties (
,
,
, etc.)
are metadata, not clinical attributes.
Example — discovering hypertension's attributes:
codesystem_lookup(code="38341003", system="http://snomed.info/sct")
# Reveals:
# 363698007 (Finding site) = 51840005 (Systemic circulatory system)
# 363713009 (Interprets) = 35105006 (Increased)
# 363714003 (Has interpretation)= 75367002 (Blood pressure)
# Note: sufficientlyDefined=false — primitive concept, sparse attributes
Hypertension's attributes describe
what it is (elevated BP in the
circulatory system), not its clinical associations. This is why the
filter for "symptoms of hypertension" returns sparse
results — see
Translating Clinical Questions.
Step 4 — Expand with a property filter
json
{
"resourceType": "ValueSet",
"compose": {
"include": [{
"system": "http://snomed.info/sct",
"version": "<use list_available_codesystem_versions>",
"filter": [
{
"property": "363698007",
"op": "=",
"value": "80891009"
}
]
}]
}
}
⚠️ pi limitation: In the pi agent the
parameter is always
received as a string due to
type serialization, causing
. Retrying or removing optional parameters
will not fix this.
Stop after one attempt and use
instead (see
skill).
If the CLI is not installed (
does not install it),
run:
curl -fsSL https://raw.githubusercontent.com/reason-healthcare/reasonhub-skills/main/bin/reasonhub-skills -o ~/.local/bin/reasonhub-skills && chmod +x ~/.local/bin/reasonhub-skills
Step 5 — Refine with stacked filters
Filters in the same
are combined with AND:
json
"filter": [
{ "property": "concept", "op": "is-a", "value": "64572001" },
{ "property": "363698007", "op": "=", "value": "80891009" },
{ "property": "inactive", "op": "=", "value": "false" }
]
→ Active disorders (
) whose finding site is the heart.
Common Clinical Patterns
All disorders of a body structure
json
{ "property": "363698007", "op": "=", "value": "<body_structure_id>" }
All disorders caused by an agent
json
{ "property": "246075003", "op": "=", "value": "<organism_or_substance_id>" }
All disorders with a morphology
json
{ "property": "116676008", "op": "=", "value": "<morphology_id>" }
All procedures on a body site
json
{ "property": "363704007", "op": "=", "value": "<body_structure_id>" }
Check with
whether the procedure uses
(Direct)
or
(Indirect) — kidney biopsy, for example, uses Indirect.
All subtypes of a condition (hierarchy)
json
{ "property": "concept", "op": "is-a", "value": "<parent_concept_id>" }
⚠️
captures clinical subtypes only, NOT complications.
Concepts like "retinopathy due to T2DM" are NOT IS-A children of T2DM — they link
via
(Due to). Use the two-include compose pattern below for complete eCQM sets.
Strict descendants only (exclude the parent itself)
json
{ "property": "concept", "op": "descendent-of", "value": "<parent_concept_id>" }
All disorders caused by / due to a condition
json
{ "property": "42752001", "op": "=", "value": "<condition_id>" }
Captures complication concepts encoded with "Due to" (e.g., retinopathy/neuropathy due to T2DM).
Complete eCQM ValueSet: condition subtypes + their complications (two-include compose)
json
{
"compose": {
"include": [
{
"system": "http://snomed.info/sct",
"filter": [{ "property": "concept", "op": "is-a", "value": "<condition_id>" }]
},
{
"system": "http://snomed.info/sct",
"filter": [{ "property": "42752001", "op": "=", "value": "<condition_id>" }]
}
]
}
}
Active concepts only (add to any filter set)
json
{ "property": "inactive", "op": "=", "value": "false" }
Translating Clinical Questions
Natural language clinical questions often don't map cleanly to a single
SNOMED attribute. Use this table to pick the best strategy, with fallbacks
when attribute coverage is thin.
| Clinical question | Best strategy | Coverage | Fallback |
|---|
| "All disorders of [body part]" | 363698007 = <body_structure>
+ | ⚠️ is exact match — look up a representative concept first to find the actual concept ID used. See note below. | on the body-site disorder parent |
| "All conditions caused by [agent]" | 246075003 = <organism/substance>
| ✅ Good — infections well-modelled | on infectious disease hierarchy |
| "All procedures on [body part]" | 363704007 = <body_structure>
+ | ⚠️ Look up first — many procedures use (Indirect) instead of Direct | on the procedure hierarchy |
| "Subtypes of [condition]" | | ✅ Always works | — |
| "Symptoms / findings associated with [condition]" | + | ⚠️ Sparse — only explicitly encoded associations | See note below |
| "Complications of [condition]" | + | ✅ Good — fully-defined complication concepts encode this | (broader "associated with") |
| "Risk factors for [condition]" | + | ⚠️ Sparse | Semantic search |
The pattern and its limits
The filter for "symptoms/findings associated with X" is:
json
"filter": [
{ "property": "47429007", "op": "=", "value": "<condition_id>" },
{ "property": "concept", "op": "is-a", "value": "404684003" }
]
This returns only concepts that explicitly encode the association as an
outbound attribute. Coverage depends entirely on how well that condition is
modelled in SNOMED:
- Well-modelled conditions (many fully-defined descendants): returns
useful results. Example: specific infectious diseases, metabolic disorders.
- Primitive conditions (
sufficientlyDefined = false
, e.g., hypertension):
the condition itself has few outbound attributes, and few concepts encode
hypertension as their value. The expansion will likely
be empty or very small.
When returns thin results, use these strategies instead:
-
Subtypes — the condition's descendants often are its more specific
presentations:
json
{ "property": "concept", "op": "is-a", "value": "<condition_id>" }
-
Finding site — find all findings at the same anatomical site:
json
{ "property": "363698007", "op": "=", "value": "<site_from_lookup>" }
Use
on the condition first to extract its finding site.
-
Semantic search — use
with the condition name plus
context terms (
"hypertension complication"
,
"elevated blood pressure finding"
) to find individual concepts, then build an explicit list.
Worked example: "All infarct disorders" — discovering associated morphology
This example shows how a single non-hierarchy attribute filter crosses every
organ system to return a clinically precise, exhaustive result set — the
capability that most distinguishes SNOMED from ICD-10 or text search.
# Step 1 — look up a known, fully-defined infarct disorder to discover the attribute
codesystem_lookup("22298006") # Myocardial infarction
→ 116676008 (Associated morphology) = 55641003 (Infarct)
→ 363698007 (Finding site) = 74281007 (Myocardium structure)
→ sufficientlyDefined = true ← fully defined, complete attribute set
# Step 2 — confirm the morphology concept is active and correctly typed
codesystem_lookup("55641003") # Infarct
→ semanticTag = "morphologic abnormality" ← correct type for 116676008 values
→ inactive = false
# Step 3 — verify the same morphology value appears on a different organ
codesystem_lookup("432504007") # Cerebral infarction
→ 116676008 (Associated morphology) = 55641003 (Infarct) ← same value
→ 363698007 (Finding site) = 83678007 (Cerebrum) ← different site
# Step 4 — confirm generic stroke uses a DIFFERENT morphology
codesystem_lookup("230690007") # Cerebrovascular accident (stroke, CVA)
→ 116676008 (Associated morphology) = 37782003 (Damage) ← NOT Infarct
# → the filter will correctly exclude hemorrhagic stroke and generic CVA
Step 5 — expand all infarct disorders (cross-organ)
json
{
"filter": [
{ "property": "116676008", "op": "=", "value": "55641003" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
→ Myocardial infarction, cerebral infarction, renal infarction, pulmonary
infarction, splenic infarction, mesenteric infarction, bone infarction…
One filter. Every organ. No text matching.
Step 6 — narrow to ischemic stroke only (stacked filters)
json
{
"filter": [
{ "property": "116676008", "op": "=", "value": "55641003" },
{ "property": "363698007", "op": "=", "value": "83678007" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
→ Cerebral infarction and its subtypes (thrombotic, embolic, lacunar,
pontine) — ischemic stroke only, hemorrhagic stroke excluded by design.
Worked Examples
"All infarct disorders across every organ"
- Lookup:
codesystem_lookup("22298006")
→ (morphology = Infarct)
- Verify:
codesystem_lookup("432504007")
→ same morphology on cerebral infarction
- Contrast:
codesystem_lookup("230690007")
→ CVA has morphology = (Damage), not Infarct
- Filter: → MI, cerebral infarction, renal infarction, pulmonary infarction…
- Stack: add (Cerebrum) to narrow to ischemic stroke subtypes only
"All cardiac disorders"
- Lookup:
codesystem_lookup("22298006")
→ confirms (Myocardium) — note this is the myocardium, not heart. Check several concepts to find the broadest commonly-used site.
- Search:
search_snomed("heart structure body structure")
→ Heart structure
- Filter: inside (Disorder)
is exact match. Concepts coded to
(Myocardium) or
(Left ventricle) are missed. Use causative-agent or
as a broader net if coverage is thin.
"All bacterial infections"
- Search:
search_snomed("bacteria organism")
→ Bacterium
- Filter: inside (Infectious disease)
"All ischemic conditions"
- Search:
search_snomed("ischemic process pathological")
→
- Filter:
"All renal procedures"
- Lookup:
codesystem_lookup("7246002")
(Kidney biopsy) → — uses Indirect site, not Direct
- Search:
search_snomed("kidney structure body structure")
→
- Filter: try both and ; use (Procedure) in both
eCQM denominator/numerator: all T2DM concepts (subtypes + complications)
This is the canonical example for building exhaustive eCQM criteria.
Step 1 — Verify the root concept
codesystem_lookup("44054006") → display = "Type 2 diabetes mellitus"
sufficientlyDefined = false ← primitive
parent = 73211009 (Diabetes mellitus)
Step 2 — Check which concepts are IS-A children vs. complication-linked
| Concept | Code | In ? | Link |
|---|
| T2DM in obese | | ✅ Yes | direct |
| Insulin-treated T2DM | | ✅ Yes | direct |
| Retinopathy due to T2DM | | ❌ No | (Due to) = |
| Neuropathy due to T2DM | | ❌ No | (Due to) = |
| CAD due to T2DM | | ❌ No | (Due to) = |
alone misses all complication concepts. For a complete eCQM set,
use a two-include ValueSet that unions both trees.
Step 3 — Build the complete ValueSet
json
{
"resourceType": "ValueSet",
"compose": {
"include": [
{
"system": "http://snomed.info/sct",
"version": "<see list_available_codesystem_versions>",
"filter": [
{ "property": "concept", "op": "is-a", "value": "44054006" },
{ "property": "inactive", "op": "=", "value": "false" }
]
},
{
"system": "http://snomed.info/sct",
"version": "<see list_available_codesystem_versions>",
"filter": [
{ "property": "42752001", "op": "=", "value": "44054006" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
]
}
}
- Include 1 captures the root code plus all IS-A subtypes (clinical variants of T2DM)
- Include 2 captures all complications encoded with "Due to = T2DM" (retinopathy, neuropathy, nephropathy, peripheral vascular disease, etc.)
- Together they form the exhaustive denominator or numerator set most eCQMs require
Scope note: use
instead of
in include 1 if the measure
exclicitly excludes the root code (uncommon but possible in pre-coordinated IGs).
Discovering Attributes by Lookup
The attribute table earlier is a starting point, not a complete list. The
authoritative source is the concept data itself.
Protocol:
- Pick a well-known, fully-defined (
sufficientlyDefined = true
) example
concept from the target clinical domain.
- Call on it.
- In the response, find entries whose is a bare numeric
SNOMED concept ID — those are attribute relationships.
- The field names the attribute (e.g., ).
To get the attribute type name, call on the typeId
itself (e.g.,
codesystem_lookup("363698007")
→ ).
- Use those typeIds in your filter.
Prefer fully-defined concepts for discovery. Primitive concepts
(
sufficientlyDefined = false
) have fewer or no attribute relationships and
will not reveal the full attribute set used by their clinical class.
# Good discovery target: Myocardial infarction (sufficientlyDefined=true)
codesystem_lookup("22298006") → 363698007, 116676008 present
# Poor discovery target: Hypertensive disorder (sufficientlyDefined=false)
codesystem_lookup("38341003") → only 363698007, 363713009, 363714003
(incomplete picture of disorder attributes)
Hierarchy Checks
Before building a hierarchy filter, verify the relationship:
codesystem_subsumes(
code_a="64572001", # Disease
code_b="22298006", # Myocardial infarction
system="http://snomed.info/sct"
)
# Expected: subsumed-by (MI is a subtype of Disease)
Getting the Full Property Reference
codesystem_filter_properties(system="http://snomed.info/sct")
Important Constraints
- Only active relationships are stored. Inactive concept attributes are
excluded.
- Primitive concepts (
sufficientlyDefined = false
) may have no or fewer
attribute relationships — they're defined only by IS-A.
- Relationship role groups are flattened during import. Combined attributes
within a single role group (e.g., "finding site + associated morphology")
are not enforced together in filters.
- No attribute exists in SNOMED. Use the
Translating Clinical Questions section
for the recommended strategies, including filters,
finding-site pivots, and hierarchy traversal.