Connector ema — European Medicines Agency, centrally authorised medicines (EU)
| Source | https://www.ema.europa.eu/en/medicines/download-medicine-data — EMA "Download medicine data": the medicines table (every medicine evaluated under the centralised procedure, human and veterinary; one row per EMA product number) as an xlsx report |
| Tier / category | 3 / regulatory (CLAUDE.md §13 — jurisdiction EU, authority EMA, approval_type 'centralised'; never a bare approved = true) |
| Access | Bulk download, no authentication: https://www.ema.europa.eu/en/documents/report/medicines-output-medicines-report_en.xlsx (897,597 bytes, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) |
| Docs verified | 2026-09-11 — download page + the live file with User-Agent: CancerIndex/0.1; EPAR URL pattern verified (HTTP 200) for xeljanz, keytruda, imfinzi |
| License | EMA legal notice (https://www.ema.europa.eu/en/about-us/legal-notice) — licenseStatus: approved, commercialUse: allowed, redistribution: attribution; attribution: "Source: European Medicines Agency (www.ema.europa.eu), medicines data download; © EMA, reproduced with acknowledgement" |
| Code | packages/connectors/src/connectors/ema/ (manifest.ts, normalize.ts, index.ts, ema.test.ts, fixtures/ incl. build-fixtures.ts); dependency exceljs (xlsx parsing) |
Terms and licence (quoted)
From the legal notice, section "European Medicines Agency copyright and limited reproduction
notices" (verified 2026-09-11, stored verbatim in manifest.termsNotes):
Information and documents made available on EMA's webpages are public and may be reproduced and / or distributed, totally or in part, irrespective of the means and / or the formats used, for non-commercial and commercial purposes, provided that EMA is always acknowledged as the source of the material. Such acknowledgement must be included in each copy of the material. Citations may be made from such material without prior permission, provided the source is always acknowledged. The above-mentioned permissions do not apply to content supplied by third parties.
The medicines table is EMA's own content (no third-party copyright is asserted on it). CancerIndex reproduces the EMA product number, status, dates, flags and the therapeutic-indication summary as a dated, sourced regulatory fact per medicine — never as a treatment recommendation.
Update frequency (download page): "The website automatically updates these data tables
overnight." The connector runs weekly (0 5 * * 3), one download per run (≈ 3 s end to end),
1 request/s, no documented rate limit.
Verified file layout (2026-09-11)
One sheet, "Medicine". Row 1 = Content type: | Medicine | Output automatically generated from content on www.ema.europa.eu | 11/09/2026 - 06:00 (the generation timestamp becomes
datasetVersion = ema-medicines-2026-09-11T06:00); rows 2–8 empty; row 9 = header; data from
row 10 — 2,732 rows (2,339 Human, 393 Veterinary). Columns are matched by normalized header
text (lower-cased, whitespace and embedded newlines collapsed — "Species\n(veterinary)",
"Pharmacotherapeutic group\n(human)"), never by position (COLUMNS in normalize.ts). A missing
required column fails the run loudly (SchemaDriftError: required column(s) missing …);
unknown columns are kept in raw.extra and reported; missing optional columns are warned.
| Column (header) | Used as |
|---|---|
Category |
Human / Veterinary — only Human rows are mapped |
Name of medicine |
medicine name → drug_codes.ema_product.label, brand alias (single-drug medicines), raw.medicine |
EMA product number |
EMEA/H/C/004214 → application_number, drug_codes.ema_product, source record id, edge source_record_id |
Medicine status |
Authorised, Withdrawn, Application withdrawn, Refused, Lapsed, Opinion, Expired, Revoked, Suspended, Opinion under re-examination, Withdrawn from rolling review → status mapping below |
International non-proprietary name (INN) / common name, Active substance |
substances (; / / separated; INN preferred, active substance = salt form fallback) |
Therapeutic area (MeSH) |
;-separated MeSH descriptors → scope + mesh_therapeutic_area edges |
ATC code (human), Pharmacotherapeutic group (human) |
scope (L01/L02), drug_codes.atc (label = group) |
Therapeutic indication |
official summary → indication (≤ 4,000 chars, full text in raw.indicationFull), cancer mapping |
Accelerated assessment, Conditional approval, Biosimilar, Generic, Orphan medicine, PRIME: priority medicine, Advanced therapy, Exceptional circumstances, Additional monitoring |
Yes/No flags → accelerated, conditional, raw.flags |
Marketing authorisation developer / applicant / holder |
raw.holder |
Marketing authorisation date |
approval_date (initial authorisation) |
European Commission decision date |
raw.ecDecisionDate only — it is the latest Commission decision (Xeljanz: 23/07/2026 vs authorisation 22/03/2017; for withdrawn medicines often the withdrawal decision), so it is never used as approval date |
Withdrawal / expiry / revocation / lapse of marketing authorisation date, Suspension of marketing authorisation date |
withdrawal_date (never derived from anything else) |
Last updated date |
source_records.source_updated_at, provenance.updated_at_source |
Medicine URL |
https://www.ema.europa.eu/en/medicines/human/EPAR/<slug> → provenance.source_url (fallback: the download page) |
Dates are DD/MM/YYYY (emaDate: malformed / impossible → null, never fabricated).
Ingestion
- Download + anomaly guard: one GET; < 50 KB or < 1,000 parsed rows →
anomaly:error;ctx.guardCount('medicine', rows)against the previous successful run (first run = baseline). - Raw layer:
workbooksource record (the file bytes, base64 + sha256, keyed by dataset version —--mode backfillreplays it without HTTP) and onemedicinerecord per row (payload = the parsed row, every cell verbatim). Rows are processed sorted by product number;ctx.cursor.lastProductNumberresumes a run stopped by the time budget; pass bookkeeping (passCount) recordsapproval_added/approval_status_changedchange events from pass 2. - Scope (
classifyOncology, human rows only) — a medicine is oncology when at least one holds:atc_l01_l02: anATC code (human)starts with L01 (antineoplastic) or L02 (endocrine therapy);mesh_neoplasm: a MeSH therapeutic area that (a) carries neoplasm vocabulary (ONCOLOGY_LABEL_RE) and (b) resolves throughcancer_aliases(CancerResolver.byLabel, MeSH inversion;mesh_headingaliases from the MeSH connector) to an active cancer entity. Generic descriptors (Neoplasms,Cancer,Carcinoma,Neoplasm Metastasis…) never count; benign conditions the ontology knows as entities (Leiomyoma,Hemangioma,Prostatic Hyperplasia,Dupuytren Contracture,Amyloidosis) fail (a) and are not oncology;indication_text: the indication matches the ClinicalTrials.gov oncology vocabulary (ONCOLOGY_LABEL_RE) after neutralising two prose false positives: tumour necrosis factor (anti-TNF biosimilars) and the lower-case word all (the\ball\babbreviation of ALL). Everything else is counted (nonOncology) and stays in the lake. Cancer mapping and drug minting are only allowed when the medicine is oncology by class (ATC L01/L02, or MeSH + oncology text), is not a diagnostic (ATC V04 / V08 / V09), and has at least one therapeutic sentence. Medicines in scope by indication text alone (denosumab for bone loss in prostate cancer, G-CSF, epoetins, fentanyl for cancer pain, antiemetics…) get an honest EU approval row when their substance is already a known drug, withcancer_id NULL, no edge and no minted drug.
- Substances → drugs (deterministic, no LLM): the INN cell is split on
;and/(never on commas — vaccine names; a/inside brackets is part of the name). Lookup indrug_aliases.normalized(in-memory index, kept in sync): the full label first, then the molecule with the salt form stripped (parseIngredientNameshared with Health Canada: "abiraterone acetate" → abiraterone, "niraparib (tosilate monohydrate)" → niraparib). Exactly one drug →ALIAS; several → thegenericalias, else the drug's own name, else ambiguous →unresolved_labels(ambiguous_alias: dacomitinib, trastuzumab deruxtecan on 2026-09-11 — two entities each). Biosimilars / generics (Biosimilar/Generic= Yes) resolve to the existing molecule (Herzuma, Ontruzant → Trastuzumab; Bortezomib Hospira → Bortezomib) — they never mint.- Unknown INN, single substance, authorised or formerly authorised, oncology by class,
therapeutic → a drug is minted (
mintId 'DRUG'; name = INN in Title Case — long common names with capitals, brackets or commas are kept verbatim;slugwith-ema-<number>on clash;developmentStatus 'marketed_eu'when authorised; aliasesgeneric,salt,brand; change eventcreated). - Otherwise →
unresolved_labelswith the reason:component_of_combination,not_authorised_medicine(refused / withdrawn application / pending opinion),diagnostic_agent,scope_from_indication_text_only,scope_from_mesh_area_only,non_therapeutic_indication(epoetin alfa, luspatercept, imetelstat, Gliolan),no_inn_or_active_substance(older withdrawn entries). - Rows are processed in product-number order, so a medicine whose INN is minted by a later row
of the same pass (Prolia → denosumab minted by Yaxwer) is retried at the end of the pass:
one run converges (
retried 103 … 2 gained rowson the first run).
- Unknown INN, single substance, authorised or formerly authorised, oncology by class,
therapeutic → a drug is minted (
- Codes and aliases per resolved drug:
drug_codesema_product(code = product number, label = medicine name) andatc(one per code, label = pharmacotherapeutic group),match_type EXACT_IDENTIFIER; abrandalias = medicine name without "(previously …)" for single-drug medicines only (a combination name is not an alias of one component). - Status (
mapEmaStatus) —Authorised→approved(conditional = truewhen Conditional approval = Yes,accelerated = truewhen Accelerated assessment = Yes — a review track, noted inraw.acceleratedNote);Withdrawn,Lapsed,Expired,Revoked,Suspended→withdrawnwithwithdrawal_datefrom the explicit column (null otherwise; a suspension is reversible — the verbatim status stays inraw.emaStatusandraw.emaStatusNotesays it is the medicine's, not the molecule's);Refused,Application withdrawn,Withdrawn from rolling review,Opinion,Opinion under re-examination→ no approval row (a refusal or a pending opinion is not an approval; counted, lake only; any existing edge of that medicine is superseded). - Approval row per (medicine, drug) —
drug_approvals:jurisdiction 'EU',authority 'EMA',approval_type 'centralised',application_number= product number,approval_date= marketing authorisation date,indication= the official summary (≤ 4,000 chars, "…"),rawwith everything above plusscope,substances,flags,cancer_match,cancerCandidates,meshCancerIds,therapeutic. Idempotent key(source, drug, 'EU', product number): existing rows are updated in place, nothing is deleted. Provenance: one row per medicine and run when the source record changed (sourceUrl= EPAR page,dataset 'EMA medicines data (xlsx)',datasetVersion,evidenceType regulatory,accessLevel open,geography 'EU',publishedAt= approval date,updatedAtSource= last updated date, methodology text); an unchanged medicine whose rows exist reuses its previous provenance row. - Cancer mapping (
resolveIndication, openFDACancerDictionary/resolveBullet/CancerReconcilerimported from../openfda/normalize.js): the indication is split into paragraphs, then;/:clauses and sentences (EMA lists indications as "…: as monotherapy for …; for treatment of …"). Sentences whose object is supportive care, prevention or diagnosis (isSupportiveCareSentence: "prevention of skeletal events", "treatment of symptomatic anaemia", "patients receiving chemotherapy", "active immunisation…", "visualisation of…", "vasomotor symptoms"…) are flagged and never yield a cancer; an introduction ending with ":" hands its nature to the clauses it introduces. Each remaining sentence → 0 / 1 cancer (PROBABILISTIC, several mentions collapse only along one lineage). The whole indication → one cancer →cancer_id; otherwisecancer_id NULLand the distinct cancers inraw.cancerCandidates.tumor_agnosticwhen a sentence pairs "solid tumours" with an agnostic biomarker. The dictionary is built without the build-time reconciler: for an alias shared by a parent and its child ("breast cancer" → Malignant Breast Neoplasm / Breast Carcinoma / Childhood Breast Carcinoma) the narrowest-of-lineage rule picked the child, so adult breast-cancer indications mapped to Childhood Breast Carcinoma (verified 2026-09-11); the resolver's preferred / display-name disambiguation decides instead. - Knowledge edges — for authorised medicines only: one
drug APPROVED_FOR canceredge per cancer named by a therapeutic sentence (evidenceLevel 'EMA indication text'), plus one per MeSH therapeutic area (evidenceLevel 'mesh_therapeutic_area', only when the text did not already name that cancer) —evidenceCategory regulatory_status,cancer_context_ids = [cancer],provenance_ids,source_record_id= product number. Edges of a medicine that is no longer authorised, or whose indication no longer names the cancer, are set tostatus 'superseded'. Multi-indication drugs therefore get one edge per cancer (Keytruda: 15 text edges + 4 MeSH).
dry_run downloads, parses and maps without writing (25 sample lines + totals); --mode backfill
replays the lake copy of the workbook (fallback: the per-row medicine payloads) — no HTTP, cursor
untouched. Health check: HEAD on the xlsx must answer a spreadsheet content-type ≥ 100 KB.
Observed runs (local copy of production, 2026-09-11)
| Dry run | 2,732 rows (2,339 human, 393 veterinary) in 1.7 s; 620 human medicines in scope — ATC 418, MeSH 462, indication text 573 (text-only 111); out of scope 1,719 |
First run (ING-EMA-20260911-000001) |
3.0 s, 1 download. Statuses among scoped medicines: Authorised 446, Withdrawn 84, Application withdrawn 68, Refused 23, Lapsed 6, Opinion 5, Expired 2, Revoked 1, Opinion under re-examination 1. 622 substances: 475 by alias + 4 after salt stripping, 36 minted, 2 ambiguous, 113 unresolved (queued), 13 medicines without INN. 459 EU approval rows on 245 drugs / 456 medicines: 385 approved (31 conditional, 12 accelerated), 74 withdrawn (Withdrawn 66, Lapsed 5, Expired 2, Revoked 1; 48 with a withdrawal date), 457 with an approval date, 190 with cancer_id, 4 tumour-agnostic, 71 flagged non-therapeutic (supportive care). 859 edges: 529 EMA indication text (188 drugs × 95 cancers), 330 mesh_therapeutic_area (126 drugs × 56 cancers). 571 codes (459 ema_product, 112 atc), 187 brand + 36 generic + 4 salt aliases, 459 provenance rows, 2,732 + 1 source records (2.9 MB in the lake). Retry pass: 103 deferred medicines, 2 gained rows |
| Second run (idempotency) | 2,733 source records unchanged, 0 created / 459 updated approval rows, 0 provenance rows, 0 change events |
| Backfill | replayed ema-medicines-2026-09-11T06:00 from the lake: same counts, 0 HTTP requests |
pnpm cix reconcile-drugs |
linked trial interventions 78,105 → 78,983 (+878, all to the minted drugs: Thalidomide 236, Imiquimod 116, Denosumab 74, Serplulimab 69, Talimogene laherparepvec 43, Duvelisib 38, Tivozanib 32…) |
Minted (36): 5-Aminolevulinic Acid, Alitretinoin, Bexarotene, Cinacalcet, Denosumab, Dinutuximab Beta, Duvelisib, Histamine, Hydroxycarbamide, Ibandronic Acid, Imiquimod, Imlunestrant, Linvoseltamab, Melphalan Flufenamide, Mifamurtide, Moxetumomab Pasudotox, Nogapendekin Alfa Inbakicept, Obecabtagene Autoleucel, Odronextamab, Padeliporfin, Pixantrone Dimaleate, Radium Ra223 dichloride, Serplulimab, Sugemalimab, Tagraxofusp, Talimogene Laherparepvec, Temoporfin, Thalidomide, Thyrotropin Alfa, Tisotumab Vedotin, Tivozanib, Toremifene, Vimseltinib, Vinflunine, two cell therapies kept under their EMA common name (sipuleucel-T "Autologous peripheral-blood mononuclear cells activated with…", Zalmoxis "Allogeneic T cells genetically modified…").
Most-connected cancers by EU text edges: NSCLC 33 drugs, malignant breast neoplasm 25, multiple myeloma 21, RCC 14, ALL 12, colorectal 11, follicular lymphoma 10, prostate 10, AML 9, melanoma 8.
Limitations / notes
- Medicine-level granularity: one row per (medicine, active substance) — biosimilars and
generics each have their own row on the same molecule (trastuzumab: Herceptin + 6 biosimilars).
The approvals feed counts medicines, not molecules;
raw.flags.biosimilar / genericsay which. - Dates:
approval_dateis the initial EU marketing authorisation of that medicine (a generic's date for a generic). The Commission decision date is the latest decision, kept inrawonly. Suspensions carry no date in the file (2026-09-11) →withdrawal_datenull. - Refusals / pending opinions are not approvals: no row, no edge; they are in the lake and
counted in the run log (
refused,notAuthorised,pending) and their unknown INNs are queued (not_authorised_medicine). Positive CHMP opinions awaiting the Commission decision (Opinion, 5 on 2026-09-11) appear on the next weekly run once authorised. - Cancer mapping is conservative: multi-indication medicines have
cancer_id NULL(edges carry the per-cancer information); a sentence naming ≥ 2 unrelated cancers yields none. MeSH-derived edges target the MeSH heading's entity, often the broad "X Neoplasm" umbrella (malignant = falseumbrella of benign + malignant) — hence the separateevidenceLevel 'mesh_therapeutic_area'. - Gray zone kept, flagged: medicines oncology by MeSH with a therapeutic sentence outside the
cancer itself (Cinacalcet — hypercalcaemia of parathyroid carcinoma + hyperparathyroidism;
Ibandronic acid — one authorised medicine with a non-supportive sentence; Thyrotropin alfa —
radioiodine ablation adjunct) are minted with their honest indication text and MeSH edge only.
Conversely luspatercept (MDS anaemia), imetelstat and epoetins are not minted because every
sentence targets anaemia — they land in
unresolved_labels(non_therapeutic_indication). - Combinations: a component whose INN is unknown is queued (
component_of_combination, 16 on 2026-09-11); the row is linked to the components that resolved (Akeega → Niraparib and Abiraterone Acetate; Phesgo → Pertuzumab and Trastuzumab). - Ambiguous aliases (2): "dacomitinib" and "trastuzumab deruxtecan" each match two drug
entities — a duplicate-entity question for
drug-duplicates.ts, not for this connector. - Veterinary rows (393) are recorded in the lake and skipped.
Species,ATCvetand the veterinary pharmacotherapeutic group are parsed for the lake only. - Finding for
openfda: the sameCancerDictionary.build(rows, disambiguate, reconciler)call with the reconciler collapses shared aliases to the child concept (adult "breast cancer" → Childhood Breast Carcinoma, "diffuse large B-cell lymphoma" → Childhood DLBCL); the EMA connector passesundefinedat build time. Not changed in the openFDA connector by this work. - Fixtures:
fixtures/ema-sample.xlsx(38 real rows — oncology L01/L02, biosimilars, a generic, withdrawn / lapsed / expired / revoked / suspended / refused / application-withdrawn / pending rows, MeSH-only and text-only rows, combinations, vaccines, veterinary rows; one marketing authorisation date corrupted to99/99/2016),ema-header-drift.xlsx(required column renamed),ema-empty.xlsx(header only). Regenerate withpnpm exec tsx packages/connectors/src/connectors/ema/fixtures/build-fixtures.ts /tmp/ema.xlsx.