Query ClinicalTrials.gov — the U.S. registry of clinical studies — for trials
matching a condition, intervention, and recruitment status. This skill uses the
modern v2 REST API (/api/v2/studies), which returns structured JSON and
paginates with an opaque cursor (pageToken), not page numbers.
The v2 API is fully public: no API key, no registration, no license barrier.
The legacy v1/classic API and the older query_term-style endpoints are
deprecated — do not build on them.
parsing-trial-eligibility).openmed.analyze_text for biomedical NER.If you already have an NCT number, fetch the single study directly
(/api/v2/studies/NCT01234567) instead of searching.
Base URL: https://clinicaltrials.gov/api/v2. No auth. JSON by default.
import requests
BASE = "https://clinicaltrials.gov/api/v2"
def search_trials(condition: str, intervention: str | None = None,
status: str = "RECRUITING", page_size: int = 50) -> dict:
"""One page of studies for a condition (+ optional intervention)."""
params = {
"query.cond": condition, # condition / disease search
"filter.overallStatus": status, # comma-separated enum values
"pageSize": min(page_size, 1000), # max 1000; default 10
"countTotal": "true", # include totalCount on first page
"format": "json",
}
if intervention:
params["query.intr"] = intervention # drug / intervention search
r = requests.get(f"{BASE}/studies", params=params, timeout=30)
r.raise_for_status()
return r.json()
data = search_trials("breast cancer", intervention="trastuzumab")
print(data["totalCount"]) # total matches (first page only)
for study in data["studies"]:
ps = study["protocolSection"]
nct = ps["identificationModule"]["nctId"]
title = ps["identificationModule"]["briefTitle"]
print(nct, "-", title)
Equivalent cURL:
curl "https://clinicaltrials.gov/api/v2/studies?query.cond=breast+cancer\
&query.intr=trastuzumab&filter.overallStatus=RECRUITING&pageSize=50&format=json"
Top level: studies (array), nextPageToken (present only if more results),
and totalCount (only when countTotal=true, on the first page). Each study is
a protocolSection of typed modules:
| Field path | Meaning |
|---|---|
identificationModule.nctId |
NCT........ study id |
identificationModule.briefTitle |
short title |
statusModule.overallStatus |
RECRUITING, COMPLETED, … |
conditionsModule.conditions |
list of condition strings |
armsInterventionsModule.interventions |
drugs / procedures |
eligibilityModule.eligibilityCriteria |
free-text inclusion/exclusion |
eligibilityModule.sex / minimumAge / maximumAge |
demographic gates |
contactsLocationsModule.locations |
recruiting sites |
There are no page numbers. Loop until nextPageToken is absent. The token is
opaque — pass it back verbatim. Do not re-send countTotal after page 1.
def iter_all(condition: str, status: str = "RECRUITING"):
params = {"query.cond": condition, "filter.overallStatus": status,
"pageSize": 1000, "format": "json"}
while True:
r = requests.get(f"{BASE}/studies", params=params, timeout=30)
r.raise_for_status()
page = r.json()
yield from page.get("studies", [])
token = page.get("nextPageToken")
if not token:
break
params["pageToken"] = token # cursor for the next page
Default responses are large. Restrict to the fields you need with fields (dotted
paths or module names) to cut bandwidth:
params["fields"] = ("NCTId,BriefTitle,OverallStatus,"
"Condition,EligibilityCriteria")
query.cond; Pharmaceutical spans → query.intr. Free-text keywords go in
query.term. Combine status filters as filter.overallStatus=RECRUITING,NOT_YET_RECRUITING.nextPageToken is gone; cap total pulls.nctId, status, conditions, interventions, and the raw eligibility
text. Eligibility goes to parsing-trial-eligibility.openmed.analyze_text to structure inclusion criteria.openmed.analyze_text(note, model_name="disease_detection_superclinical")
yields Disease and Pharmaceutical entities. Use the surface forms (or a grounded
term from coding-icd10 / normalizing-rxnorm) as query.cond / query.intr.eligibilityModule.eligibilityCriteria and brief
summaries back through openmed.analyze_text to extract conditions, meds, and
labs mentioned in the criteria. Then hand to parsing-trial-eligibility for
inclusion/exclusion matching against patient facts.RECRUITING,
NOT_YET_RECRUITING, ENROLLING_BY_INVITATION, ACTIVE_NOT_RECRUITING,
COMPLETED, SUSPENDED, TERMINATED, WITHDRAWN, UNKNOWN. Comma-separate;
do not lowercase.totalCount is first-page only. Request countTotal=true once; it is not
repeated on subsequent pages.pageToken expires if the underlying index shifts; restart the query if a
token is rejected.