Skip to content

Extracting Medication Lists From a Medical Record

11 min read · updated August 11, 2026

A discharge summary can contain three medication lists that contradict each other, and all three are correct. Getting the medications out of a record starts with deciding which list you were asked for, and ends with normalising free text onto a code system that can survive a brand name changing.

Which list are you extracting?

The lists in a typical inpatient document answer different questions about different points in time:

  • Home or prior-to-admission medications — what the patient reported taking before arriving. Often the least reliable, because it is a patient-reported history.
  • Inpatient orders — what was administered during the stay, including one-off doses and things that were stopped after a day.
  • Discharge medications — the instruction the patient leaves with, which is the one a downstream consumer usually means.

Reconciling these against each other is a clinical process with its own accreditation requirements — the Joint Commission’s national patient safety goal on maintaining and communicating accurate medication information is precisely about it. That reconciliation is not your pipeline’s job and should not be attempted by a model. Your job is to keep the three lists apart, tag each row with the section heading it came from, and let the difference between them remain visible. A single flat array is a lossy answer to a question nobody asked.

A row is not a drug

A medication line carries at least six things, and they are not separated by anything consistent:

Lisinopril 10 mg oral tablet — take 1 tablet by mouth daily — #30, 3 refills
 |          |     |            |                                    |    |
 ingredient strength form       sig                                 qty  refills

Three structural traps live in that line. First, combination products: a row reading lisinopril-hydrochlorothiazide 20-12.5 mg is one product with two ingredients and two strengths, positionally paired. Splitting on the hyphen gives you four tokens with no way to know which strength belongs to which ingredient except by order, and a schema with a single strength string cannot represent it honestly. Model the product as having a list of components, each with its own ingredient, numerator and unit, even though the common case has exactly one.

Second, strength is a ratio, not a number. 10 mg on a tablet is per unit; 25 mg/mL on a suspension is a concentration; 0.05% on a cream is a mass fraction. Storing a numeric strength and a string unit handles the first and mangles the other two. Store numerator value, numerator unit, denominator value and denominator unit, and let the denominator be one unit of the dose form when it is not printed.

Third, the dose form and the route are different fields that look alike. “Oral tablet” is a form; “by mouth” in the sig is a route. They usually agree and occasionally do not, and when they disagree the row deserves review rather than a silent choice between them — a cross-field validation rule rather than a field-level one.

The NDC and its two digit lengths

Where a dispensed product is recorded, you will meet the National Drug Code. The FDA publishes the NDC Directory, and the code has exactly three segments: a labeler code identifying the firm, which the FDA assigns; a product code identifying strength, dose form and formulation; and a package code identifying package size. The firm assigns the last two itself.

The problem is that the segments do not have fixed widths. The code as the FDA assigns it is ten digits in one of three configurations — 4-4-2, 5-3-2 or 5-4-1 — while billing formats require eleven digits in a fixed 5-4-2 layout. Converting between them is a matter of inserting one leading zero into whichever segment is short:

10-digit configuration → 11-digit (5-4-2), by padding one segment

  4-4-2   1234-5678-90   →  01234-5678-90   → 01234567890
  5-3-2   12345-678-90   →  12345-0678-90   → 12345067890
  5-4-1   12345-6789-0   →  12345-6789-00   → 12345678900

All three source codes above are ten digits. Two of them are the
same ten digits in different places.

Read that last line again, because it is the whole difficulty. If the hyphens are present, the configuration is unambiguous and the padding is mechanical. If they have been stripped — and they usually have, by the time a code reaches a database column or an OCR pass over a label — a bare ten-digit string does not tell you which segment is short. 1234567890 could be 4-4-2 or 5-3-2 or 5-4-1, and the three readings normalise to three different eleven-digit codes identifying three different products. There is no arithmetic that resolves it: the NDC has no check digit. The only resolution is to look the candidates up in the FDA directory and see which one exists, or to keep the hyphens from the source and never lose them.

Prefer to store the code exactly as printed, with its hyphens, in a ndc_as_printed field, alongside a normalised eleven-digit form and a flag recording how the normalisation was decided. Reconstructing a lost configuration later is impossible; recording it costs one column. The FDA’s National Drug Code Directory is the authority on which codes exist, and it changes continuously as products are listed and delisted.

RxNorm as the normalisation target

Free-text medication strings are not comparable to each other. The same product appears as a brand name, a generic name, an abbreviated generic, and a misspelling, and no amount of string similarity makes HCTZ 12.5 match hydrochlorothiazide 12.5 mg tab reliably. RxNorm, published by the US National Library of Medicine, exists for exactly this. It assigns an RXCUI — a concept unique identifier — to normalised drug concepts, and types each concept with a term type.

  • IN — ingredient, for example the concept “lisinopril” alone.
  • BN — brand name, with no strength or form attached.
  • SCD — semantic clinical drug: ingredient, strength and dose form together, in the form “Lisinopril 10 MG Oral Tablet”. This is the level a medication list should normalise to.
  • SBD — the branded equivalent of an SCD.
  • GPCK / BPCK — packs, where a single dispensed item contains more than one drug concept.

Normalising to the SCD rather than to the ingredient is what makes two records comparable without throwing away strength, and it is also what makes the brand/generic problem disappear: an SBD and its corresponding SCD are linked in the release, so a brand string resolves to the same clinical concept as its generic. RxNorm ships a full release monthly with weekly updates, and it carries NDC properties, so a dispensed NDC can be walked to a concept and a concept walked back to the set of NDCs that realise it.

Two cautions. RxNorm’s approximate-match service will return a candidate for almost any string you hand it, including one that is wrong, so treat a match as a suggestion with a score rather than as a resolution. And the NDC-to-RXCUI relationship is time-dependent: an NDC that appeared on a chart five years ago may not be in the current release at all. Record the release version you matched against, in the same spirit as versioning the schema itself.

Where the extraction goes wrong

The happy path is a clean table. The failures are mundane and they all produce output that validates:

  • A wrapped row. A long sig wraps to a second physical line with no drug name on it. Row-based extraction emits an orphan line with a sig and no medication, or worse, attaches the wrapped sig to the next drug down.
  • A struck-through or annotated entry. Discontinued medications are often printed with a stop date, a strikethrough, or the word “D/C” in a status column. A vision model reading the text and not the strike returns an active medication that was stopped.
  • The same drug twice. A patient legitimately takes two strengths of the same ingredient, and also legitimately appears twice because the brand row and the generic row were never merged. These look identical after normalisation to ingredient and different after normalisation to SCD, which is another reason to normalise at SCD.
  • A decimal that is not a decimal. A dose printed without a leading zero — a naked point-five — is on the abbreviation hazard lists for exactly the reason that it OCRs as the whole number. A magnitude check against the plausible strengths for that ingredient in RxNorm is a genuine catch here, and it is one of the few places where a downstream lookup can detect an upstream reading error.

None of these are model quality problems and none of them are fixed by a better prompt. They are document-layout problems, which is why reading order and table structure are worth handling with the tools in PDF parsing and merged-cell table extraction before the text reaches a model at all.