Extract structured variables from free-text clinical notes

Point Claude Code at a folder of de-identified clinical notes and a codebook of variables you need; get back a committed, one-row-per-patient registry table with every extracted value traceable to the source sentence.

   
Problem class Data analysis
Subject areas Translational Medicine
Evidence level Validated
Complexity Claude Code alone
Availability Fully open
Compute Laptop

Problem

Most clinically actionable detail lives in unstructured notes — diagnosis, stage, prior lines of therapy, biomarker status, acute symptoms — not in the structured EHR fields. Building a disease registry, screening a cohort for a study, or assembling an analysis-ready table means someone reads each note and transcribes a fixed set of variables by hand. For a few hundred patients this is weeks of chart abstraction, it is inconsistent between abstractors, and the rules used are rarely written down, so the next team re-abstracts from scratch.

“Solved” looks like: hand over a folder of de-identified notes plus a codebook (the exact variables, their allowed values, and the abstraction rules), get back a registry.csv — one row per patient, one column per variable — where every non-null cell carries the source note ID and the quoted sentence it came from, produced by a committed prompt/script so the run is auditable and re-runnable rather than a one-off chat.

  1. Write the codebook first. Before any extraction, define a codebook.md: each variable, its type (binary / categorical with allowed values / free number), and a one-line abstraction rule. This is the spec the run is graded against; it belongs under version control alongside the outputs. Keep the note folder de-identified — this recipe assumes notes are already stripped of PHI (see Availability).

  2. Have Claude extract to a structured table with per-cell evidence. Point Claude Code at the notes folder and the codebook. Force it to quote the source sentence for every value and to distinguish “not mentioned” from “explicitly negated”:

    Read codebook.md. For every note under ./notes/, extract exactly the
    variables it defines. Emit one row per note into records.jsonl with:
    note_id, and for each variable: value, evidence_quote (verbatim
    sentence from the note), and status (found | negated | not_mentioned).
    Use only allowed values from the codebook; never infer a value the
    note does not state. If a variable is not addressed, set
    status=not_mentioned and value=null — do not guess.
    
  3. Capture the extraction as a committed script, not a chat. Ask Claude to write the loop it just ran to a versioned file so the run is reproducible over the whole folder:

    Write extract_registry.py that iterates ./notes/, applies the
    codebook prompt to each note via the Anthropic API, validates each
    value against the codebook's allowed set, and writes records.jsonl
    plus a flattened registry.csv (one row per note_id, one column per
    variable). Pin the environment in requirements.txt.
    
  4. Record provenance. External-service and model calls are not byte-reproducible, so capture what pins the run: provenance.json with the model id and version, the codebook sha256, the number of notes, the run date, and per-variable extraction counts. Commit codebook.md, extract_registry.py, the pinned env, records.jsonl, registry.csv, and provenance.json. records.jsonl (with the evidence_quote per cell) is the audit trail — any downstream claim must trace to a quote in it. See the reproducibility guide.

  5. Validate against a gold subset. Have a clinician manually abstract 20–30 notes, then compute per-variable accuracy / F1 of the extraction against that gold set. Report it in provenance.json. This is the number that tells you whether the registry is trustworthy for your notes and codebook.

  6. (Optional) Harmonize the extracted terms to standard codes. If the extracted diagnoses/drugs/labs must join other cohorts, feed the distinct values into the harmonize free-text clinical terms recipe to produce an ICD-11/RxNorm/LOINC crosswalk. That is a separate, rung-2 step — keep it out of the extraction artifact.

Why this assembly

Rung 1 of the simplicity ladder. Structured extraction from free text is exactly what a frontier LLM does well unaided — no MCP, no skill, no external database is needed to read a note and fill a table. The discipline that makes it a reproducible artifact rather than a chat (a written codebook, per-cell evidence quotes, a committed script, a provenance record, and a gold-set accuracy check) is prompt structure plus a small script, not another component. Escalating to rung 2 buys nothing for the extraction itself; the only genuinely separate task — mapping the extracted terms to authoritative code systems — is handled by a sibling recipe and would confabulate codes if folded into rung 1 (which is precisely why coding is not done here).

Availability

Fully open — the recipe needs only Claude Code and an Anthropic API key; no external service or subscription. The binding constraint is data governance, not licensing: clinical notes are PHI. Run only on de-identified notes, under the data-use agreement and IRB/privacy approvals that cover your cohort, and confirm your Anthropic API tier’s data-handling terms meet your institution’s requirements (a BAA / zero-retention configuration where required). The output is a research artifact, not a clinical record; any registry feeding patient care needs clinician sign-off.

Compute requirements

Laptop-sufficient. Each note is one API call of 1–2 seconds; a few hundred notes complete in minutes and cost roughly US $0.001–0.11 per note-prompt depending on model and note length (≈ $0.05–$10.50 per note if you run multiple prompts and repeats for stability). No GPU, negligible local RAM; the deliverable is a small CSV/JSONL.

Evidence

Validated. Chen et al., J. Med. Internet Res. 2026 benchmarked 12 LLMs extracting structured binary variables from 100 interstitial-lung-disease clinic notes against a three-physician consensus gold standard: Claude 3.5 Sonnet reached 96.2% accuracy — identical to the human clinicians — processing each note-prompt in 1–2 s at $0.001–0.11 per call; seven models matched human-level accuracy, while multiclass classification was lower (88–91%). Bhayana et al., Radiology 2025 extracted 10 oncologic-history parameters from 200 EHR notes at F1 = 0.983, and radiologists preferred the LLM-generated histories 89% vs 5%. Both validate the exact task — an LLM reading free-text notes into a fixed structured schema at human-level accuracy — though neither packaged it as this committed-artifact workflow; treat your own gold-subset accuracy (step 5) as the confirming number for your codebook and note style.

Alternatives considered

  • Classical clinical NLP (cTAKES, MetaMap, CRF pipelines). Purpose-built extractors like the CRF geriatric-syndrome model of Chen et al., JMIR Med. Inform. 2019 (patient-level F1 = 0.83) are the pre-LLM standard and remain a fit for very large batches or fully on-prem constraints. Reach for them when you cannot send notes to an API at all; the LLM path wins on setup speed, codebook flexibility, and accuracy on nuanced variables.
  • PyHealth pipeline (rung 2). The PyHealth skill is the right tool when extraction is one step of a downstream predictive-modeling pipeline (e.g., readmission risk) rather than a standalone registry — see Predict hospital readmission from an EHR cohort. For building an analysis-ready table from notes, plain Claude Code is simpler.
  • Escalating to rung 2 for the extraction itself. Unnecessary — no external database is queried during extraction. The only rung-2 step is optional downstream code harmonization, handled by its own recipe.

See also

Sources


Tried this recipe?

Share feedback — what worked, what didn’t, what you’d change. The form opens with this recipe pre-selected and a link back to this page.