
Literature Search Openalex
- 1.8k installs
- 2.6k repo stars
- Updated July 7, 2026
- google-deepmind/science-skills
literature-search-openalex is an agent skill for query the openalex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. use when searching aca
About
The literature-search-openalex skill is designed for query the OpenAlex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. Use when searching academic. uv: Read the uv skill and follow its Setup instructions to ensure uv is installed and on PATH. .env file: Make sure the .env file exists in your home directory. Invoke when the user searching academic papers, resolving DOIs, downloading open-access PDFs, finding an author's publications, aggregating bibliometric data (citation counts, h-index, impact factor), exploring the research taxonomies, or performing DOI lookups.
- With key: ~10 req/s, $1/day free budget.
- Without key: Very limited, $0.01/day budget.
- --search <query>: Full-text search (10× cost of --filter).
- --filter <expr>: Filter expressions. Use , for AND and | for OR.
- --sort <field:dir>: Sort results (e.g., cited_by_count:desc).
Literature Search Openalex by the numbers
- 1,774 all-time installs (skills.sh)
- +206 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #211 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
literature-search-openalex capabilities & compatibility
- Capabilities
- with key: ~10 req/s, $1/day free budget · without key: very limited, $0.01/day budget · search <query>: full text search (10× cost of · filter <expr>: filter expressions. use , for a
- Use cases
- frontend
What literature-search-openalex says it does
Query the OpenAlex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. Use when searching academic pap
Query the OpenAlex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. Use when se
npx skills add https://github.com/google-deepmind/science-skills --skill literature-search-openalexAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 2.6k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 7, 2026 |
| Repository | google-deepmind/science-skills ↗ |
How do I query the openalex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. use when searching academic?
Query the OpenAlex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. Use when searching academic.
Who is it for?
Developers using literature search openalex workflows documented in SKILL.md.
Skip if: Skip when the task falls outside literature-search-openalex scope or needs a different stack.
When should I use this skill?
User searching academic papers, resolving DOIs, downloading open-access PDFs, finding an author's publications, aggregating bibliometric data (citation counts, h-index, impact factor), exploring the research taxonomies,
What you get
Completed literature-search-openalex workflow with documented commands, files, and expected deliverables.
- Academic work listings
- Author records with citation metadata
By the numbers
- Author reference documents sort, group_by, and filter support across OpenAlex metadata fields
- Exposes cited_by_count and has_orcid as filterable author attributes
Files
OpenAlex Skill
Prerequisites
1. `uv`: Read the uv skill and follow its Setup instructions to ensure uv is installed and on PATH. 2. User Notification: If LICENSE_NOTIFICATION.txt does not already exist in this skill directory then (1) prominently notify the user to check the terms at https://developers.openalex.org/ and to always check the license of the papers retrieved by the skill for any restrictions, then (2) create the file recording the notification text and timestamp. 3. `.env` file: Make sure the .env file exists in your home directory. Create one if it does not exist. 4. `OPENALEX_API_KEY` (optional but recommended): Enables the OpenAlex Premium API with higher rate limits. The skill works without it (using the free "polite pool"). If the variable is missing from .env, do NOT ask the user to paste it into the chat (this would leak the key into the agent's context). Instead, give the user this command — substituting `ENV_FILE` with the resolved literal path to the `.env` file:
printf "Enter OpenAlex API key (typing hidden): " && read -s key && echo && echo "OPENALEX_API_KEY=$key" >> "ENV_FILE" && echo "Saved."The scripts load credentials automatically via dotenv. NEVER read, print, or inspect the .env file or its variables (e.g. no cat, grep, echo, printenv, or os.environ.get on keys). Credentials must stay out of the agent's context. See the Rate Limits section for more details.
Core Rules
1. List Sources. If this skill is used, ensure this is mentioned in the output AND list the URLs of all papers that were used in producing the output. 2. Resolve before filter. NEVER filter by name. Always resolve a name to an ID first, then use that ID in --filter. 3. Use the CLI only. Never call the API via curl/urllib. The CLI handles retries and rate limiting. 4. No fabrication. Never invent OpenAlex IDs or DOIs. Use resolve/get to look them up. Report empty results accurately. 5. API key. If a command returns 401/429 or you need high-volume queries, follow the prerequisite instructions above to help the user add OPENALEX_API_KEY to the .env file. Keys are at OpenAlex.org → account settings. 6. Keep output small. Always use --select and --per-page 5–10 for overview queries. Pipe filter output to a file (> results.json), then slim with jq before reading into context.
Rate Limits
- With key: ~10 req/s, $1/day free budget.
- Without key: Very limited, $0.01/day budget.
| Operation | Cost |
|---|---|
Singleton get | Free |
filter | $0.0001 |
--search / resolve | $0.001 |
download-pdf | $0.01 |
CLI Reference
uv run scripts/openalex_cli.py [--api-key KEY] <command> [flags]Entity types (shared across commands): works, authors, sources, institutions, topics, domains, fields, subfields, sdgs, countries, continents, languages, keywords, publishers, funders, work-types, source-types, institution-types, licenses
Commands
resolve <entity> <query> — Name → ID candidates. Returns id, display_name, hint. Use --per-page N for more candidates.
get <entity> <id> — Full metadata for one entity. Accepts short ID (W2741809807), full URL, or DOI URL. Use --select to limit fields.
filter <entity> — Search/filter entities. Key flags are:
-
--search <query>: Full-text search (10× cost of--filter) -
--filter <expr>: Filter expressions. Use,for AND and|for OR. -
--sort <field:dir>: Sort results (e.g.,cited_by_count:desc) -
--select <fields>: Limit the fields returned in the output. -
--group-by <field>: Aggregate results by a specific field. -
--per-page <N>: Number of results per page (default 25, max 100). -
--page <N>: Specify the page number to retrieve. -
--sample <N>: Get a random sample of up to 10,000 results. -
--seed <N>: Seed for reproducible sampling.
download-pdf <work-id> <output-path> — Download PDF (requires API key). Falls back to alternative pdf_url locations if primary fails. Whenever you download a PDF, verify it is not empty or corrupted.
rate-limit — Check current rate limit status (requires API key).
Search Tips
- If
resolvereturns no matches, try alternate spellings or abbreviations. - If
--searchreturns 0 results, try broader terms (max 3 retries). - If
resolvereturns multiple candidates, present them to the user with
display_name and hint for manual selection.
Entity References
Consult references/ for valid filter, sort, and group-by fields per entity:
- Works — Authors —
Sources
- Institutions — Topics
— Taxonomy
- Geo & Language —
Publishers & Funders
- Type Values
Common Workflows
# Author's works (resolve → filter)
uv run scripts/openalex_cli.py resolve authors "Geoffrey Hinton"
uv run scripts/openalex_cli.py filter works \
--filter "authorships.author.id:A5108093963" \
--sort "cited_by_count:desc" --per-page 10 > papers.json
cat papers.json | jq '[.results[] | {id, title: .display_name, year: .publication_year, citations: .cited_by_count}]'
# DOI lookup
uv run scripts/openalex_cli.py get works "https://doi.org/10.1038/s41586-021-03819-2"
# Bulk DOI lookup (up to 100)
uv run scripts/openalex_cli.py filter works \
--filter "doi:10.1234/a|10.1234/b|10.1234/c" --per-page 100 > results.json
# Institutional impact by year
uv run scripts/openalex_cli.py resolve institutions "MIT"
uv run scripts/openalex_cli.py filter works \
--filter "authorships.institutions.id:I63966007" \
--group-by "publication_year" > mit_by_year.json
# Random sample
uv run scripts/openalex_cli.py filter works \
--filter "publication_year:2023,is_oa:true" \
--sample 100 --seed 42 > results.jsonError Handling
| Code | Meaning | Action |
|---|---|---|
| 401 | Unauthorized | Help user add API key to .env (see prereqs) |
| 403 | Plan upgrade needed | Inform user; see https://openalex.org/pricing |
| 404 | Not found | Verify ID; try resolve first |
| 429 | Rate limited | Wait and retry; suggest adding API key to .env |
Known premium-only filters: from_updated_date, to_updated_date.
Never fabricate results on empty responses — report accurately and suggest alternate search terms.
Authors Reference
Authors are people who create works.
Top-level
| Field | Sort | Group_by | Filter |
|---|---|---|---|
block_key | ✓ | ✓ | ✓ |
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
has_orcid | ✓ | ✓ | ✓ |
id | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
openalex_id | ✓ | ✓ | |
orcid | ✓ | ✓ | ✓ |
scopus | ✓ | ✓ | |
to_created_date | ✓ | ✓ | |
to_updated_date | ✓ | ✓ | |
works_count | ✓ | ✓ | ✓ |
Affiliations
| Field | Sort | Group_by | Filter |
|---|---|---|---|
affiliations.institution.country_code | ✓ | ✓ | ✓ |
affiliations.institution.id | ✓ | ✓ | ✓ |
affiliations.institution.lineage | ✓ | ✓ | ✓ |
affiliations.institution.ror | ✓ | ✓ | ✓ |
affiliations.institution.type | ✓ | ✓ | ✓ |
Ids
| Field | Sort | Group_by | Filter |
|---|---|---|---|
ids.openalex | ✓ | ✓ | ✓ |
Last Known Institutions
| Field | Sort | Group_by | Filter |
|---|---|---|---|
last_known_institutions.continent | ✓ | ✓ | ✓ |
last_known_institutions.country_code | ✓ | ✓ | ✓ |
last_known_institutions.id | ✓ | ✓ | ✓ |
last_known_institutions.is_global_south | ✓ | ✓ | ✓ |
last_known_institutions.lineage | ✓ | ✓ | ✓ |
last_known_institutions.ror | ✓ | ✓ | ✓ |
last_known_institutions.type | ✓ | ✓ | ✓ |
Parsed Longest Name
| Field | Sort | Group_by | Filter |
|---|---|---|---|
parsed_longest_name.first | ✓ | ✓ | ✓ |
parsed_longest_name.last | ✓ | ✓ | ✓ |
parsed_longest_name.middle | ✓ | ✓ | ✓ |
parsed_longest_name.suffix | ✓ | ✓ | ✓ |
Summary Stats
| Field | Sort | Group_by | Filter |
|---|---|---|---|
summary_stats.2yr_mean_citedness | ✓ | ✓ | ✓ |
summary_stats.h_index | ✓ | ✓ | ✓ |
summary_stats.i10_index | ✓ | ✓ | ✓ |
Topic Share
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topic_share.id | ✓ | ✓ |
Topics
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topics.id | ✓ | ✓ | ✓ |
Geo and Language Reference
Geographic (Continents, Countries) and Language entity types for filtering and grouping scholarly output by location or language.
Continents
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
is_global_south | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Countries
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
continent.id | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
is_global_south | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Languages
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Institutions Reference
Institutions are universities and other organizations to which authors claim affiliations.
Top-level
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
continent | ✓ | ✓ | ✓ |
country_code | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
has_ror | ✓ | ✓ | ✓ |
id | ✓ | ✓ | ✓ |
is_global_south | ✓ | ✓ | ✓ |
is_super_system | ✓ | ✓ | ✓ |
lineage | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
openalex_id | ✓ | ✓ | |
ror | ✓ | ✓ | ✓ |
type | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Ids
| Field | Sort | Group_by | Filter |
|---|---|---|---|
ids.openalex | ✓ | ✓ | ✓ |
Repositories
| Field | Sort | Group_by | Filter |
|---|---|---|---|
repositories.host_organization | ✓ | ✓ | ✓ |
repositories.host_organization_lineage | ✓ | ✓ | ✓ |
repositories.id | ✓ | ✓ | ✓ |
Roles
| Field | Sort | Group_by | Filter |
|---|---|---|---|
roles.id | ✓ | ✓ | ✓ |
Summary Stats
| Field | Sort | Group_by | Filter |
|---|---|---|---|
summary_stats.2yr_mean_citedness | ✓ | ✓ | ✓ |
summary_stats.h_index | ✓ | ✓ | ✓ |
summary_stats.i10_index | ✓ | ✓ | ✓ |
Topic Share
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topic_share.id | ✓ | ✓ | ✓ |
Topics
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topics.id | ✓ | ✓ | ✓ |
Publishers and Funders Reference
Publishers are companies that publish journals and other sources. Funders are organizations that fund research.
Publishers
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
country_codes | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
lineage | ✓ | ✓ | ✓ |
location | ✓ | ||
openalex | ✓ | ✓ | ✓ |
roles.id | ✓ | ✓ | ✓ |
summary_stats.2yr_mean_citedness | ✓ | ✓ | ✓ |
summary_stats.h_index | ✓ | ✓ | ✓ |
summary_stats.i10_index | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Funders
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
continent | ✓ | ✓ | |
country_code | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
is_global_south | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
roles.id | ✓ | ✓ | ✓ |
summary_stats.2yr_mean_citedness | ✓ | ✓ | ✓ |
summary_stats.h_index | ✓ | ✓ | ✓ |
summary_stats.i10_index | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Sources Reference
Sources are where works are hosted: journals, conferences, preprint repositories, and institutional repositories.
Top-level
| Field | Sort | Group_by | Filter |
|---|---|---|---|
apc_usd | ✓ | ✓ | ✓ |
cited_by_count | ✓ | ✓ | ✓ |
continent | ✓ | ✓ | ✓ |
country_code | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
first_publication_year | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
has_issn | ✓ | ✓ | ✓ |
host_organization | ✓ | ✓ | ✓ |
host_organization_lineage | ✓ | ✓ | ✓ |
is_core | ✓ | ✓ | ✓ |
is_global_south | ✓ | ✓ | ✓ |
is_high_oa_rate | ✓ | ✓ | ✓ |
is_high_oa_rate_since_year | ✓ | ✓ | |
is_in_doaj | ✓ | ✓ | ✓ |
is_in_doaj_since_year | ✓ | ✓ | |
is_in_jstage | ✓ | ✓ | |
is_in_jstage_since_year | ✓ | ||
is_oa | ✓ | ✓ | ✓ |
is_ojs | ✓ | ✓ | ✓ |
issn | ✓ | ✓ | ✓ |
issn_l | ✓ | ✓ | ✓ |
last_publication_year | ✓ | ✓ | |
oa_flip_year | ✓ | ✓ | |
openalex | ✓ | ✓ | ✓ |
openalex_id | ✓ | ✓ | |
to_created_date | ✓ | ✓ | |
to_updated_date | ✓ | ✓ | |
type | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Apc Prices
| Field | Sort | Group_by | Filter |
|---|---|---|---|
apc_prices.currency | ✓ | ✓ | ✓ |
apc_prices.price | ✓ | ✓ |
Host Organization
| Field | Sort | Group_by | Filter |
|---|---|---|---|
host_organization.id | ✓ | ✓ |
Ids
| Field | Sort | Group_by | Filter |
|---|---|---|---|
ids.mag | ✓ | ✓ | |
ids.openalex | ✓ | ✓ | ✓ |
Summary Stats
| Field | Sort | Group_by | Filter |
|---|---|---|---|
summary_stats.2yr_mean_citedness | ✓ | ✓ | ✓ |
summary_stats.h_index | ✓ | ✓ | ✓ |
summary_stats.i10_index | ✓ | ✓ | ✓ |
Topic Share
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topic_share.id | ✓ | ✓ | ✓ |
Topics
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topics.id | ✓ |
Taxonomy Reference
The topic taxonomy has four levels: domain > field > subfield > topic. Domains, Fields, Subfields, and SDGs are separate entity types that can be queried independently.
Domains
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
fields.id | ✓ | ✓ | ✓ |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Fields
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
domain.id | ✓ | ✓ | ✓ |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
subfields.id | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Subfields
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
domain.id | ✓ | ✓ | ✓ |
field.id | ✓ | ✓ | ✓ |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
topics.id | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Sustainable Development Goals (SDGs)
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
display_name.search (deprecated) | ✓ | ||
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Topics Reference
Topics are research areas automatically assigned to works. Topics exist in a four-level hierarchy: domain > field > subfield > topic.
Top-level
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Hierarchy
| Field | Sort | Group_by | Filter |
|---|---|---|---|
domain.id | ✓ | ✓ | ✓ |
field.id | ✓ | ✓ | ✓ |
subfield.id | ✓ | ✓ | ✓ |
Ids
| Field | Sort | Group_by | Filter |
|---|---|---|---|
ids.openalex | ✓ | ✓ | ✓ |
Type Values Reference
Enumeration entity types that classify works, sources, institutions, and licenses. These are queryable as standalone entity types.
Work Types
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
crossref_types | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Source Types
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Institution Types
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Licenses
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Keywords
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_count | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
from_created_date | ✓ | ✓ | |
id | ✓ | ✓ | ✓ |
ids.openalex | ✓ | ✓ | ✓ |
openalex | ✓ | ✓ | ✓ |
works_count | ✓ | ✓ | ✓ |
Works Reference
Works are scholarly documents like journal articles, books, datasets, and theses.
Top-level
| Field | Sort | Group_by | Filter |
|---|---|---|---|
authors_count | ✓ | ✓ | ✓ |
best_open_version | ✓ | ✓ | ✓ |
cited_by | ✓ | ||
cited_by_count | ✓ | ✓ | ✓ |
cites | ✓ | ||
concepts_count | ✓ | ✓ | ✓ |
corresponding_author_ids | ✓ | ✓ | ✓ |
corresponding_institution_ids | ✓ | ✓ | ✓ |
countries_distinct_count | ✓ | ✓ | ✓ |
created_date | ✓ | ✓ | |
datasets | ✓ | ✓ | ✓ |
display_name | ✓ | ✓ | |
doi | ✓ | ✓ | |
doi_starts_with | ✓ | ✓ | ✓ |
from_created_date | ✓ | ✓ | |
from_publication_date | ✓ | ✓ | |
fulltext_origin | ✓ | ✓ | ✓ |
fwci | ✓ | ✓ | |
has_abstract | ✓ | ✓ | |
has_doi | ✓ | ✓ | ✓ |
has_embeddings | ✓ | ||
has_fulltext | ✓ | ✓ | ✓ |
has_oa_accepted_or_published_version | ✓ | ✓ | |
has_oa_submitted_version | ✓ | ✓ | |
has_old_authors | ✓ | ✓ | |
has_orcid | ✓ | ✓ | ✓ |
has_pdf_url | ✓ | ✓ | ✓ |
has_pmcid | ✓ | ✓ | ✓ |
has_pmid | ✓ | ✓ | ✓ |
has_raw_affiliation_strings | ✓ | ✓ | |
has_references | ✓ | ✓ | ✓ |
indexed_in | ✓ | ✓ | ✓ |
institutions_distinct_count | ✓ | ✓ | ✓ |
is_corresponding | ✓ | ✓ | ✓ |
is_oa | ✓ | ✓ | |
is_paratext | ✓ | ✓ | ✓ |
is_retracted | ✓ | ✓ | ✓ |
is_xpac | ✓ | ✓ | ✓ |
journal | ✓ | ✓ | ✓ |
language | ✓ | ✓ | ✓ |
locations_count | ✓ | ✓ | ✓ |
mag | ✓ | ✓ | |
mag_only | ✓ | ✓ | |
oa_status | ✓ | ✓ | |
openalex | ✓ | ✓ | ✓ |
openalex_id | ✓ | ✓ | |
pmcid | ✓ | ✓ | |
pmid | ✓ | ✓ | |
publication_date | ✓ | ✓ | |
publication_year | ✓ | ✓ | ✓ |
raw_affiliation_strings | ✓ | ✓ | ✓ |
referenced_works | ✓ | ✓ | |
referenced_works_count | ✓ | ✓ | ✓ |
related_to | ✓ | ||
repository | ✓ | ✓ | ✓ |
to_created_date | ✓ | ✓ | |
to_publication_date | ✓ | ✓ | |
to_updated_date | ✓ | ✓ | |
topics_count | ✓ | ||
type | ✓ | ✓ | ✓ |
type_crossref | ✓ | ✓ | ✓ |
updated_date | ✓ | ✓ | |
version | ✓ | ✓ | ✓ |
Apc List
| Field | Sort | Group_by | Filter |
|---|---|---|---|
apc_list.currency | ✓ | ✓ | ✓ |
apc_list.provenance | ✓ | ✓ | ✓ |
apc_list.value | ✓ | ✓ | ✓ |
apc_list.value_usd | ✓ | ✓ | ✓ |
Apc Paid
| Field | Sort | Group_by | Filter |
|---|---|---|---|
apc_paid.currency | ✓ | ✓ | ✓ |
apc_paid.provenance | ✓ | ✓ | ✓ |
apc_paid.value | ✓ | ✓ | ✓ |
apc_paid.value_usd | ✓ | ✓ | ✓ |
Author
| Field | Sort | Group_by | Filter |
|---|---|---|---|
author.id | ✓ | ✓ | |
author.orcid | ✓ | ✓ |
Authorships
| Field | Sort | Group_by | Filter |
|---|---|---|---|
authorships.affiliations.institution_ids | ✓ | ✓ | ✓ |
authorships.author.id | ✓ | ✓ | ✓ |
authorships.author.orcid | ✓ | ✓ | ✓ |
authorships.countries | ✓ | ✓ | ✓ |
authorships.institutions.continent | ✓ | ✓ | ✓ |
authorships.institutions.country_code | ✓ | ✓ | ✓ |
authorships.institutions.id | ✓ | ✓ | ✓ |
authorships.institutions.is_global_south | ✓ | ✓ | ✓ |
authorships.institutions.lineage | ✓ | ✓ | ✓ |
authorships.institutions.ror | ✓ | ✓ | ✓ |
authorships.institutions.type | ✓ | ✓ | ✓ |
authorships.is_corresponding | ✓ | ✓ | ✓ |
Awards
| Field | Sort | Group_by | Filter |
|---|---|---|---|
awards.doi | ✓ | ✓ | ✓ |
awards.funder_award_id | ✓ | ✓ | ✓ |
awards.funder_display_name | ✓ | ✓ | ✓ |
awards.funder_id | ✓ | ✓ | ✓ |
awards.id | ✓ | ✓ | ✓ |
Best Oa Location
| Field | Sort | Group_by | Filter |
|---|---|---|---|
best_oa_location.is_accepted | ✓ | ✓ | ✓ |
best_oa_location.is_oa | ✓ | ✓ | ✓ |
best_oa_location.is_published | ✓ | ✓ | ✓ |
best_oa_location.landing_page_url | ✓ | ✓ | ✓ |
best_oa_location.license | ✓ | ✓ | ✓ |
best_oa_location.license_id | ✓ | ✓ | ✓ |
best_oa_location.raw_type | ✓ | ||
best_oa_location.source.host_organization | ✓ | ✓ | ✓ |
best_oa_location.source.host_organization_lineage | ✓ | ✓ | ✓ |
best_oa_location.source.id | ✓ | ✓ | ✓ |
best_oa_location.source.is_in_doaj | ✓ | ✓ | ✓ |
best_oa_location.source.is_oa | ✓ | ✓ | ✓ |
best_oa_location.source.issn | ✓ | ✓ | ✓ |
best_oa_location.source.type | ✓ | ✓ | ✓ |
best_oa_location.version | ✓ | ✓ | ✓ |
Biblio
| Field | Sort | Group_by | Filter |
|---|---|---|---|
biblio.first_page | ✓ | ✓ | |
biblio.issue | ✓ | ✓ | ✓ |
biblio.last_page | ✓ | ✓ | |
biblio.volume | ✓ | ✓ | ✓ |
Citation Normalized Percentile
| Field | Sort | Group_by | Filter |
|---|---|---|---|
citation_normalized_percentile.is_in_top_10_percent | ✓ | ✓ | ✓ |
citation_normalized_percentile.is_in_top_1_percent | ✓ | ✓ | ✓ |
citation_normalized_percentile.value | ✓ | ✓ |
Cited By Percentile Year
| Field | Sort | Group_by | Filter |
|---|---|---|---|
cited_by_percentile_year.max | ✓ | ✓ | ✓ |
cited_by_percentile_year.min | ✓ | ✓ | ✓ |
Funders
| Field | Sort | Group_by | Filter |
|---|---|---|---|
funders.id | ✓ | ✓ | ✓ |
Has Content
| Field | Sort | Group_by | Filter |
|---|---|---|---|
has_content.grobid_xml | ✓ | ✓ | ✓ |
has_content.pdf | ✓ | ✓ | ✓ |
Ids
| Field | Sort | Group_by | Filter |
|---|---|---|---|
ids.mag | ✓ | ✓ | |
ids.openalex | ✓ | ✓ | ✓ |
ids.pmcid | ✓ | ✓ | |
ids.pmid | ✓ | ✓ |
Institution
| Field | Sort | Group_by | Filter |
|---|---|---|---|
institution.id | ✓ | ✓ |
Institution Assertions
| Field | Sort | Group_by | Filter |
|---|---|---|---|
institution_assertions.country_code | ✓ | ✓ | ✓ |
institution_assertions.id | ✓ | ✓ | ✓ |
institution_assertions.lineage | ✓ | ✓ | ✓ |
institution_assertions.ror | ✓ | ✓ | ✓ |
institution_assertions.type | ✓ | ✓ | ✓ |
Institutions
| Field | Sort | Group_by | Filter |
|---|---|---|---|
institutions.continent | ✓ | ✓ | |
institutions.country_code | ✓ | ✓ | ✓ |
institutions.id | ✓ | ✓ | ✓ |
institutions.is_global_south | ✓ | ✓ | ✓ |
institutions.ror | ✓ | ✓ | ✓ |
institutions.type | ✓ | ✓ | ✓ |
Keywords
| Field | Sort | Group_by | Filter |
|---|---|---|---|
keywords.id | ✓ | ✓ | ✓ |
Locations
| Field | Sort | Group_by | Filter |
|---|---|---|---|
locations.is_accepted | ✓ | ✓ | ✓ |
locations.is_oa | ✓ | ✓ | ✓ |
locations.is_published | ✓ | ✓ | ✓ |
locations.landing_page_url | ✓ | ✓ | ✓ |
locations.license | ✓ | ✓ | ✓ |
locations.license_id | ✓ | ✓ | ✓ |
locations.raw_type | ✓ | ||
locations.source.has_issn | ✓ | ✓ | ✓ |
locations.source.host_institution_lineage | ✓ | ✓ | ✓ |
locations.source.host_organization | ✓ | ✓ | ✓ |
locations.source.host_organization_lineage | ✓ | ✓ | ✓ |
locations.source.id | ✓ | ✓ | ✓ |
locations.source.is_core | ✓ | ✓ | ✓ |
locations.source.is_in_doaj | ✓ | ✓ | ✓ |
locations.source.is_oa | ✓ | ✓ | ✓ |
locations.source.issn | ✓ | ✓ | ✓ |
locations.source.publisher_lineage | ✓ | ✓ | ✓ |
locations.source.type | ✓ | ✓ | ✓ |
locations.version | ✓ | ✓ | ✓ |
Open Access
| Field | Sort | Group_by | Filter |
|---|---|---|---|
open_access.any_repository_has_fulltext | ✓ | ✓ | ✓ |
open_access.is_oa | ✓ | ✓ | ✓ |
open_access.oa_status | ✓ | ✓ | ✓ |
Primary Location
| Field | Sort | Group_by | Filter |
|---|---|---|---|
primary_location.is_accepted | ✓ | ✓ | ✓ |
primary_location.is_oa | ✓ | ✓ | ✓ |
primary_location.is_published | ✓ | ✓ | ✓ |
primary_location.landing_page_url | ✓ | ✓ | ✓ |
primary_location.license | ✓ | ✓ | ✓ |
primary_location.license_id | ✓ | ✓ | ✓ |
primary_location.raw_type | ✓ | ||
primary_location.source.has_issn | ✓ | ✓ | ✓ |
primary_location.source.host_institution_lineage | ✓ | ✓ | ✓ |
primary_location.source.host_organization | ✓ | ✓ | ✓ |
primary_location.source.host_organization_lineage | ✓ | ✓ | ✓ |
primary_location.source.id | ✓ | ✓ | ✓ |
primary_location.source.is_core | ✓ | ✓ | ✓ |
primary_location.source.is_in_doaj | ✓ | ✓ | ✓ |
primary_location.source.is_oa | ✓ | ✓ | ✓ |
primary_location.source.issn | ✓ | ✓ | ✓ |
primary_location.source.publisher_lineage | ✓ | ✓ | ✓ |
primary_location.source.type | ✓ | ✓ | ✓ |
primary_location.version | ✓ | ✓ | ✓ |
Primary Topic
| Field | Sort | Group_by | Filter |
|---|---|---|---|
primary_topic.domain.id | ✓ | ✓ | ✓ |
primary_topic.field.id | ✓ | ✓ | ✓ |
primary_topic.id | ✓ | ✓ | ✓ |
primary_topic.subfield.id | ✓ | ✓ | ✓ |
Sustainable Development Goals
| Field | Sort | Group_by | Filter |
|---|---|---|---|
sustainable_development_goals.id | ✓ | ✓ | ✓ |
sustainable_development_goals.score | ✓ | ✓ |
Topics
| Field | Sort | Group_by | Filter |
|---|---|---|---|
topics.domain.id | ✓ | ✓ | ✓ |
topics.field.id | ✓ | ✓ | ✓ |
topics.id | ✓ | ✓ | ✓ |
topics.subfield.id | ✓ | ✓ | ✓ |
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""CLI tool for querying the OpenAlex API.
This script provides robust access to the OpenAlex REST API with automatic
pagination handling, rate limit backoffs, and error checking.
"""
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "scienceskillscommon",
# "python-dotenv",
# ]
# [tool.uv.sources]
# scienceskillscommon = { path = "../../scienceskillscommon" }
# ///
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import sys
from typing import Any, Sequence
import urllib.error
import urllib.parse
import dotenv
from science_skills.skills.scienceskillscommon import http_client
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
BASE_URL = "https://api.openalex.org"
PDF_BASE_URL = "https://content.openalex.org"
_API_CLIENT = http_client.HttpClient(
BASE_URL, qps=10.0, referer_skill="literature-search-openalex"
)
_PDF_CLIENT = http_client.HttpClient(
PDF_BASE_URL, qps=1.0, referer_skill="literature-search-openalex"
)
DEFAULT_PER_PAGE = 25
MAX_PER_PAGE = 100
RESOLVE_PER_PAGE = 5
DEFAULT_TIMEOUT_SECS = 30
PDF_TIMEOUT_SECS = 60
MAX_RETRIES = 5
TRUNCATE_LINE_LIMIT = 500
ENTITY_TYPES = [
"works",
"authors",
"sources",
"institutions",
"topics",
"domains",
"fields",
"subfields",
"sdgs",
"countries",
"continents",
"languages",
"keywords",
"publishers",
"funders",
"awards",
"work-types",
"source-types",
"institution-types",
"licenses",
]
def _build_url_with_api_key(url: str, api_key: str | None) -> str:
"""Appends the ``api_key`` query parameter to *url* if a key is provided.
Callers should keep the original *url* for logging so the key does not
leak into log messages.
Args:
url: The base URL, which may already contain query parameters.
api_key: Optional API key string.
Returns:
The URL with ``api_key`` appended, or the original URL unchanged
if *api_key* is ``None``.
"""
if not api_key:
return url
separator = "&" if "?" in url else "?"
return f"{url}{separator}{urllib.parse.urlencode({'api_key': api_key})}"
def fetch_with_retry(
url: str,
params: dict[str, Any],
api_key: str | None = None,
max_retries: int = MAX_RETRIES,
exit_on_error: bool = True,
return_headers: bool = False,
) -> dict[str, Any] | None:
"""Fetches data from the OpenAlex API with exponential backoff.
Args:
url: The base URL to fetch.
params: URL query parameters.
api_key: Optional API key for authentication.
max_retries: Maximum number of retry attempts.
exit_on_error: If True, exit the process on non-retriable errors.
return_headers: If True, include response headers under a
"_response_headers" key in the returned dict.
Returns:
The JSON-parsed response as a dictionary.
"""
# Build the URL from params, then append the API key separately via the
# shared helper. This keeps the key out of the params dict so it cannot
# be accidentally logged or returned to the caller.
query_string = urllib.parse.urlencode(params, doseq=True)
full_url = f"{url}?{query_string}" if query_string else url
full_url = _build_url_with_api_key(full_url, api_key)
try:
if url.startswith(PDF_BASE_URL):
resp = _PDF_CLIENT.fetch(full_url, max_retries=max_retries)
elif url.startswith(BASE_URL):
resp = _API_CLIENT.fetch(full_url, max_retries=max_retries)
else:
raise ValueError(f"Unsupported URL base: {url}")
data = resp.json()
if return_headers:
data["_response_headers"] = resp.headers
return data
except http_client.HttpError as e:
if e.status_code == 429 and not api_key:
logging.warning(
"Rate limit hit (429 Too Many Requests). You are currently running "
"without an API key, which limits API usage. Please provide an API "
"key via --api-key to increase your limit."
)
if e.status_code == 404:
logging.error(
"HTTP Error 404: Entity not found at %s. Hint: verify the ID is"
" correct, or use the 'resolve' command to look it up by name.",
url,
)
else:
logging.error(
"HTTP Error %s while fetching %s: %s", e.status_code, url, e
)
if exit_on_error:
sys.exit(1)
return None
def print_json(data: Any) -> None:
"""Prints JSON data.
Truncates output if it exceeds TRUNCATE_LINE_LIMIT lines to protect context
when printing to a terminal. If output is redirected to a file, the full data
is printed.
Args:
data: The JSON-serializable Python object to print.
"""
output = json.dumps(data, indent=2)
if not sys.stdout.isatty():
print(output)
return
lines = output.splitlines()
if len(lines) > TRUNCATE_LINE_LIMIT:
remaining = len(lines) - TRUNCATE_LINE_LIMIT
print("\n".join(lines[:TRUNCATE_LINE_LIMIT]))
logging.warning(
"Output truncated. %d additional lines not shown. Redirect"
" output to a file if you need the full data.",
remaining,
)
else:
print(output)
# --- Command Handlers ---
def handle_resolve(args: argparse.Namespace) -> None:
"""Searches for an entity by name and returns candidate IDs.
Args:
args: Parsed command-line arguments containing `entity_type`, `query`,
`per_page`, and optional `api_key`.
"""
# Construct search URL (e.g., https://api.openalex.org/works)
url = f"{BASE_URL}/{args.entity_type}"
params = {
"search": args.query,
"per_page": max(1, min(args.per_page, MAX_PER_PAGE)),
}
data = fetch_with_retry(url, params, api_key=args.api_key)
if data is None:
logging.error("No response received from the API.")
sys.exit(1)
# Extract minimal fields to make disambiguation easier for the user/agent.
results = []
for item in data.get("results", []):
entry = {
"id": item.get("id"),
"display_name": item.get("display_name"),
"hint": item.get("hint") or item.get("works_count"),
}
results.append(entry)
print_json(results)
# Matches standard OpenAlex short IDs like W2741809807, A5023888391, etc.
_OPENALEX_SHORT_ID_RE = re.compile(r"^[A-Za-z]\d+$")
def _is_valid_entity_id(entity_id: str) -> bool:
"""Returns True if *entity_id* looks like a valid OpenAlex entity reference.
Accepted formats:
- Short IDs: W2741809807, A5023888391
- Full OpenAlex URLs: https://openalex.org/W2741809807
- DOIs: https://doi.org/10.xxx
"""
return bool(
_OPENALEX_SHORT_ID_RE.match(entity_id)
or entity_id.startswith("https://openalex.org/")
or entity_id.startswith("https://doi.org/")
)
def handle_get(args: argparse.Namespace) -> None:
"""Retrieves complete details for a single entity by its ID.
Args:
args: Parsed command-line arguments containing `entity_type`, `id`,
optional `select` (fields to return), and optional `api_key`.
"""
if not _is_valid_entity_id(args.id):
logging.error(
"Invalid entity ID %r. Expected a short ID (e.g. W2741809807),"
" a full OpenAlex URL, or a DOI URL.",
args.id,
)
sys.exit(1)
# Construct entity-specific URL (e.g., https://api.openalex.org/works/W123)
url = f"{BASE_URL}/{args.entity_type}/{args.id}"
params = {}
# Add optional 'select' parameter to return only requested fields.
if args.select is not None:
params["select"] = args.select
data = fetch_with_retry(url, params, api_key=args.api_key)
print_json(data)
def _is_valid_pdf(data: bytes) -> bool:
"""Checks whether raw bytes represent a valid PDF by inspecting the header.
PDF files must start with the magic bytes '%PDF-'. This catches common
failure modes where a server returns an HTML error page, paywall redirect,
or other non-PDF content with a 200 status code.
Args:
data: The raw bytes to validate.
Returns:
True if the data starts with the PDF magic bytes, False otherwise.
"""
return data[:5] == b"%PDF-"
def _try_download_url(url: str, output_path: str) -> bool:
"""Attempts to download a PDF from a URL.
Downloads the content and validates that it is actually a PDF by checking
for the %PDF- magic bytes before writing to disk.
Args:
url: The URL to download from.
output_path: The local file path to save to.
Returns:
True if a valid PDF was downloaded, False otherwise.
"""
try:
# Extract the base URL (scheme + netloc) for the HttpClient.
parsed = urllib.parse.urlparse(url)
base = f"{parsed.scheme}://{parsed.netloc}"
client = http_client.HttpClient(base, qps=1.0)
data = client.fetch_bytes(url, timeout=PDF_TIMEOUT_SECS)
if not _is_valid_pdf(data):
logging.warning(
"Downloaded content from %s is not a valid PDF (missing %%PDF-"
" header). Skipping.",
url,
)
return False
with open(output_path, "wb") as f:
f.write(data)
return True
except http_client.HttpError as e:
logging.warning("Failed to download from %s: %s", url, e)
return False
def _try_fallback_locations(
work_id: str, output_path: str, api_key: str | None
) -> bool:
"""Tries to download a PDF from alternative locations in work metadata.
Queries the OpenAlex API for the work's metadata and attempts each location
that has a pdf_url. If no PDF can be downloaded, logs any available landing
pages as manual alternatives.
Args:
work_id: The OpenAlex work ID.
output_path: The local file path to save the PDF to.
api_key: Optional API key for authentication.
Returns:
True if a PDF was successfully downloaded, False otherwise.
"""
logging.info("Checking alternative locations in work metadata...")
work_url = f"{BASE_URL}/works/{work_id}"
work_data = fetch_with_retry(
work_url, {}, api_key=api_key, exit_on_error=False
)
if work_data is None:
logging.error("Failed to fetch work metadata for %s.", work_id)
return False
locations = work_data.get("locations", [])
if not locations and "best_oa_location" in work_data:
locations = [work_data["best_oa_location"]]
for loc in locations:
if loc and loc.get("pdf_url"):
logging.info("Attempting fallback download from: %s", loc["pdf_url"])
if _try_download_url(loc["pdf_url"], output_path):
logging.info("Successfully downloaded PDF from fallback location.")
return True
logging.error("No direct PDF download link found in locations.")
landing_pages = [
loc.get("landing_page_url")
for loc in locations
if loc and loc.get("landing_page_url")
]
if landing_pages:
logging.info("Alternative Landing Pages to check manually:")
for page_url in landing_pages:
logging.info(" - %s", page_url)
return False
def handle_download_pdf(args: argparse.Namespace) -> None:
"""Fetches and saves the open-access PDF for a specific work.
Tries the OpenAlex premium content server first (requires API key), then
falls back to PDF URLs listed in the work's metadata.
Args:
args: Parsed command-line arguments containing `id` (the OpenAlex work
ID), `output` (destination file path), and optional `api_key`.
"""
# OpenAlex serves PDFs via a separate base URL (content.openalex.org)
url = f"{PDF_BASE_URL}/works/{args.id}.pdf"
# Build the authenticated URL in a separate variable so the API key
# does not leak into log messages that reference ``url``.
fetch_url = _build_url_with_api_key(url, args.api_key)
logging.info("Downloading PDF for %s to %s", args.id, args.output)
try:
data = _PDF_CLIENT.fetch_bytes(fetch_url, timeout=PDF_TIMEOUT_SECS)
if not _is_valid_pdf(data):
logging.warning(
"Content server returned non-PDF content (missing %%PDF- header)."
)
else:
with open(args.output, "wb") as f:
f.write(data)
logging.info("Successfully downloaded PDF to %s", args.output)
return
except http_client.HttpError as e:
if e.status_code == 401:
logging.warning(
"Premium content server requires an API key (HTTP 401)."
" Trying fallback locations from work metadata..."
)
elif e.status_code == 404:
logging.warning("PDF not found on OpenAlex content server (404).")
else:
logging.warning("Failed to download PDF from primary server: %s", e)
except (urllib.error.URLError, OSError) as e:
logging.warning("Network error downloading PDF: %s", e)
# Try fallback: download from PDF URLs in the work's metadata.
if _try_fallback_locations(args.id, args.output, args.api_key):
return
logging.error(
"Could not download PDF for %s. If the work requires premium access,"
" provide an API key via --api-key or OPENALEX_API_KEY.",
args.id,
)
sys.exit(1)
def handle_filter(args: argparse.Namespace) -> None:
"""Searches and filters entities based on various criteria.
Args:
args: Parsed arguments defining filters (`search`, `filter`, `sort`,
`group_by`, pagination vars, random `sample`, `seed`, `select`), and
`api_key`.
"""
# Warn about incompatible flag combinations before making the request.
if args.sample is not None and args.sort is not None:
logging.warning(
"--sort is ignored when --sample is used. OpenAlex returns random"
" results regardless of sort order."
)
url = f"{BASE_URL}/{args.entity_type}"
# Populate provided query parameters. Exclude --sort when sampling since
# the API ignores it and including it would be misleading.
optional_params = [
"search",
"filter",
"group_by",
"sample",
"seed",
"select",
]
params = {
k: getattr(args, k)
for k in optional_params
if getattr(args, k) is not None
}
# Only include sort when NOT sampling.
if args.sort is not None and args.sample is None:
params["sort"] = args.sort
# Pagination parameters are only valid if we are NOT grouping or sampling.
# OpenAlex does not allow paging when 'group_by' or 'sample' is requested.
if args.group_by is None and args.sample is None:
params["per_page"] = max(1, min(args.per_page, MAX_PER_PAGE))
params["page"] = args.page
data = fetch_with_retry(url, params, api_key=args.api_key)
print_json(data)
def handle_rate_limit(args: argparse.Namespace) -> None:
"""Checks the current rate limit status.
Args:
args: Parsed command-line arguments containing optional `api_key`.
"""
url = f"{BASE_URL}/works"
# Fetch a single result just to obtain the rate limit HTTP headers.
data = fetch_with_retry(
url, {"per_page": 1}, api_key=args.api_key, return_headers=True
)
if data is None:
logging.error("Could not fetch rate limit information from the API.")
sys.exit(1)
# Read rate limits from standard headers injected by OpenAlex.
headers = {k.lower(): v for k, v in data.pop("_response_headers", {}).items()}
limit_val = headers.get("x-ratelimit-limit")
remaining_val = headers.get("x-ratelimit-remaining")
reset_val = headers.get("x-ratelimit-reset")
limits = {
"x-ratelimit-limit": limit_val,
"x-ratelimit-remaining": remaining_val,
"x-ratelimit-reset": reset_val,
}
if limit_val is None and remaining_val is None:
logging.warning(
"Rate limit headers were not returned by the API. This can happen"
" when using an unauthenticated (polite pool) request. Provide an"
" API key via --api-key or OPENALEX_API_KEY for detailed limits."
)
# Output the captured limits to the user.
print_json({"rate_limits": limits})
def main(argv: Sequence[str]) -> None:
"""Main entry point for the OpenAlex CLI.
Args:
argv: Command line arguments, including the executable name.
"""
dotenv.load_dotenv(os.path.expanduser("~/.env"))
parser = argparse.ArgumentParser(description="OpenAlex API CLI Utility")
# Global arguments applying to all subcommands.
parser.add_argument(
"--api-key",
type=str,
default=os.environ.get("OPENALEX_API_KEY"),
help=(
"Optional API key for higher rate limits. Without it, usage is "
"strictly limited. If you hit rate limits frequently, please provide "
"an API key. Defaults to the OPENALEX_API_KEY environment variable "
"if set. An explicit --api-key flag overrides the environment "
"variable."
),
)
# Organize commands using subparsers for clarity and isolated configurations.
subparsers = parser.add_subparsers(dest="command", required=True)
# Resolve Command
parser_resolve = subparsers.add_parser(
"resolve", help="Resolve an entity name to its OpenAlex ID"
)
parser_resolve.add_argument("entity_type", choices=ENTITY_TYPES)
parser_resolve.add_argument(
"query", help="Name or part of the name to search for"
)
parser_resolve.add_argument(
"--per-page",
type=int,
default=RESOLVE_PER_PAGE,
help=f"Number of candidates to return (default: {RESOLVE_PER_PAGE})",
)
parser_resolve.set_defaults(func=handle_resolve)
# Get Entity Command
parser_get = subparsers.add_parser(
"get", help="Get a single entity by its ID"
)
parser_get.add_argument("entity_type", choices=ENTITY_TYPES)
parser_get.add_argument("id", help="The OpenAlex ID (e.g., W2741809807)")
parser_get.add_argument(
"--select", type=str, help="Limit returned fields (e.g., id,title)"
)
parser_get.set_defaults(func=handle_get)
# Download PDF Command
parser_pdf = subparsers.add_parser(
"download-pdf", help="Download PDF for a work (costs $0.01 per request)"
)
parser_pdf.add_argument("id", help="The OpenAlex Work ID (e.g., W2741809807)")
parser_pdf.add_argument("output", help="Output file path (e.g., paper.pdf)")
parser_pdf.set_defaults(func=handle_download_pdf)
# Filter Command
parser_filter = subparsers.add_parser(
"filter", help="Filter and search entities"
)
parser_filter.add_argument("entity_type", choices=ENTITY_TYPES)
parser_filter.add_argument(
"--search", type=str, help="Full-text search query"
)
parser_filter.add_argument(
"--filter", type=str, help="Filter string (e.g. is_oa:true)"
)
parser_filter.add_argument(
"--sort", type=str, help="Sort string (e.g. cited_by_count:desc)"
)
parser_filter.add_argument(
"--group-by", type=str, help="Group results by a field"
)
parser_filter.add_argument(
"--per-page",
type=int,
default=DEFAULT_PER_PAGE,
help=(
f"Results per page (max {MAX_PER_PAGE}, default: {DEFAULT_PER_PAGE})"
),
)
parser_filter.add_argument("--page", type=int, default=1, help="Page number")
parser_filter.add_argument(
"--sample", type=int, help="Number of random samples to return"
)
parser_filter.add_argument(
"--seed", type=int, help="Seed for random sampling"
)
parser_filter.add_argument(
"--select", type=str, help="Limit returned fields (e.g., id,title)"
)
parser_filter.set_defaults(func=handle_filter)
# Rate Limit Command
parser_rate_limit = subparsers.add_parser(
"rate-limit",
help="Check current rate limit status",
)
parser_rate_limit.set_defaults(func=handle_rate_limit)
# Parse the arguments and dispatch execution to the appropriate handle_*
# function set as the default via .set_defaults(func=...) above.
args = parser.parse_args(argv[1:])
args.func(args)
if __name__ == "__main__":
main(sys.argv)
Related skills
How it compares
Pick literature-search-openalex for structured OpenAlex API queries; use general web search skills for non-academic or blog content.
FAQ
What does literature-search-openalex do?
Query the OpenAlex scholarly database for research papers, authors, institutions, topics, sources, publishers, funders, geo-locations, and keywords. Use when searching academic.
When should I use literature-search-openalex?
User searching academic papers, resolving DOIs, downloading open-access PDFs, finding an author's publications, aggregating bibliometric data (citation counts, h-index, impact factor), exploring the research taxonomies, or performing DOI lookups.
Is literature-search-openalex safe to install?
Review the Security Audits panel on this page before installing in production.