Schedule icon
Get icon
If icon
Queries icon
Script icon
Process icon
Set icon
SlackIncomingWebhook icon
Log icon

Track ClinicalTrials.gov Study Status Changes, Results Postings and Completion Slips

Track ClinicalTrials.gov studies weekly, diff Parquet snapshots in DuckDB and alert on terminations, posted results, enrollment changes and completion slips.

Categories
Data

Competitive intelligence, medical affairs and patient-advocacy teams follow dozens to thousands of clinical trials and need to know when one of them stops, posts results or pushes its readout back. ClinicalTrials.gov shows the current state of each record, not what changed. This flow searches the registry through the public API v2 (by condition, intervention, sponsor, plus NCT ids you watch explicitly), keeps a Parquet snapshot of the fields that matter, diffs every run against the previous one in DuckDB and sends one Slack digest with the transitions, ordered by severity. A cumulative change log keeps the history of every transition detected.

How it works

  1. A weekly Schedule (Monday 07:00) runs the check; any input can be changed for an ad-hoc run.
  2. previous_snapshot and previous_changelog (KV Get) read the URIs of the Parquet files written by the last run. On the first run they are missing: bootstrap (If) writes empty files with the right schema and the run is a BASELINE that records the studies without reporting changes.
  3. previous_ids (DuckDB) lists the NCT ids of the previous snapshot.
  4. fetch_studies (Python, standard library only) calls GET /api/v2/studies with query.cond, query.intr, query.spons and the optional filter.overallStatus (skipped when use_search is false), pageSize=1000 and pageToken pagination, and the fields parameter limited to the 15 compared fields (about 0.5 KB per study). The first page uses countTotal=true and the run stops before paginating when the search exceeds max_studies. Watched ids and previously tracked ids that the search no longer returns are then fetched by filter.ids in batches of 100, so a study that left a status filter or had its conditions edited still shows its new status. Ids are validated first (one malformed id makes the API answer HTTP 400 for the whole batch); unknown ids are reported as NOT_FOUND and old ids merged into another record are resolved through NCTIdAlias. Requests are paced at 1.2 seconds with backoff on 429 and 5xx.
  5. diff (DuckDB) normalises month-only completion dates (2027-06) to the last day of the month, joins the new studies to the previous snapshot and classifies changes: STATUS_CHANGE (HIGH when the new status is TERMINATED, WITHDRAWN or SUSPENDED, with the whyStopped reason), RESULTS_POSTED, ENROLLMENT_CHANGE above enrollment_change_pct, COMPLETION_DATE_SLIP and COMPLETION_DATE_PULLED_IN beyond completion_slip_days, PRIMARY_COMPLETION_REACHED, NEW_STUDY (newly registered since the last snapshot, or an existing study that now matches), LEFT_SEARCH and REMOVED. It writes the new snapshot, the appended change log, this run's changes as CSV and a Slack payload built with json_object.
  6. save_snapshot and save_changelog (KV Set) point the keys at the new files.
  7. report_changes (If) sends the digest (SlackIncomingWebhook, allowFailure) and logs it when HIGH or MEDIUM changes exist (INFO too with alert_on_info); otherwise it logs how many studies were updated without a material change. warn_ids lists invalid, unknown and alias ids, and summary logs the counts and status breakdown.

What you get

  • A Slack digest of the trials that stopped, posted results, changed enrollment or slipped their primary completion date, HIGH first, each linked to its study page.
  • changes_csv with one row per change (type, severity, old and new value, reason, last update date, URL) as a flow output, plus the snapshot and the cumulative change log as Parquet.
  • Transitions caught even when the study leaves the search, and typos in the watched ids surfaced as INVALID or NOT_FOUND instead of silently ignored.
  • No false completion slips from month-only dates, and no alert storm on the first run.
  • A hard cap on search size so a broad query fails fast instead of paging the whole registry.

Who it's for

  • Competitive intelligence and business development teams in pharma and biotech following competitor pipelines.
  • Medical affairs, clinical operations and investor-relations analysts tracking readout timelines.
  • Patient-advocacy groups, research librarians and journalists watching trials in one disease area.

Why orchestrate this with Kestra

  • State lives in the KV store and internal storage, so one flow serves many watchlists through snapshot_key, with no database to run.
  • DuckDB inputFiles and outputFiles hand the snapshot, the change log and the CSV between tasks declaratively.
  • If branches handle the baseline and skip the alert when nothing material moved; allowFailure keeps state saved when Slack is down or not configured.
  • Every run keeps its CSV, snapshot and logs for audit, and the schedule replaces a fragile cron script.

Prerequisites

  • Kestra 2.0 with the Python script, JDBC DuckDB and Slack plugins.
  • python3 on the worker: the script uses only the standard library and runs with the Process task runner (switch to the Docker runner and python:3.13-slim if your workers have Docker).
  • Outbound HTTPS to clinicaltrials.gov. The API v2 is public, no key or account needed; stay within about 50 requests per minute.
  • Snapshot files live in the execution that wrote them; if you purge executions, keep the latest successful run of this flow.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook for the digest (optional; without it the notify task ends in WARNING and the digest is still logged).

Quick start

  1. Add SLACK_WEBHOOK_URL as a secret if you want the Slack digest.
  2. Execute with the defaults: every glioblastoma study (2,274 in October 2026, three pages of 1,000) plus two COVID-19 vaccine trials watched by id, in 4 API requests. The first run is the BASELINE and logs the status breakdown (1,033 completed, 323 recruiting, 277 terminated, 96 withdrawn, 418 with results).
  3. Execute again: the snapshot is compared and the run logs how many studies were updated and whether any change is material.
  4. For a pure watchlist, set use_search to false and list NCT ids: malformed ids are logged as INVALID, unknown ones as NOT_FOUND, and merged ids are resolved to the current record.
  5. Point the inputs at your own portfolio (an intervention, a sponsor, or a list of NCT ids) with its own snapshot_key (changing the search under an existing key reports every newly matching study as NEW_STUDY), then enable the schedule.
  6. Download changes_csv from the execution outputs for the full list.

How to extend

  • Load changes_csv or the change log into Postgres, Snowflake or BigQuery for a trial-timeline dashboard.
  • Add an AI task that summarises newly posted results from the full study record (/api/v2/studies/{nctId}).
  • Fan out several watchlists with a ForEach over sponsor names, each with its own snapshot_key.
  • Route HIGH changes to email or Microsoft Teams with the notifications plugin, and post MEDIUM ones as a weekly digest.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.