CtrlK
BlogDocsLog inGet started
Tessl Logo

searching-clinicaltrials

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

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

SKILL.md
Quality
Evals
Security

Searching ClinicalTrials.gov (v2 REST API)

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.

When to use

  • OpenMed extracted a diagnosis ("metastatic colorectal cancer") or a drug ("pembrolizumab") and you want open trials for it.
  • You are building a patient-to-trial matching feature and need candidate studies before applying eligibility logic (parsing-trial-eligibility).
  • You need a corpus of trial records (eligibility text, outcomes) to feed back into 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.

Quick start (real v2 API call)

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"

Response shape

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 pathMeaning
identificationModule.nctIdNCT........ study id
identificationModule.briefTitleshort title
statusModule.overallStatusRECRUITING, COMPLETED, …
conditionsModule.conditionslist of condition strings
armsInterventionsModule.interventionsdrugs / procedures
eligibilityModule.eligibilityCriteriafree-text inclusion/exclusion
eligibilityModule.sex / minimumAge / maximumAgedemographic gates
contactsLocationsModule.locationsrecruiting sites

Cursor pagination

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

Trimming payloads

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")

Workflow

  1. Build the query from OpenMed facts. Map extracted Disease spans → query.cond; Pharmaceutical spans → query.intr. Free-text keywords go in query.term. Combine status filters as filter.overallStatus=RECRUITING,NOT_YET_RECRUITING.
  2. Page through with the cursor until nextPageToken is gone; cap total pulls.
  3. Persist nctId, status, conditions, interventions, and the raw eligibility text. Eligibility goes to parsing-trial-eligibility.
  4. Optionally re-NER the eligibility / outcomes text with openmed.analyze_text to structure inclusion criteria.

Hand-off to / from OpenMed

  • From OpenMed → trial search. 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.
  • Trial text → OpenMed. Feed 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.
  • Keep patient data local. The API call carries only the query terms (condition/drug names), never the patient note or any PHI.

Edge cases & gotchas

  • Synonyms & spelling. The condition matcher is fuzzy but not infinite — "MI" will not match "myocardial infarction". Normalize OpenMed output first (ICD-10 / RxNorm) and consider issuing a few synonym variants.
  • Status enums are exact. Valid values include 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.
  • Page size cap is 1000. Larger values are silently clamped.
  • Rate limits. No key required, but throttle politely (a short sleep between pages); aggressive scraping can be blocked. For bulk/offline work, consider the full registry data dump rather than thousands of paged calls.
  • pageToken expires if the underlying index shifts; restart the query if a token is rejected.
  • Not medical advice. A trial appearing in results does not mean the patient qualifies — eligibility is decided downstream and reviewed by a clinician.

Standards & references

  • ClinicalTrials.gov API v2 — https://clinicaltrials.gov/data-api/api
  • Study data structure (modules / field paths) — https://clinicaltrials.gov/data-api/about-api/study-data-structure
  • Search areas & query syntax — https://clinicaltrials.gov/data-api/about-api/search-areas
  • OpenAPI / interactive reference — https://clinicaltrials.gov/api/v2/
Repository
maziyarpanahi/openmed
Last updated
First committed

Is this your skill?

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.