Schedule icon
Request icon
Return icon
Loop icon
Download icon
Script icon
Log icon
Set icon

CKAN Open Data Incremental Sync to Parquet

Daily incremental sync from any CKAN open-data portal: changed datasets only, parallel CSV downloads, DuckDB to Parquet, and a KV cursor that never skips.

Categories
Data

Governments and cities publish thousands of datasets on CKAN portals (Toronto, Canada, the UK, the Humanitarian Data Exchange and many more). Re-downloading a whole portal every day is slow and wasteful, and a script that only grabs "today's" files misses anything changed while it was down. This blueprint keeps a cursor in the Kestra KV store, asks CKAN only for datasets modified after it, downloads their CSV resources in parallel, converts each to Parquet with DuckDB, and moves the cursor forward only when every file succeeded.

How it works

  1. daily (io.kestra.plugin.core.trigger.Schedule) runs at 05:30. It ships disabled: true.
  2. The cursor variable reads ckan_cursor_<feed> from the KV store; on the first run it starts initial_days ago.
  3. search_changed (io.kestra.plugin.core.http.Request) calls package_search with fq=metadata_modified:{cursor TO *] AND <filter_query>, sorted oldest first, at most max_datasets results, with retries.
  4. plan_downloads (io.kestra.plugin.core.debug.Return) uses one jq query to list the CSV resources of those datasets, the starting cursor, and the newest metadata_modified as the next cursor.
  5. each_resource (io.kestra.plugin.core.flow.Loop, two at a time) runs download_csv (io.kestra.plugin.core.http.Download, three attempts, allowFailure so a permanently broken link is logged as DOWNLOAD FAILED (skipped) instead of blocking the feed), then to_parquet (io.kestra.plugin.scripts.python.Script): DuckDB sniffs the CSV, retries leniently for messy files (quoted line breaks, ragged rows) and finally as all-text columns, writes data.parquet and reports rows and columns, which log_resource logs. A resource labelled CSV that is not CSV at all is reported as unreadable with its first bytes, so one bad file does not block the sync.
  6. The loop exposes each file's status and rows as loop outputs; advance_cursor (io.kestra.plugin.core.kv.Set) stores the new cursor once the loop finished, and log_summary totals the files by status (converted, converted_lenient, converted_text, unreadable, download_failed) and the rows written.

What you get

  • Only changed datasets are fetched, oldest first, so a long outage catches up in order and nothing is skipped.
  • One Parquet file per CSV resource, attached to the execution, plus row and column counts per file.
  • Parallel downloads with retries; a permanently broken link is reported and skipped, so one bad file cannot stall the feed.
  • A filter you control (organization, tag, format) and a cap per run.

Who it's for

  • Data engineers and analysts who build on public open data.
  • Civic tech, journalism and research teams tracking a city or government portal.
  • Teams mirroring a CKAN catalog into their own lake or warehouse.

Why orchestrate this with Kestra

A sync script needs state, retries, parallelism and an audit trail. Kestra keeps the cursor in the KV store, retries each HTTP call, fans out the downloads with a concurrency limit, attaches every Parquet file to its execution, and reports each file that could not be fetched or parsed, so the feed keeps moving and every gap is visible.

Prerequisites

  • Network access from the worker to the CKAN portal and its file hosts.
  • A Kestra worker that can run containers (Docker or Kubernetes task runner) and reach PyPI.

Secrets

  • None for public portals. For a private CKAN instance, add an Authorization header with an API token stored as a secret to search_changed and download_csv.

Quick start

  1. Set ckan_url, filter_query and a feed name.
  2. Execute the flow; the first run syncs the last initial_days of changes and stores the cursor.
  3. Check log_resource and download a Parquet file from the to_parquet outputs.
  4. Execute again: only datasets changed since the first run are processed. Then enable daily.

How to extend

  • Load each Parquet file into your warehouse (Snowflake, BigQuery, Postgres) in the loop instead of keeping it on the execution.
  • Add organization: or tags: to filter_query and run one feed per topic.
  • Skip files above a size limit using the resource size field in the jq query.
  • Post the summary to Slack, or trigger a downstream dbt run when new data arrived.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.