Clinical Code Search — ICD-10-CM · LOINC · RxNorm
Overview
SNOMED CT is the richest semantic model, but most clinical data lives in ICD-10-CM (diagnoses), LOINC (lab and clinical observations), and RxNorm (drugs). Each system has its own query model:
| System | Strengths | Primary filter mechanism |
|---|---|---|
| ICD-10-CM | Clinical diagnosis classification; encounter data; claims | parent is-a <category> hierarchy |
| LOINC | Lab and clinical observations; multi-axis axes (CLASS, COMPONENT, SYSTEM, SCALE_TYP) | CLASS, COMPONENT, panel-parent, STATUS filters |
| RxNorm | Drug products; ingredient → product navigation; NDC linkage | has_ingredient = <IN_RxCUI> + TTY filter |
This skill walks the full workflow for each: detect the right system, run a
semantic search to find the anchor concept, confirm it via lookup, and
compose a ValueSet that you can expand or hand to a FHIR server.
Step 0 — Code System Selection
Signal Detection
Read the user's query for these signals to determine which system to target. Multiple strong signals → target multiple systems (cross-system ValueSet).
| Signal in query | Target system |
|---|---|
| "diagnosis", "ICD", "condition codes", "encounter", "claims", "billing", "disease codes" | ICD-10-CM |
| "lab", "test", "observation", "LOINC", "panel", "orderable", "result", "measurement", "analyte", "assay" | LOINC |
| "drug", "medication", "ingredient", "brand name", "RxNorm", "NDC", "prescription", "dose form", "pill", "tablet" | RxNorm |
| "procedure", "body structure", "organism", "morphology", "finding", "semantic relationship" | SNOMED → use snomed-semantic skill |
Disambiguation Prompt
When signals are absent or conflict, ask before proceeding:
"I can search for this concept in multiple clinical code systems. Which do you need?
- ICD-10-CM — diagnosis and condition codes (claims/encounters)
- LOINC — lab and clinical observation codes
- RxNorm — drug and medication codes
- All three — cross-system ValueSet
Or tell me your use case (e.g., CDS rule, eCQM, lab order set, formulary) and I'll recommend the best fit."
ICD-10-CM Workflow
When to use ICD-10-CM
- Building condition/diagnosis ValueSets for eCQMs, CDS rules, or claims analytics
- Enumerating all subtypes of a clinical category (e.g., all T2DM codes, all sepsis codes)
- Finding the canonical billing code for a condition
Step 1 — Semantic search for the category anchor
search_icd10(query="<clinical concept>", top_k=5)
Distance interpretation:
| Distance | Signal |
|---|---|
| < 0.10 | Excellent — high confidence match |
| 0.10–0.20 | Good — review display before using |
| > 0.25 | Weak — refine query or try alternate terms |
Pick the highest-level (shortest code) that covers the clinical domain.
For a complete subtree, prefer a category code (e.g., E11) over a
specific leaf code (e.g., E11.9).
Step 2 — Confirm with lookup
codesystem_lookup(code="E11", system="http://hl7.org/fhir/sid/icd-10-cm")
Check:
displaymatches intent- Review
includes,inclusionTerm,excludes1,excludes2notes to understand scope boundaries excludes1entries are mutually exclusive — codes in that note cannot coexist with the selected category; be aware when combining
Step 3 — Build the ValueSet
{
"resourceType": "ValueSet",
"name": "Type2DiabetesMellitus",
"title": "Type 2 Diabetes Mellitus (ICD-10-CM)",
"status": "draft",
"compose": {
"include": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"version": "<from list_available_codesystem_versions>",
"filter": [
{ "property": "parent", "op": "is-a", "value": "E11" }
]
}]
}
}
is-aincludes the anchor code itself. Usedescendent-ofto exclude it.
Common ICD-10-CM Patterns
| Clinical goal | Filter |
|---|---|
| All codes in a category | parent is-a <category> e.g., E11 |
| Strict children only | parent descendent-of <category> |
| Specific leaf code | Enumerate by code directly in include.concept |
| Active codes only | ICD-10-CM does not carry an inactive boolean at leaf level; the expansion itself only includes valid codes for the version |
Tip — finding the right category code: ICD-10-CM hierarchy levels:
- Chapter — e.g.,
E00-E89Endocrine, nutritional and metabolic diseases - Block — e.g.,
E08-E13Diabetes mellitus - Category — e.g.,
E11Type 2 diabetes mellitus ← preferred anchor - Subcategory — e.g.,
E11.3With ophthalmic complications - Code — e.g.,
E11.311With unspecified retinopathy with macular edema
Use search_icd10 on the clinical domain name to land at the right level,
then verify with codesystem_lookup.
ICD-10-CM Limitations
- No semantic attributes. ICD-10-CM has no "finding site", "causative
agent", or "associated morphology" equivalents. For cross-organ queries
(e.g., "all infarct disorders") use SNOMED CT via the
snomed-semanticskill. - Tabular hierarchy only. The parent/child structure mirrors the printed tabular list; it is a classification hierarchy, not a clinical ontology.
- Coarse granularity. ICD-10 categories are intentionally coarser than SNOMED — a single category can conflate conditions
that SNOMED distinguishes precisely. For more granular or semantically
filtered sets, crossmap to SNOMED via
terminology-crossmap.
LOINC Workflow
When to use LOINC
- Building lab order sets, result observation ValueSets, or CDS rules on lab values
- Finding all LOINC codes for a given analyte across specimen types
- Identifying members of a panel (e.g., CMP, CBC)
- Mapping clinical observations to standard codes for interoperability
Step 1 — Semantic search for the anchor observation
search_loinc(query="<analyte or test name>", top_k=10)
LOINC semantic search distances tend to run higher than ICD-10:
| Distance | Signal |
|---|---|
| < 0.15 | Strong match |
| 0.15–0.30 | Reasonable — confirm with lookup |
| > 0.35 | Likely off-domain — refine query |
Pick a representative observation that matches the user's intent. If the user named a specific analyte (e.g., "glucose"), look for the code with the best COMPONENT match.
Step 2 — Confirm and extract axes via lookup
codesystem_lookup(code="<LOINC_code>", system="http://loinc.org")
The response includes all six LOINC axes and metadata. Extract:
| Property | Use |
|---|---|
COMPONENT |
LP code for the analyte — use this for sibling searches |
CLASS |
Domain (e.g., CHEM, HEM/BC, MICRO, RAD) |
CLASSTYPE |
1=Lab, 2=Clinical, 3=Claims, 4=Survey |
SCALE_TYP |
LP7753-9=Quantitative, LP7751-3=Ordinal, etc. |
ORDER_OBS |
Order, Observation, Both |
STATUS |
ACTIVE, DEPRECATED, DISCOURAGED, TRIAL |
EXAMPLE_UCUM_UNITS |
Recommended UCUM unit for quantitative codes — e.g., mg/dL for glucose |
panel-parent |
Panel(s) this observation belongs to |
UCUM units: Read
EXAMPLE_UCUM_UNITSfromcodesystem_lookupfor the recommended unit on any quantitative LOINC code. To validate a composed expression (e.g.,mg/dL,mmol/L) use:codesystem_verify_code(code="mg/dL", system="http://unitsofmeasure.org")
Always read
COMPONENTfromcodesystem_lookup— do not guess LP codes, do not scrape loinc.org. The LP code is in the response.
Step 3 — Build the ValueSet
Pattern A — all observations for a given analyte (COMPONENT filter):
{
"resourceType": "ValueSet",
"name": "GlucoseObservations",
"title": "Glucose Observations (LOINC)",
"status": "draft",
"compose": {
"include": [{
"system": "http://loinc.org",
"version": "<from list_available_codesystem_versions>",
"filter": [
{ "property": "COMPONENT", "op": "=", "value": "LP14635-4" },
{ "property": "STATUS", "op": "=", "value": "ACTIVE" }
]
}]
}
}
Pattern B — all active orderable lab codes in a CLASS:
{
"filter": [
{ "property": "CLASS", "op": "=", "value": "CHEM" },
{ "property": "CLASSTYPE", "op": "=", "value": "1" },
{ "property": "STATUS", "op": "=", "value": "ACTIVE" },
{ "property": "ORDER_OBS", "op": "in", "value": "Order,Both" }
]
}
Pattern C — panel members:
{
"filter": [
{ "property": "panel-parent", "op": "is-a", "value": "24323-8" }
]
}
Common LOINC Patterns
| Clinical goal | Key filter |
|---|---|
| All observations for one analyte | COMPONENT = <LP_code> + STATUS = ACTIVE |
| All active orderable lab tests in a domain | CLASS = <CLASS> + ORDER_OBS in Order,Both + STATUS = ACTIVE |
| All quantitative lab tests in a domain | CLASS = <CLASS> + SCALE_TYP = LP7753-9 |
| Members of a specific panel | panel-parent is-a <panel_code> |
| All tests in a hierarchical LOINC group | parent is-a <LP_or_LOINC_code> |
CLASS values for common domains:
| CLASS | Domain |
|---|---|
CHEM |
Chemistry |
HEM/BC |
Hematology / Blood count |
MICRO |
Microbiology |
UA |
Urinalysis |
RAD |
Radiology |
CARD |
Cardiology |
PATH |
Pathology |
COAG |
Coagulation |
ALLERGY |
Allergy |
DRUG/TOX |
Toxicology / Drug levels |
⚠️ Do not expand
SYSTEMLP codes. For common blood chemistry analytes,SYSTEM = LP7576-4(Ser/Plas) matches 1000+ unrelated codes.COMPONENTis the discriminating axis.
LOINC Limitations
- No semantic relationships. LOINC does not have "causative agent" or IS-A clinical hierarchies beyond its component/panel structure. For semantic reasoning about what a lab result means clinically, crossmap to SNOMED.
- COMPONENT filter is exact.
COMPONENT = LP14635-4(Glucose) finds codes with that exact LP code. It does not subsume related analytes. search_loincreturns semantic proximity, not structural siblings. Usesearch_loinconly for discovery; always follow with aCOMPONENTorCLASSfilter for a precise, exhaustive set.
RxNorm Workflow
When to use RxNorm
- Building drug ValueSets for formularies, CDS alerts, eCQM numerators
- Finding all clinical drug products containing a given ingredient
- Enumerating branded drug names for a generic
- Linking to NDC codes via
codesystem_lookup
Step 1 — Find the ingredient
search_rxnorm(query="<drug name>", top_k=5)
Select the result whose TTY is IN (Ingredient). If no IN result
appears, the search returned product-level codes — search again using
only the bare ingredient name stripped of dose, form, and strength
(e.g., "metformin" not "metformin 500 MG oral tablet").
Step 2 — Confirm the ingredient
codesystem_lookup(code="<RxCUI>", system="http://www.nlm.nih.gov/research/umls/rxnorm")
Verify TTY includes IN (Ingredient) and inactive = false.
The response will list has_ingredient properties pointing outward to all
drug products containing this ingredient — useful for previewing scope.
Step 3 — Build the ValueSet
Pattern A — all generic clinical drug products for an ingredient:
{
"resourceType": "ValueSet",
"name": "MetforminClinicalDrugs",
"title": "Metformin Clinical Drug Products (RxNorm)",
"status": "draft",
"compose": {
"include": [{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"version": "<from list_available_codesystem_versions>",
"filter": [
{ "property": "has_ingredient", "op": "=", "value": "6809" },
{ "property": "TTY", "op": "in", "value": "SCD,SCDF,SCDG" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}]
}
}
Pattern B — all branded products for an ingredient:
{
"filter": [
{ "property": "has_ingredient", "op": "=", "value": "<IN_RxCUI>" },
{ "property": "TTY", "op": "in", "value": "SBD,SBDF,SBDG" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
Pattern C — all products (generic + branded) for a specific dose form group:
{
"filter": [
{ "property": "has_ingredient", "op": "=", "value": "<IN_RxCUI>" },
{ "property": "TTY", "op": "in", "value": "SCD,SBD,SCDF,SBDF" },
{ "property": "has_doseformgroup", "op": "=", "value": "<DFG_RxCUI>" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
Use
search_rxnorm("<dose form> dose form group")to find the RxCUI for any dose form group — do not hard-code these values.
TTY Selection Guide
| Use case | Include these TTY values |
|---|---|
| Generic clinical drug products | SCD |
| Generic + dose form (no strength) | SCD,SCDF |
| Generic drug class groups | SCD,SCDF,SCDG |
| Branded products | SBD,SBDF,SBDG |
| Everything (generic + branded) | SCD,SCDF,SCDG,SBD,SBDF,SBDG |
| Ingredient only | IN |
| Combination ingredients | MIN |
RxNorm Expand Timeout Handling
RxNorm expansions can time out for popular ingredients (e.g., metformin,
aspirin, lisinopril) that have hundreds of associated products. If
valueset_expand returns a timeout error:
- Immediately fall back to
reasonhub-skills expand— do not retryvalueset_expandwith different parameters. - The ValueSet JSON is still the primary deliverable; expansion is informational only.
- Narrow the ValueSet (add
has_doseformgroup, restrictTTY) if the user needs a smaller set.
RxNorm Limitations
- Sparse hierarchy. RxNorm's
parent is-ahierarchy via RB relationships is thin. Usehas_ingredient+TTYfilters rather than hierarchy traversal for drug product sets. - No therapeutic class in FHIR filters. RxNorm does not expose ATC or NDF-RT drug class hierarchy as a filterable FHIR property. For drug-class ValueSets (e.g., "all beta-blockers"), enumerate ingredients individually and union them in a multi-include compose, or crossmap to SNOMED substance hierarchy.
- Combination drugs. A drug with two ingredients has two
has_ingrediententries. A single-ingredient filter will match combination products too (e.g., metformin/sitagliptin). Add a secondhas_ingredientexclude or restrictTTYto limit scope.
Cross-System ValueSets
When the user's use case spans systems (e.g., an eCQM that needs both diagnosis codes and lab tests), compose a multi-system ValueSet:
{
"resourceType": "ValueSet",
"name": "DiabetesManagementBundle",
"title": "Diabetes — Diagnoses (ICD-10-CM) + Glucose Tests (LOINC)",
"status": "draft",
"compose": {
"include": [
{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"version": "<icd10_version>",
"filter": [{ "property": "parent", "op": "is-a", "value": "E11" }]
},
{
"system": "http://loinc.org",
"version": "<loinc_version>",
"filter": [
{ "property": "COMPONENT", "op": "=", "value": "LP14635-4" },
{ "property": "STATUS", "op": "=", "value": "ACTIVE" }
]
}
]
}
}
Each
includesection must specifyversion. Uselist_available_codesystem_versionsto get current versions for all systems.
Output
Every query delivers two things.
1. Code System Selection Summary
State which system was selected and why (or which options were offered if disambiguated by the user):
Selected ICD-10-CM — query mentions diagnosis codes for an eCQM. Anchor:
E11Type 2 diabetes mellitus.
2. FHIR ValueSet JSON (always deliver this)
Return a complete ValueSet resource with name, title, status, and
a populated compose.include. This is the primary artifact.
3. 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 valueset_expand once. On failure or
timeout, immediately run reasonhub-skills expand — do not explain,
do not offer alternatives, just run it.
If expansion returns fewer rows than total, label the output as partial
and stop. Do not retry. See reasonhub-expand-mechanics
for expand failures, CLI fallback, scripting, and debugging.
Markdown table (default):
| Code | Display |
|---|---|
E11 |
Type 2 diabetes mellitus |
E11.9 |
Type 2 diabetes mellitus without complications |
CSV (when user asks to download or import):
code,display
E11,"Type 2 diabetes mellitus"
E11.9,"Type 2 diabetes mellitus without complications"
Worked Examples
ICD-10-CM: "All T2DM diagnosis codes for an eCQM denominator"
search_icd10("type 2 diabetes mellitus", top_k=5)
# → E11.6 (distance 0.063), E11 (distance 0.098)
codesystem_lookup("E11", "http://hl7.org/fhir/sid/icd-10-cm")
# display: "Type 2 diabetes mellitus"
# includes: "diabetes NOS", "insulin resistant diabetes"
# excludes1: type 1 (E10.-), gestational (O24.4-) — scope boundary confirmed
{
"resourceType": "ValueSet",
"name": "Type2DiabetesMellitusDx",
"title": "Type 2 Diabetes Mellitus — All ICD-10-CM Codes",
"status": "draft",
"compose": {
"include": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"version": "2026",
"filter": [{ "property": "parent", "op": "is-a", "value": "E11" }]
}]
}
}
Result: 65 codes covering all T2DM subtypes.
LOINC: "All active orderable glucose lab tests"
search_loinc("glucose blood measurement quantitative", top_k=5)
# → 110939-6 "Glucose [Measurement] in Blood" (distance 0.200)
# → 2345-7 "Glucose, Blood" also in scope
codesystem_lookup("2345-7", "http://loinc.org")
# COMPONENT = LP14635-4 (Glucose)
# CLASS = CHEM
# CLASSTYPE = 1
# ORDER_OBS = Both
# SCALE_TYP = LP7753-9 (Quantitative)
# EXAMPLE_UCUM_UNITS = mg/dL
{
"resourceType": "ValueSet",
"name": "GlucoseLabTests",
"title": "Glucose Lab Observations — Active Orderable (LOINC)",
"status": "draft",
"compose": {
"include": [{
"system": "http://loinc.org",
"version": "2.81",
"filter": [
{ "property": "COMPONENT", "op": "=", "value": "LP14635-4" },
{ "property": "STATUS", "op": "=", "value": "ACTIVE" },
{ "property": "ORDER_OBS", "op": "in", "value": "Order,Both" }
]
}]
}
}
RxNorm: "All generic metformin clinical drug products"
search_rxnorm("metformin ingredient", top_k=5)
# → 372803 "metformin Oral Tablet" (SCDF, distance 0.040)
# → ingredient is 6809
codesystem_lookup("6809", "http://www.nlm.nih.gov/research/umls/rxnorm")
# display: "Metformin"
# TTY includes: IN ← confirmed ingredient
# inactive: false
{
"resourceType": "ValueSet",
"name": "MetforminGenericDrugs",
"title": "Metformin Generic Clinical Drug Products (RxNorm)",
"status": "draft",
"compose": {
"include": [{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"version": "10062025",
"filter": [
{ "property": "has_ingredient", "op": "=", "value": "6809" },
{ "property": "TTY", "op": "in", "value": "SCD,SCDF,SCDG" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}]
}
}
⚠️ Metformin is a high-volume ingredient. If
valueset_expandtimes out, runreasonhub-skills expandwith the ValueSet JSON above.
Cross-system: "Diabetes eCQM — diagnoses + glucose tests + metformin drugs"
{
"resourceType": "ValueSet",
"name": "DiabetesECQMBundle",
"title": "Diabetes eCQM — ICD-10-CM + LOINC + RxNorm",
"status": "draft",
"compose": {
"include": [
{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"version": "2026",
"filter": [{ "property": "parent", "op": "is-a", "value": "E11" }]
},
{
"system": "http://loinc.org",
"version": "2.81",
"filter": [
{ "property": "COMPONENT", "op": "=", "value": "LP14635-4" },
{ "property": "STATUS", "op": "=", "value": "ACTIVE" }
]
},
{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"version": "10062025",
"filter": [
{ "property": "has_ingredient", "op": "=", "value": "6809" },
{ "property": "TTY", "op": "in", "value": "SCD,SCDF" },
{ "property": "inactive", "op": "=", "value": "false" }
]
}
]
}
}
When to Escalate to Other Skills
| Situation | Use instead |
|---|---|
| User wants semantic relationships (finding site, causative agent) | snomed-semantic skill |
| User has an ICD-10/LOINC/RxNorm code and wants SNOMED equivalents | terminology-crossmap skill |
| Expansion fails or you need bulk scripting | load reasonhub-expand-mechanics |
| User wants to understand what a SNOMED concept's attributes are | snomed-semantic skill |