ClinicalTrials.gov is a public record of every trial being run. A sponsor moving a trial into a new phase, or opening sites in a new country, is a sponsor about to staff it. Trial activity leads hiring, which puts you in front of the role before it exists.
A signal source for pharma, biotech and CRO desks. It names the sponsor; the rest of the pipeline finds the hiring manager.
put a name to the sponsor's hiring side
see whether the trial has turned into job postings yet
direct dials into clinical operations
run the BD sequence off the trial signal
Ask for the outcome and your agent composes the run itself. Or start from a skill, a whole pipeline it already knows end to end.
browse all skills →Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.
Read the ClinicalTrials docs →HOW in lib/clinicaltrials.py. Index: reference/docs/clinicaltrials/raw/endpoints.md.
https://clinicaltrials.gov/api/v2 · No authentication — free public API (no env var needed).pageSize (≤1000) + pageToken → nextPageToken; iter_studies(max_studies=…) auto-walks it. List params (filter.overallStatus, fields, sort) are comma-joined for you. Use fields=RECRUITER_FIELDS to keep responses compact.Search: search_studies / iter_studies (query.cond/term/spons/lead/intr/locn + filter.overallStatus/advanced/ids/geo). Convenience: search_by_sponsor, recruiting_studies, recently_updated(since) (the daily signal pull — AREA[LastUpdatePostDate]RANGE[since,MAX]). Single: get_study(nct_id) (json/csv/fhir.json/ris). Reference: studies_metadata, search_areas, enums, field_values, stats_size, version.
query.* and filter.advanced use Essie expression syntax. Full operator reference cached at
reference/docs/clinicaltrials/raw/search-syntax.md. Essentials:
AND / OR / NOT (NOT is unary). Group with (); phrase with "…". Ex: (head OR neck) AND NOT pain.AREA[Field]value — AREA[Phase]PHASE3, AREA[LeadSponsorClass]INDUSTRY, AREA[InterventionName]aspirin.AREA[StartDate]RANGE[2024-01-01,MAX], AREA[LastUpdatePostDate]RANGE[2026-05-01,MAX] (MIN/MAX open ends; won't match records missing the value).Relaxation = fuzzy (heart disease also matches "heart and lung disease"). For precise matches use EXPANSION[None] or COVERAGE[FullMatch].SEARCH[Location](…) gotcha: to require a US site that is itself recruiting → SEARCH[Location](AREA[LocationCountry]United States AND AREA[LocationStatus]Recruiting). Without it, the US site and the recruiting site can be different facilities. (Different from study-level filter.overallStatus=RECRUITING.)MISSING, DISTANCE[lat,lon,25mi], ALL, TILT[DateField]. Escape an operator as a term with \ (e.g. \MISSING).Use ENUM values, not labels (#1 cause of 4xx): filter.overallStatus=RECRUITING (not "Recruiting"); AREA[Phase]PHASE3 (not "Phase 3"); LeadSponsorClass ∈ {INDUSTRY, NIH, FED, OTHER_GOV, INDIV, NETWORK, OTHER, …}. Don't guess field names or enum values — discover them from the API: search_areas() (field→param), enums() (valid enum values), studies_metadata() (all field names for fields/sort/AREA[]).
Paging: subsequent pages repeat the SAME params except countTotal/pageSize/pageToken (handled by iter_studies). If a query.* param contains ONLY NCT IDs, filters are ignored. List params are comma/pipe-separated (adapter joins lists). query.* affects ranking; filter.* does not.
"Pharma Hiring Signals," not "trial monitoring." A trial can't start/expand/advance without people, so trial events predict hiring before a job is posted. Detect these events and map to job families:
| Event (detected by diffing snapshots over time) | Likely roles |
|---|---|
| New trial started | CRA, Clinical Trial/Project/Operations Manager, Data Manager |
| Phase I→II | CRA, Clinical Ops, Regulatory Affairs, Data Mgmt (2–10× scale-up) |
| Phase II→III (strongest) | Senior CRA, Clinical Trial Manager, Regulatory, Biostatistician, Medical Monitor |
| NOT_YET_RECRUITING→RECRUITING | hiring happens right before/at this transition |
| +sites / +country | (Senior) CRA, Site/Regional Managers, Regulatory |
| Sponsor trial-count growth / CRO study wins | Clinical Ops, Program Managers, Medical Affairs |
A simple hiring-score (e.g. Phase II→III +50, recruiting +25, +50 sites +40, new country +20) ranks sponsors.
Phase transitions / +sites / status flips are changes over time — they require pulling trials (e.g. recently_updated daily) and diffing against a stored snapshot. That detection + scoring + job-family mapping is a recipe (agent-side, Model A), not a single endpoint. This adapter is the data layer that feeds it.
Signal layer (pharma/biotech): detect trial event → map to job families → enrich the sponsor/CRO decision-makers (waterfall) → qualify vs ICP → outreach/ATS. A far stronger outbound trigger than "saw you hiring on LinkedIn" because it's tied to the underlying business event that created the need.
lib/clinicaltrials.pyImport: from lib.clinicaltrials import ClinicalTrials → instantiate ClinicalTrials() (reads key from env). Base: https://clinicaltrials.gov/api/v2. Generic passthrough: request(method, path, *, params, json).
enums() -> Any — GET /studies/enums — enum types + values (e.g. OverallStatus, Phase).field_values(**params) -> Any — GET /stats/field/values — value stats for leaf fields (types, fields).get_study(nct_id: str, **params) -> Any — GET /studies/{nctId} — single study. format: json|csv|json.zip|fhir.json|ris; fields (json).iter_studies(*, max_studies: int = 1000, **params) -> Iterator[dict] — Auto-paginate /studies via nextPageToken (bounded by max_studies).recently_updated(since: str, *, fields: list[str] | None = None, **params) -> Any — Studies updated on/after since (YYYY-MM-DD) — the daily signal pull. Usesrecruiting_studies(*, condition: str | None = None, sponsor: str | None = None, fields: list[str] | None = None, **params) -> Any — Currently-recruiting studies (filter.overallStatus=RECRUITING) — active hiring demand.search_areas() -> Any — GET /studies/search-areas — searchable areas + their query params.search_by_sponsor(sponsor: str, *, status: list[str] | str | None = None, fields: list[str] | None = None, **params) -> Any — Studies for a sponsor/collaborator (query.spons). Optional overallStatus filter + compact fields.search_studies(**params) -> Any — GET /studies — one page. Params: query.cond/term/locn/titles/intr/outc/spons/lead/id/patientstats_size() -> Any — GET /stats/size — study JSON size statistics.studies_metadata(**params) -> Any — GET /studies/metadata — the data-model field list (includeIndexedOnly, includeHistoricOnly).version() -> Any — GET /version — API + data version (safe read-only pilot).