Searches ClinicalTrials.gov for studies by condition, intervention, and recruitment status using the modern v2 REST API with cursor (pageToken) pagination. Use when the user wants to find trials for a diagnosis or drug, screen patients against open studies, build a trial-matching feature, or pull a trial corpus for analysis. Trigger keywords: clinical trial, ClinicalTrials.gov, NCT number, trial search, recruiting studies, eligibility, query.cond, query.intr, pageToken, v2 API. Pairs adjacent to OpenMed: take Disease/Pharmaceutical entities from openmed.analyze_text and turn them into query.cond / query.intr filters; the returned eligibility text feeds parsing-trial-eligibility. ClinicalTrials.gov API v2 is fully public — no API key, no license.
74
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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 pageDefault 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.80da98c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.