--- name: europe-pmc-scraper description: Search Europe PMC's life-science literature index via the Apify Actor arman-bd/europe-pmc-scraper. Returns one record per article with title, authors, journal, abstract, PMID, PMCID, DOI, MeSH terms, funder grant records, cited-by count, every full-text link, and optionally the complete open-access body as clean plain text. Use when a task needs a biomedical literature sweep, a RAG corpus of open-access papers, funder or grant analysis, or systematic-review screening on full text rather than abstracts. Not for clinical trial registries, chemical or gene databases, paywalled article bodies, or citation graphs. --- # Europe PMC Scraper Apify Actor `arman-bd/europe-pmc-scraper`. Give it search terms, get one dataset record per article. Europe PMC indexes everything MEDLINE does and adds preprints, patents, theses, agency reports and the European funder record, so `grantsList` and open-access full text are the two things you get here that a PubMed mirror will not give you. The Actor takes no credentials. ## When to use it - Building a RAG corpus: open-access articles with the body already extracted and cleaned to plain text, plus the metadata that makes a chunk citable. - Grant and funding analysis, searching by agency and getting grant IDs back. - Systematic-review screening where the decision needs the full text, not the abstract. - A broad literature sweep that must go deeper than a thousand results. - Scheduled monitoring of a topic, diffing on `id` between runs. ## When not to use it - Clinical trial registries, gene or chemical databases, or structured datasets. This is bibliographic literature only. - Paywalled article bodies. Full text exists for the open-access subset alone, which is roughly a fifth of the index. - Citation graphs. You get `citedByCount` as a number, not the citing or cited lists. - One known DOI or PMID that a single lookup elsewhere would answer. This Actor earns its keep on result sets. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/europe-pmc-scraper').call({ searchQueries: ['MESH:"Neoplasms" AND MESH:"Immunotherapy"'], resultType: 'core', openAccessOnly: true, includeFullText: true, maxResults: 200, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~europe-pmc-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"searchQueries":["GRANT_AGENCY:\"Wellcome Trust\" AND PUB_YEAR:2025"],"resultType":"core","maxResults":100}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/europe-pmc-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `searchQueries` | string[] | yes | | One search per entry, paginated independently and processed in order. Plain words work; the source's own field syntax works too. Blanks dropped, duplicates removed. An empty list aborts the run. | | `resultType` | string | no | `"core"` | `core` returns the abstract, MeSH terms, grants and full-text links. `lite` returns identifiers and citation basics only, but pages 1000 records at a time instead of 100. Both carry the PMCID. Anything other than `lite` is treated as `core`. | | `openAccessOnly` | boolean | no | `false` | Wraps every query so only open-access records come back. | | `includeFullText` | boolean | no | `false` | For each record with a PMCID that is held in the archive, download the article body and add it as clean plain text. One extra request per article. | | `maxResults` | integer | no | `200` | Total cap across all queries combined, not per query. `0` means no cap. Maximum 1000000. | Query syntax beyond plain words: `TITLE:"gene therapy"`, `AUTH:"Smith J"`, `JOURNAL:"Nature"`, `PUB_YEAR:2025` or `PUB_YEAR:[2023 TO 2026]`, `MESH:"Neoplasms"`, `GRANT_AGENCY:"Wellcome Trust"`, `DOI:"10.1038/…"`, `SRC:PPR` for preprints only, `HAS_FT:y` for records with full text held in the archive. Combine with AND, OR, NOT and parentheses. **The decision that matters is `includeFullText` together with `openAccessOnly`.** Full text exists only for the open-access subset, so turning full text on without the open-access filter means most articles come back with `fullText: null` while you still pay for the row and the extra request. Turn both on together, or neither. The second decision is `resultType`: `lite` is the cheaper way to build a full-text corpus, because it still carries the PMCID and pages ten times faster, and the only things you give up are `abstractText`, `meshTerms` and `grantsList`. ## Output One record per article. | Field | Type | Notes | |---|---|---| | `query` | string | Which of your queries produced this row. Join back on it. | | `id` | string \| null | The source's record ID. Stable, and the right diff key. | | `source` | string \| null | Which index it came from: `MED`, `PMC`, `PPR` for preprints, `PAT` for patents, and others. | | `pmid` | string \| null | String, not a number. | | `pmcid` | string \| null | Present only for articles deposited in the archive, for example `PMC13279208`. `null` means no full text is possible. | | `doi` | string \| null | | | `title` | string \| null | Markup stripped. Chemistry subscripts survive as flat text. | | `authorString` | string \| null | The whole author list as one string, not an array. | | `journalTitle` | string \| null | | | `journalIssn` | string \| null | | | `pubYear` | string \| null | A string, for example `"2026"`. Cast before arithmetic. | | `firstPublicationDate` | string \| null | `YYYY-MM-DD`. | | `abstractText` | string \| null | Plain text, markup stripped, section headings kept as separated words. `null` in `lite` mode and for records that have no abstract. | | `isOpenAccess` | boolean | Converted from the source's `"Y"`/`"N"` strings. | | `inEPMC` | boolean | The body is held in the archive. This plus `pmcid` is what makes a full-text fetch possible. | | `hasPdf` | boolean | | | `citedByCount` | number \| null | A count only. | | `grantsList` | object[] | Funder records, keys `grantId`, `agency`, `acronym`. Empty array in `lite` mode. | | `meshTerms` | string[] | MeSH descriptor names. Empty array in `lite` mode. | | `fullTextUrl` | string \| null | Best available single link: open access preferred, otherwise the first link on record. | | `fullTextUrls` | object[] | Every known link, keys `site`, `url`, `availability`, `documentStyle`. | | `europePmcUrl` | string \| null | Canonical article page. | | `fullText` | string \| null | Only present when `includeFullText` is on. Body prose with references, figures, tables and formulae removed. Capped at 500000 characters. | | `fullTextChars` | number | Only present when `includeFullText` is on. `0` when no text came back. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, long strings trimmed: ```json { "query": "long covid", "id": "42328279", "source": "MED", "pmid": "42328279", "pmcid": "PMC13279208", "doi": "10.1016/j.jhlto.2026.100600", "title": "Long COVID sequelae in heart transplant recipients.", "authorString": "Lees CR, Morad T, Webb M, Sim MS, Hsu JJ.", "journalTitle": "JHLT open", "journalIssn": "2950-1334", "pubYear": "2026", "firstPublicationDate": "2026-05-26", "abstractText": "Post acute sequelae of SARS-CoV-2 infection (also known as Long COVID) have been defined as symptoms that persist after 3 months …", "isOpenAccess": true, "inEPMC": true, "hasPdf": false, "citedByCount": 0, "grantsList": [ { "grantId": "226350/Z/22/Z", "agency": "Wellcome Trust", "acronym": "WT" } ], "meshTerms": ["Humans", "Post-Acute COVID-19 Syndrome", "Heart Transplantation"], "fullTextUrl": "https://europepmc.org/articles/PMC13279208", "fullTextUrls": [ { "site": "DOI", "url": "https://doi.org/10.1016/j.jhlto.2026.100600", "availability": "Subscription required", "documentStyle": "doi" }, { "site": "Europe_PMC", "url": "https://europepmc.org/articles/PMC13279208", "availability": "Open access", "documentStyle": "html" } ], "europePmcUrl": "https://europepmc.org/article/MED/42328279", "fullText": "The COVID-19 pandemic has resulted in nearly 800 million confirmed cases …", "fullTextChars": 20665, "scrapedAt": "2026-08-06T11:48:44.221Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** With full text on it is the only place that tells you the hit rate. ```json { "resultType": "core", "queriesRequested": 1, "queriesFailed": 0, "failures": [], "articlesSaved": 500, "fullTextFetched": 431, "filters": { "searchQueries": ["CRISPR gene editing"], "resultType": "core", "openAccessOnly": true, "includeFullText": true, "maxResults": 500 }, "finishedAt": "2026-08-06T12:04:11.903Z" } ``` Three ways a run can be partial, and all three show here. `queriesFailed` above zero means at least one query broke mid-pagination, with the reason in `failures` as `{ query, error }`; anything already saved for that query stays in the dataset. `articlesSaved` equal to `maxResults` means the cap bound, and because the cap is global and queries run in order, later queries may have contributed nothing at all. `fullTextFetched` well below `articlesSaved` means most records had no open-access body, which is normal rather than an error. ## Behaviour to plan around - **`maxResults` is global and consumed in order.** Three queries with `maxResults: 200` can end with the first query taking all 200 and the other two never running. Give each query its own run, or set the cap to what you want per query times the number of queries. - **Full text is the open-access subset only.** A `null` `fullText` is the normal case, not a failure, and it is never counted as one. `fullTextChars: 0` and `fullTextFetched` in the summary are how you measure the yield. - **`pmcid: null` guarantees no full text.** Subscription-only articles are indexed with a PMID and DOI but no archive copy. Filter on `pmcid` and `inEPMC` before you budget for a full-text run. - **`pubYear` is a string.** So is `pmid`. Cast before sorting or comparing, or 2026 will sort next to 202. - **A rejected query does not look like an HTTP error.** The source answers a bad query with a success status and an error payload. The Actor detects that, records the query in `failures`, and carries on. It only throws when every query failed and nothing was saved. - **Full text is cleaned, not raw.** The body is extracted and references, figures, tables, formulae and supplementary blocks are dropped, because they turn into token soup once markup is gone. Do not expect a reference list or table data in `fullText`. Text is capped at 500000 characters, which no real article reaches. - **Titles and abstracts arrive markup-free.** Block-level tags become a space so a structured abstract's headings do not fuse onto the following sentence, and inline tags are removed outright so a chemical formula stays readable. Entities are decoded twice, because some free-text fields come double-encoded. - **`lite` mode nulls three fields rather than omitting them.** `abstractText` comes back `null`, `meshTerms` and `grantsList` come back empty. Do not read that as the article having no abstract or no funding. - **There is no pagination depth limit.** `maxResults: 0` on a broad query really will try to fetch every hit, and a broad biomedical query can have over a million. Always set a cap you mean. - **Transient errors are retried** three times with linear backoff. A query rejected by the source is final and is not retried. ## Recipes **Open-access corpus for RAG.** Both flags on, `core` for the metadata that makes a chunk citable. ```json { "searchQueries": ["CRISPR gene editing"], "resultType": "core", "openAccessOnly": true, "includeFullText": true, "maxResults": 500 } ``` Chunk `fullText`, and carry `doi`, `title`, `pubYear` and `europePmcUrl` onto every chunk as its citation. Drop rows where `fullTextChars` is `0`. **Funder analysis.** `grantsList` is the field that only this source carries. ```json { "searchQueries": [ "GRANT_AGENCY:\"Wellcome Trust\" AND PUB_YEAR:2025" ], "resultType": "core", "maxResults": 2000 } ``` Explode `grantsList` and group by `grantId` to see what each award produced. One query per run, because the cap is global. **Systematic review screened on full text.** ```json { "searchQueries": ["MESH:\"Neoplasms\" AND MESH:\"Immunotherapy\" AND PUB_YEAR:[2023 TO 2026]"], "resultType": "core", "openAccessOnly": true, "includeFullText": true, "maxResults": 1000 } ``` Screen on `abstractText` first, then run inclusion criteria against `fullText` for the survivors. **Cheap scheduled watch.** Identifiers only, no bodies, a wide net. ```json { "searchQueries": ["long covid"], "resultType": "lite", "maxResults": 5000 } ``` Keep the set of `id` values already seen; anything new, sorted by `firstPublicationDate`, is the feed since the last run.