Navigate the RxNorm drug graph
RxNorm links its drug concepts with directed relationships: a clinical drug consists of components,
a component has an ingredient, a branded drug is a tradename of a clinical drug. loadRxNormGraph
reads those links from an RxNorm release you supply, and the navigation calls follow them. The
engine bundles no RxNorm release. The rows below are synthetic, in the RRF format, with made-up
RXCUIs.
Load the release
Pass the contents of RXNCONSO.RRF (concepts) and RXNREL.RRF (relationships), and
RXNSAT.RRF if you want to resolve NDCs (see find RxNorm concepts).
import { loadRxNormGraph } from "@cosyte/terminology";
const graph = loadRxNormGraph({
conso: [
"1|ENG||||||||||RXNORM|IN||lisinopril||N||",
"2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||",
"3|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
].join("\n"),
rel: ["1|||RN|2|||has_ingredient|||RXNORM||||||", "2|||RN|3|||consists_of|||RXNORM||||||"].join("\n"),
version: "RXNORM_2026AA",
});
graph.conceptCount; // => 3
graph.edgeCount; // => 2
graph.warnings; // => []
Only RXNORM atoms define concepts; atoms from other sources are skipped without a warning. A row
of RXNREL.RRF is read the way RxNorm documents it: the relationship is the one the second
concept (RXCUI2, column 5) has to the first (RXCUI1, column 1). So the first row above says
concept 2 has ingredient 1. How the RxNorm graph is read explains the
direction and why it matters.
Find a drug's active ingredient
RxNorm authors the ingredient on the clinical drug component (SCDC), not on the clinical drug
(SCD). So ingredientsOf on an SCD returns an empty list, and the ingredient is two steps
away: the drug's component, then the component's ingredient.
import { loadRxNormGraph, consistsOf, ingredientsOf } from "@cosyte/terminology";
const graph = loadRxNormGraph({
conso: [
"1|ENG||||||||||RXNORM|IN||lisinopril||N||",
"2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||",
"3|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
].join("\n"),
rel: ["1|||RN|2|||has_ingredient|||RXNORM||||||", "2|||RN|3|||consists_of|||RXNORM||||||"].join("\n"),
});
// The clinical drug has no ingredient edge of its own.
const direct = ingredientsOf(graph, "3");
direct.found && direct.targets.length; // => 0
// Step 1: the drug's components. Step 2: each component's ingredients.
const components = consistsOf(graph, "3");
const ingredients = components.found
? components.targets.flatMap((component) => {
const r = ingredientsOf(graph, component.rxcui);
return r.found ? r.targets.map((ingredient) => ingredient.name) : [];
})
: [];
ingredients; // => ["lisinopril"]
ingredientsOf follows has_ingredient and has_precise_ingredient. It does not follow
has_boss (basis of strength), which is where a precise clinical drug form (SCDFP) points.
From a branded drug
A branded drug (SBD) consists of its branded component (SBDC) and the clinical component
(SCDC). The branded side's ingredient edge points to the brand name (BN), not to the active
ingredient. So pick the component by its term type:
import { loadRxNormGraph, consistsOf, ingredientsOf } from "@cosyte/terminology";
const graph = loadRxNormGraph({
conso: [
"1|ENG||||||||||RXNORM|IN||lisinopril||N||",
"2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||",
"4|ENG||||||||||RXNORM|BN||Zestril||N||",
"5|ENG||||||||||RXNORM|SBDC||lisinopril 10 MG [Zestril]||N||",
"6|ENG||||||||||RXNORM|SBD||lisinopril 10 MG Oral Tablet [Zestril]||N||",
].join("\n"),
rel: [
"1|||RN|2|||has_ingredient|||RXNORM||||||",
"4|||RN|5|||has_ingredient|||RXNORM||||||",
"4|||RN|6|||has_ingredient|||RXNORM||||||",
"5|||RN|6|||consists_of|||RXNORM||||||",
"2|||RN|6|||consists_of|||RXNORM||||||",
].join("\n"),
});
// The branded drug's own ingredient edge is its brand name.
const direct = ingredientsOf(graph, "6");
direct.found && direct.targets.map((c) => c.tty); // => ["BN"]
// Take the clinical component, then its ingredient.
const parts = consistsOf(graph, "6");
const clinical = parts.found ? parts.targets.find((c) => c.tty === "SCDC") : undefined;
const active = ingredientsOf(graph, clinical?.rxcui ?? "");
active.found && active.targets[0]?.name; // => "lisinopril"
Read the tty of what comes back rather than assuming it from what you asked.
Find a generic, its brands, and the dose form
import { loadRxNormGraph, genericFor, brandsFor, doseFormsOf } from "@cosyte/terminology";
const graph = loadRxNormGraph({
conso: [
"3|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||",
"6|ENG||||||||||RXNORM|SBD||lisinopril 10 MG Oral Tablet [Zestril]||N||",
"7|ENG||||||||||RXNORM|DF||Oral Tablet||N||",
].join("\n"),
rel: [
"3|||RB|6|||tradename_of|||RXNORM||||||",
"6|||RN|3|||has_tradename|||RXNORM||||||",
"7|||RN|3|||has_dose_form|||RXNORM||||||",
].join("\n"),
});
const generic = genericFor(graph, "6");
generic.found && generic.targets[0]?.name; // => "lisinopril 10 MG Oral Tablet"
const brands = brandsFor(graph, "3");
brands.found && brands.targets[0]?.name; // => "lisinopril 10 MG Oral Tablet [Zestril]"
const forms = doseFormsOf(graph, "3");
forms.found && forms.targets[0]?.name; // => "Oral Tablet"
genericFor follows tradename_of and brandsFor follows has_tradename. RxNorm ships both
directions as separate rows, and each call follows only the rows that exist: if your release has one
direction and not the other, the missing one returns an empty list rather than a reversed edge.
doseFormsOf follows has_dose_form and has_doseformgroup.
Follow any relationship
relatedByRela follows the relationship names you give it, one or several. RELA holds the common
names, and RELA_INVERSE pairs each with its inverse.
import { loadRxNormGraph, relatedByRela, RELA, RELA_INVERSE } from "@cosyte/terminology";
const graph = loadRxNormGraph({
conso: ["3|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||", "7|ENG||||||||||RXNORM|DF||Oral Tablet||N||"].join("\n"),
rel: "7|||RN|3|||has_dose_form|||RXNORM||||||",
});
const r = relatedByRela(graph, "3", [RELA.HAS_DOSE_FORM, RELA.HAS_TRADENAME]);
r.found && r.targets.map((c) => c.tty); // => ["DF"]
RELA_INVERSE[RELA.HAS_DOSE_FORM]; // => "dose_form_of"
Read the result
Every navigation call returns one of two shapes:
found: true, withtargets: the related concepts, in file order, each with itsrxcui,tty,nameandsuppressed. An emptytargetsmeans the concept is in your release and has no such relationship there.found: false, withTERM_RXNORM_UNKNOWN_RXCUI: theRXCUIyou asked about is not in your release.
import { loadRxNormGraph, ingredientsOf } from "@cosyte/terminology";
const graph = loadRxNormGraph({ conso: "", rel: "" });
ingredientsOf(graph, "99999999"); // => { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI", rxcui: "99999999" }
A relationship that points to a concept your release does not contain is left out of targets.
Reference
loadRxNormGraph,RxNormGraphSourceandRxNormGraphingredientsOf,consistsOf,genericFor,brandsFor,doseFormsOfandrelatedByRelaRxNormNavResult,RxNormRelated,RxNormUnknownandRxNormConceptRELA,RELA_INVERSEandTERM_TYPES- Codes:
TERM_RXNORM_UNKNOWN_RXCUI,TERM_RXNORM_MALFORMED_ROWandTERM_RXNORM_UNTYPED_CONCEPT