Extracting Allergies and Adverse Reactions From a Medical Record
10 min read · updated August 11, 2026
Every other field in this cluster is extracted by finding something. The allergy field is extracted by distinguishing between three different kinds of nothing, and a schema that returns an empty array for all three has destroyed the information before anybody sees it.
Three states, not two
Consider what an empty result means. A document might contain an allergy section that says “NKDA”. It might contain an allergy section that is blank. It might contain no allergy section at all, because the document is a two-page operative note that was never going to have one. These are three genuinely different facts and they license three different downstream behaviours, and a schema whose allergy field is string[] renders all three as [].
The fix is structural rather than prompt-level. The field needs a status alongside the list:
{
"allergy_status": "none_asserted" | "present" | "not_documented",
"allergies": [ ... ],
"source_section": "Allergies",
"source_span": [1840, 1846]
}none_asserted means the document positively states there are none, and carries a span pointing at the statement. present means the list is populated. not_documented means the extractor looked and found no assertion either way, and it is the value that must never be silently converted into the first one — the general form of that distinction is missing required field handling.
The failure mode this prevents is concrete. A batch of a thousand records is processed; three hundred of them are consultation letters with no allergy section. With a two-state schema, those three hundred arrive downstream indistinguishable from patients affirmatively documented as having no allergies. Nothing errors, nothing is flagged, every record validates against the schema, and the resulting dataset asserts something about three hundred people that no clinician ever wrote. The tri-state schema turns that from a silent data-quality incident into three hundred rows that are visibly incomplete — which is a report, not a bug.
Prompting has to match the schema. A model instructed to “list the patient’s allergies” is being asked a question that has no permitted answer for the third case, and it will produce the most plausible-sounding one. Instruct it instead to report what the document states about allergies, with an explicit enumerated value for “the document does not address this”, and constrain the output with a strict schema so the enumeration is enforced rather than hoped for — see JSON mode versus structured outputs for why the difference between those two matters here.
What NKDA actually asserts
The abbreviations are not interchangeable and the difference is load-bearing. NKDA is no known drug allergies. NKA is no known allergies, which is broader and covers food and environmental agents. “No known allergies to medications” and “allergies: none” under a heading that only ever held drugs are both narrower than they look.
All of them contain the word “known”, and that word is doing real work: the assertion is about the state of documentation, not about the patient’s immune system. An extraction that renders NKDA as “patient has no allergies” has strengthened a claim the clinician deliberately hedged. Keep the verbatim string.
Watch also for the negated allergy inside a populated list — an entry reading “penicillin — tolerated on rechallenge, allergy disproven”. That row belongs in the output with a status that says it was refuted, not omitted and not listed as active.
Coding the allergen
An allergen is written at whatever level of specificity the clinician had in mind, and the levels are not interchangeable:
- An ingredient — “penicillin”. Maps to an RxNorm ingredient concept, and the whole class is implied.
- A drug class — “sulfa drugs”, “NSAIDs”. Not a single RxNorm ingredient at all; a class assertion needs a class vocabulary to hold it.
- A branded product — a trade name, which resolves through RxNorm to its ingredients.
- A non-drug substance — shellfish, latex, bee sting, contrast media. These are not in RxNorm; SNOMED CT is the vocabulary that covers substances generally.
So the allergen field needs a coded value and a coding system identifier, because a single-system design forces every non-drug allergen into an uncoded string. Record the verbatim text in all cases. A latex allergy stored as a null RxNorm code with no text is worse than no extraction, because it looks like a completed field.
Class assertions deserve one more note. “Sulfa allergy” is shorthand that covers a set of agents whose membership is a clinical question, and expanding it into a list of specific drugs is a clinical judgement, not a normalisation step. Store the class as stated.
Allergy, intolerance and side effect
The chart uses one section for three different things: a true immunologic allergy, an intolerance (a drug the patient cannot take but which is not immune-mediated), and a documented adverse effect. A rash and nausea are both written in the same column and mean different things.
HL7 FHIR models this explicitly, and its vocabulary is worth adopting even if you are not producing FHIR resources, because it has already made the distinctions you would otherwise invent badly. The R4 AllergyIntolerance resource carries a type of allergy or intolerance, a category from food, medication, environment or biologic, a clinicalStatus of active, inactive or resolved, and a verificationStatus from unconfirmed, presumed, confirmed, refuted and entered-in-error.
That verification vocabulary is the one to notice. Most allergies in most charts are unconfirmed patient report, and the resource has a value for that. It also has refuted, which is where the disproven-penicillin row goes, and entered-in-error, which is where a row explicitly annotated as a mistake goes. An extractor that only knows “present” has to throw those away.
Criticality is not severity
The last confusion is between two fields that both sound like “how bad is it”. They are not the same and FHIR keeps them apart deliberately.
Severity attaches to a reaction that happened: mild, moderate or severe, describing that historical event. Criticality attaches to the allergy as a whole and is a forward-looking estimate of the risk of a serious reaction on re-exposure, with values of low, high, or unable to assess. A patient whose only recorded reaction was a mild rash can carry high criticality; a patient with a severe recorded reaction to something they will never be given again may not.
Extraction cannot invent criticality. It is a clinical assessment, and if the document does not record one, the field is null. What extraction can do is capture severity where the document states it, keep each reaction as its own object rather than a comma-joined string — a single entry may record several manifestations — and preserve the onset date when present, because “childhood reaction, never rechallenged” is a different clinical picture from one recorded last month, and nobody downstream can recover it from the allergen name.
Finally, the handling obligation. An allergy list is small, which makes it tempting to ship the whole allergy section verbatim to a model and keep the text. Resist it. The section commonly sits directly beneath the patient banner and the two are captured together by any crop generous enough to be reliable, so the identifiers travel with the clinical field for no benefit. Crop or redact before the page leaves your infrastructure, keep only the fields you need under the minimum necessary standard, and make sure the provider that sees whatever remains is covered by a business associate agreement — the general mechanics of that are in PII redaction and PII in LLM logs, and the logs are the part people forget.