Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Query ClinicalTrials.gov API v2 for trial data. Search by condition, drug/intervention, location, sponsor, or phase; fetch details by NCT ID; filter by status; paginate; export CSV. For clinical research, patient matching, and trial portfolio analysis.
.claude/skills/jaechang-hits-clinicaltrials-database-search/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-18 | ✗→✓ | ▲ Improved | 244% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 190% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 390% | 0% |
Query the ClinicalTrials.gov API v2 (public, no authentication) to search and retrieve clinical trial data worldwide. Supports searching by condition, intervention, location, sponsor, and status; retrieving detailed study information by NCT ID; paginating large result sets; and exporting to CSV.
bashuv pip install requests pandas
API details:
https://clinicaltrials.gov/api/v2pythonimport requests import time CT_API = "https://clinicaltrials.gov/api/v2" def ct_search(params): """Reusable helper for ClinicalTrials.gov searches.""" response = requests.get(f"{CT_API}/studies", params=params, timeout=30) response.raise_for_status() return response.json() # Search for recruiting breast cancer trials results = ct_search({ "query.cond": "breast cancer", "filter.overallStatus": "RECRUITING", "pageSize": 10, "sort": "LastUpdatePostDate:desc" }) print(f"Found {results['totalCount']} trials") for study in results['studies'][:3]: nct = study['protocolSection']['identificationModule']['nctId'] title = study['protocolSection']['identificationModule']['briefTitle'] print(f" {nct}: {title}")
ClinicalTrials.gov returns deeply nested JSON. Key navigation paths:
| Data | Path | |------|------| | NCT ID | study['protocolSection']['identificationModule']['nctId'] | | Title | study['protocolSection']['identificationModule']['briefTitle'] | | Status | study['protocolSection']['statusModule']['overallStatus'] | | Phase | study['protocolSection']['designModule']['phases'] | | Enrollment | study['protocolSection']['designModule']['enrollmentInfo']['count'] | | Eligibility | study['protocolSection']['eligibilityModule'] | | Locations | study['protocolSection']['contactsLocationsModule']['locations'] | | Interventions | study['protocolSection']['armsInterventionsModule']['interventions'] | | Results | study.get('resultsSection') (None if no results posted) |
| Status | Description | |--------|-------------| | RECRUITING | Currently recruiting participants | | NOT_YET_RECRUITING | Approved but not yet open | | ENROLLING_BY_INVITATION | Invitation-only enrollment | | ACTIVE_NOT_RECRUITING | Active, enrollment closed | | SUSPENDED | Temporarily halted | | TERMINATED | Stopped prematurely | | COMPLETED | Study concluded | | WITHDRAWN | Withdrawn before enrollment |
| Phase | Description | |-------|-------------| | EARLY_PHASE1 | Early Phase 1 (formerly Phase 0) | | PHASE1 | Phase 1 — safety and dosing | | PHASE2 | Phase 2 — efficacy and side effects | | PHASE3 | Phase 3 — large-scale efficacy | | PHASE4 | Phase 4 — post-market surveillance | | NA | Not applicable (non-drug studies) |
| Parameter | Type | Description | Example | |-----------|------|-------------|---------| | query.cond | string | Condition/disease | lung cancer | | query.intr | string | Intervention/drug | Pembrolizumab | | query.locn | string | Geographic location | New York | | query.spons | string | Sponsor name | National Cancer Institute | | query.term | string | General full-text search | immunotherapy | | filter.overallStatus | string | Status filter (comma-separated) | RECRUITING,COMPLETED | | filter.phase | string | Phase filter | PHASE2,PHASE3 | | filter.ids | string | NCT ID filter | NCT04852770 | | sort | string | Sort order | LastUpdatePostDate:desc | | pageSize | int | Results per page (max 1000) | 100 | | pageToken | string | Pagination token | (from previous response) | | format | string | Response format | json or csv |
Sort options: LastUpdatePostDate, EnrollmentCount, StartDate, StudyFirstPostDate — each with :asc or :desc.
pythonresults = ct_search({ "query.cond": "type 2 diabetes", "filter.overallStatus": "RECRUITING", "pageSize": 20, "sort": "LastUpdatePostDate:desc" }) print(f"Found {results['totalCount']} recruiting diabetes trials") for study in results['studies'][:5]: proto = study['protocolSection'] nct = proto['identificationModule']['nctId'] title = proto['identificationModule']['briefTitle'] print(f" {nct}: {title}")
python# Find Phase 3 trials testing Pembrolizumab results = ct_search({ "query.intr": "Pembrolizumab", "filter.overallStatus": "RECRUITING,ACTIVE_NOT_RECRUITING", "filter.phase": "PHASE3", "pageSize": 50 }) print(f"Phase 3 Pembrolizumab trials: {results['totalCount']}")
pythonresults = ct_search({ "query.cond": "cancer", "query.locn": "New York", "filter.overallStatus": "RECRUITING", "pageSize": 20 }) # Extract location details for study in results['studies'][:3]: locs = study['protocolSection'].get('contactsLocationsModule', {}).get('locations', []) for loc in locs: if 'New York' in loc.get('city', ''): print(f" {loc.get('facility')}: {loc['city']}, {loc.get('state', '')}")
pythonresults = ct_search({ "query.spons": "National Cancer Institute", "pageSize": 20 }) for study in results['studies'][:5]: sponsor_mod = study['protocolSection']['sponsorCollaboratorsModule'] lead = sponsor_mod['leadSponsor']['name'] collabs = [c['name'] for c in sponsor_mod.get('collaborators', [])] print(f" Lead: {lead}, Collaborators: {collabs}")
pythonnct_id = "NCT04852770" response = requests.get(f"{CT_API}/studies/{nct_id}", timeout=30) response.raise_for_status() study = response.json() # Extract key information proto = study['protocolSection'] print(f"Title: {proto['identificationModule']['briefTitle']}") print(f"Status: {proto['statusModule']['overallStatus']}") # Eligibility criteria elig = proto.get('eligibilityModule', {}) print(f"Ages: {elig.get('minimumAge')} - {elig.get('maximumAge')}") print(f"Sex: {elig.get('sex')}") print(f"Criteria:\n{elig.get('eligibilityCriteria', 'N/A')[:300]}")
pythonall_studies = [] page_token = None max_pages = 10 for page in range(max_pages): params = { "query.cond": "cancer", "filter.overallStatus": "RECRUITING", "pageSize": 1000, } if page_token: params["pageToken"] = page_token results = ct_search(params) all_studies.extend(results['studies']) page_token = results.get('nextPageToken') if not page_token: break time.sleep(1.5) # respect rate limits print(f"Retrieved {len(all_studies)} studies across {page + 1} pages")
pythonresponse = requests.get(f"{CT_API}/studies", params={ "query.cond": "heart disease", "filter.overallStatus": "RECRUITING", "format": "csv", "pageSize": 1000 }, timeout=60) with open("heart_disease_trials.csv", "w") as f: f.write(response.text) print("Exported to heart_disease_trials.csv")
pythonimport requests, time CT_API = "https://clinicaltrials.gov/api/v2" def ct_search(params): response = requests.get(f"{CT_API}/studies", params=params, timeout=30) response.raise_for_status() return response.json() # Step 1: Search with multiple filters results = ct_search({ "query.cond": "lung cancer", "query.intr": "immunotherapy", "query.locn": "California", "filter.overallStatus": "RECRUITING,NOT_YET_RECRUITING", "pageSize": 100, "sort": "LastUpdatePostDate:desc" }) print(f"Total matches: {results['totalCount']}") # Step 2: Filter by phase phase23 = [ s for s in results['studies'] if any(p in ['PHASE2', 'PHASE3'] for p in s['protocolSection'].get('designModule', {}).get('phases', [])) ] print(f"Phase 2/3 trials: {len(phase23)}") # Step 3: Extract summaries for study in phase23[:5]: proto = study['protocolSection'] nct = proto['identificationModule']['nctId'] title = proto['identificationModule']['briefTitle'] enrollment = proto.get('designModule', {}).get('enrollmentInfo', {}).get('count', 'N/A') print(f" {nct}: {title} (n={enrollment})")
python# Step 1: Find completed trials with posted results results = ct_search({ "query.cond": "alzheimer disease", "filter.overallStatus": "COMPLETED", "pageSize": 100, "sort": "LastUpdatePostDate:desc" }) with_results = [s for s in results['studies'] if s.get('hasResults', False)] print(f"Completed with results: {len(with_results)} / {len(results['studies'])}") # Step 2: Get detailed results for top trial if with_results: nct = with_results[0]['protocolSection']['identificationModule']['nctId'] detail = requests.get(f"{CT_API}/studies/{nct}", timeout=30).json() if 'resultsSection' in detail: outcomes = detail['resultsSection'].get('outcomeMeasuresModule', {}) measures = outcomes.get('outcomeMeasures', []) for m in measures[:3]: print(f" Outcome: {m.get('title')}") print(f" Type: {m.get('type')}")
pythonsponsors = ["Pfizer", "Novartis", "Roche"] for sponsor in sponsors: results = ct_search({ "query.spons": sponsor, "filter.overallStatus": "RECRUITING", "pageSize": 1 }) print(f"{sponsor}: {results['totalCount']} recruiting trials") time.sleep(1.5)
pythondef ct_search_with_retry(params, max_retries=3): for attempt in range(max_retries): try: response = requests.get(f"{CT_API}/studies", params=params, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if e.response.status_code == 429: wait = 60 print(f"Rate limited. Waiting {wait}s...") time.sleep(wait) else: raise except requests.exceptions.RequestException: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) raise Exception("Max retries exceeded")
pythondef extract_summary(study): proto = study.get('protocolSection', {}) ident = proto.get('identificationModule', {}) status = proto.get('statusModule', {}) design = proto.get('designModule', {}) return { 'nct_id': ident.get('nctId'), 'title': ident.get('officialTitle') or ident.get('briefTitle'), 'status': status.get('overallStatus'), 'phases': design.get('phases', []), 'enrollment': design.get('enrollmentInfo', {}).get('count'), 'last_update': status.get('lastUpdatePostDateStruct', {}).get('date') } # Usage for study in results['studies'][:3]: s = extract_summary(study) print(f"{s['nct_id']}: {s['status']} | Phase: {s['phases']} | n={s['enrollment']}")
pythondef safe_get(study, *keys, default='N/A'): """Navigate nested study JSON safely.""" current = study for key in keys: if isinstance(current, dict): current = current.get(key) else: return default if current is None: return default return current # Usage — handles missing fields gracefully nct = safe_get(study, 'protocolSection', 'identificationModule', 'nctId') phases = safe_get(study, 'protocolSection', 'designModule', 'phases', default=[]) enrollment = safe_get(study, 'protocolSection', 'designModule', 'enrollmentInfo', 'count')
| Parameter | Endpoint | Default | Description | |-----------|----------|---------|-------------| | query.cond | search | — | Condition/disease search term | | query.intr | search | — | Intervention/drug search term | | query.locn | search | — | Geographic location filter | | query.spons | search | — | Sponsor/organization filter | | query.term | search | — | General full-text search | | filter.overallStatus | search | all | Comma-separated status values | | filter.phase | search | all | Comma-separated phase values | | pageSize | search | 10 | Results per page (max 1000) | | sort | search | relevance | {field}:{asc\|desc} | | format | both | json | json or csv | | timeout | (client) | 30s | Set in requests call |
| Problem | Cause | Solution | |---------|-------|----------| | 429 Too Many Requests | Rate limit exceeded (~50/min) | Wait 60s; use max pageSize=1000; implement exponential backoff | | Empty studies array | No trials match filters | Broaden search (remove status/phase filters); check spelling | | 400 Bad Request | Invalid parameter value | Verify status/phase values match enumeration exactly (e.g., RECRUITING not recruiting) | | Missing resultsSection | Trial has no posted results | Check study['hasResults'] before accessing results | | KeyError on nested field | Not all trials have all modules | Use .get() with defaults or safe_get helper (see Recipes) | | Pagination stops early | nextPageToken absent | All results retrieved; check totalCount vs collected count | | CSV format differs from JSON | Different field structure | CSV flattens nested structure; use JSON for programmatic access | | Timeout on large exports | CSV with many results | Increase timeout; paginate with pageSize=1000 instead |
hasResults before accessing resultsSection — most trials have no posted results.get() chains — not all trials populate all modules (especially contactsLocationsModule, armsInterventionsModule)RECRUITING,NOT_YET_RECRUITING) — don't make separate requests per statussort=LastUpdatePostDate:desc by default — returns most recently updated trials firstlastUpdatePostDateStruct.date is ISO 8601 string; type field indicates ACTUAL vs ESTIMATEDpubmed-database — Published literature search complementary to trial registry datachembl-database-bioactivity — Compound bioactivity data for drugs under investigationbioservices-multi-database — Alternative database access via unified Python interfaceSelf-contained entry. Original total: 866 lines (SKILL.md 507 + api_reference.md 359). Scripts: 216 lines (query_clinicaltrials.py).
Original file disposition:
SKILL.md (507 lines) → Core API modules 1-7 (condition, intervention, location, sponsor, details, pagination, CSV export). "Core Capabilities" sections 1-10 consolidated: Search by Condition → Module 1, Search by Intervention → Module 2, Geographic Search → Module 3, Search by Sponsor → Module 4, Retrieve Detailed Study → Module 5, Pagination → Module 6, Data Export → Module 7, Combined Query → Workflow 1, Extract Summary → Recipe. "Resources" section stub → removed, content consolidated inline. Per-use-case disposition: Patient Matching → When to Use bullet + Workflow 1; Research Analysis → When to Use + Workflow 2; Drug Tracking → When to Use + Module 2; Geographic Search → Module 3; Sponsor Tracking → Module 4 + Workflow 3; Data Export → Module 7; Trial Monitoring → When to Use bullet; Eligibility Screening → Module 5references/api_reference.md (359 lines) → Fully consolidated inline: endpoint parameters → Key Concepts "Query Parameters Reference" table; status/phase values → Key Concepts tables; response structure → Key Concepts "Response Data Structure" table; HTTP error codes → Troubleshooting table; rate limit guidance → Prerequisites + Best Practices; use cases → duplicated main SKILL.md examples, absorbed into Core API; data standards (ISO 8601, CommonMark) → Prerequisites note. Error handling patterns → Recipes "Rate-Limited Bulk Search"scripts/query_clinicaltrials.py (216 lines) → Helper function pattern: search_studies() → Quick Start ct_search() helper; get_study_details() → Module 5 inline; search_with_all_results() → Module 6 pagination pattern; extract_study_summary() → Recipe "Extract Study Summary". Thin-wrapper shortcut applied — each function was a thin wrapper around requests.get()Retention: ~465 lines / 866 original (excl. scripts) = ~54%.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-18 | fail→pass | 8,640 | 3,700 | -57% | 1 | 1 | 0% | 1,693 | 5,830 | +244% | 0 | 0 | — |
case-01 | pass→pass | 5,834 | 5,693 | -2% | 1 | 1 | 0% | 1,197 | 5,862 | +390% | 0 | 0 | — |
case-02 | pass→pass | 4,411 | 3,329 | -25% | 1 | 1 | 0% | 875 | 5,784 | +561% | 0 | 0 | — |
case-03 | pass→pass | 5,549 | 2,714 | -51% | 1 | 1 | 0% | 1,051 | 5,626 | +435% | 0 | 0 | — |
case-04 | pass→pass | 12,168 | 3,098 | -75% | 1 | 1 | 0% | 2,314 | 5,628 | +143% | 0 | 0 | — |
case-05 | pass→pass | 11,335 | 4,639 | -59% | 1 | 1 | 0% | 2,258 | 6,077 | +169% | 0 | 0 | — |
case-06 | fail→pass | 13,264 | 3,904 | -71% | 1 | 1 | 0% | 2,634 | 5,914 | +125% | 0 | 0 | — |
case-07 | pass→pass | 5,496 | 4,185 | -24% | 1 | 1 | 0% | 1,072 | 5,859 | +447% | 0 | 0 | — |
case-08 | pass→pass | 2,547 | 2,246 | -12% | 1 | 1 | 0% | 513 | 5,554 | +983% | 0 | 0 | — |
case-19 | pass→pass | 11,190 | 3,753 | -66% | 1 | 1 | 0% | 1,838 | 5,823 | +217% | 0 | 0 | — |
case-09 | pass→pass | 5,824 | 4,770 | -18% | 1 | 1 | 0% | 1,192 | 6,077 | +410% | 0 | 0 | — |
case-10 | pass→pass | 5,681 | 3,242 | -43% | 1 | 1 | 0% | 1,184 | 5,742 | +385% | 0 | 0 | — |
case-11 | pass→pass | 8,952 | 16,290 | +82% | 1 | 1 | 0% | 1,751 | 6,280 | +259% | 0 | 0 | — |
case-12 | pass→pass | 9,444 | 4,340 | -54% | 1 | 1 | 0% | 1,694 | 5,957 | +252% | 0 | 0 | — |
case-20 | fail→pass | 13,078 | 13,957 | +7% | 1 | 1 | 0% | 2,727 | 7,909 | +190% | 0 | 0 | — |
case-13 | pass→pass | 5,950 | 2,856 | -52% | 1 | 1 | 0% | 1,129 | 5,658 | +401% | 0 | 0 | — |
case-14 | fail→pass | 15,037 | 5,190 | -65% | 1 | 1 | 0% | 2,858 | 6,138 | +115% | 0 | 0 | — |
case-15 | pass→pass | 3,111 | 2,742 | -12% | 1 | 1 | 0% | 666 | 5,698 | +756% | 0 | 0 | — |
case-16 | pass→pass | 15,275 | 8,210 | -46% | 1 | 1 | 0% | 3,164 | 6,818 | +115% | 0 | 0 | — |
case-17 | pass→pass | 7,233 | 4,397 | -39% | 1 | 1 | 0% | 1,285 | 5,918 | +361% | 0 | 0 | — |
case-21 | pass→pass | 7,178 | 6,095 | -15% | 1 | 1 | 0% | 1,379 | 6,314 | +358% | 0 | 0 | — |
case-22 | pass→pass | 8,391 | 7,382 | -12% | 1 | 1 | 0% | 1,646 | 6,541 | +297% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.