Update Dataset (PR → snapshot → steps → grapher)
SkillDatabases & dataEnd-to-end dataset update workflow with PR creation, snapshot, meadow, garden, and grapher steps. Use when user wants to update a dataset, refresh data, run ETL update, or mentions updating dataset versions.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Update Dataset (PR → snapshot → steps → grapher) skill
What this skill tells your AI
The instructions your AI receives, as published by owid/etl in .claude/skills/update-dataset/SKILL.md and read by ahel’s review.
Use this skill to run a complete dataset update with Claude Code subagents, keep a live progress checklist, and pause for user approval only when something needs attention.
Paired skill — keep in sync.
/review-data-pris the reviewer-side counterpart of this skill: it verifies the outcomes of the author-side steps defined here. Whenever you add, remove, or change a workflow step in this file, check whetherreview-data-pr/SKILL.mdneeds a matching reviewer-side check (and add it in the same commit if so). The reverse also holds — see the mirror note there. The creation-side skills/create-datasetand/create-snapshotbelong to the same family: they point into this file's canonical sections (§5b-bis sanity bounds, §5c harmonization audit, §6b metadata quality, §6c metadata checklist + link verification, §6d scheduled issues, and the snapshot-related Guardrails), so when one of those sections changes, check whether the create skills need a matching edit in the same commit too.The two announcements are owned elsewhere.
/data-updates-commsowns the internal Slack form (step 9) and/data-update-announcementowns the public/latestpost (step 9b). Both steps here are delegation stubs on purpose — put wording, format, and style guidance in those skills, never back into this file.
Inputs
<namespace>/<old_version>/<name>- Get
<new_version>as today's date by runningdate -u +"%Y-%m-%d" - A bare
<short_name>(no namespace/version) is also valid — it's what owid-issues reminder bodies use. Resolve it to<namespace>/<old_version>/<short_name>via the DAG:rg "/<short_name>:?$" dag/ -g "*.yml" | grep -v "^dag/archive"— the:?matters because active entries are YAML keys ending in:(a$-anchored pattern without it only matches dependency lines), and archived entries must never be resolution targets. Take the latest active version; ask the user if the short name is ambiguous across namespaces. Several space-separated short names (/update-dataset <short_name1> <short_name2>) mean a grouped update of related datasets: run the full workflow for each, on one shared branch/PR.
Read the dataset's scheduled-issue body before running etl update (step 6d locates the update-*.yml workflow — peek at it now, not only at 6d): it often carries grouped-update instructions naming companion datasets that must bump together, e.g. a deflator ("US PPI New Office Construction also needs to be updated alongside to adjust for inflation"). Also check the target's garden DAG entry for same-vintage dependency datasets (a data://garden/... dep sharing the old version date is usually a companion). Seed etl update with every companion snapshot so the chains move in one run — updating only the main dataset leaves the derived series (e.g. inflation-adjusted) with a silent NaN tail for the new months.
Optional trailing args:
- branch: The working branch name (defaults to current branch)
Assumptions:
- All artifacts are written to
workbench/<short_name>/. - Persist progress to
workbench/<short_name>/progress.mdand update it after each step. - Persist reusable update facts to
workbench/<short_name>/update-context.ymlas they are discovered. This is the canonical context artifact for the PR description, review handoff, anddata-updates-comms.
Progress checklist (maintain, tick live, and persist to progress.md)
- Parse inputs and resolve: channel, namespace, version, short_name, old_version, branch
- Clean workbench directory: delete
workbench/<short_name>unless continuing existing update - Run ETL update workflow via
etl-updatesubagent (help → dry run → approval → real run) - Add yourself to
dataset.ownersin the new garden.meta.yml(don't reorder; preserve existing names and markers) - Catalog
# NOTE:/# TODO:comments carried over from the old step files intonotes_to_check.md - Detect any
sanity_checksfunctions and their log-control flags; append tonotes_to_check.md - Create or reuse draft PR and work branch
- Update snapshot and compare to previous version; capture summary
- Meadow step: run + fix + diff + summarize
- Garden step: run + fix + diff + summarize
- Surface new indicators: check meadow + garden diffs (and snapshot columns when meadow hardcodes a column subset) for new-version columns absent from the old; warn user + PR body, flag new-but-dropped columns, route rename pairs to the step-7 mapping
- Review
sanity_checksoutput (enable log flag, re-run, scan log, revert flag) — if none found and the garden step does non-trivial logic, recommend adding them; if present but missing value bounds (positive / [0,1] / [0,100] per indicator type), suggest those too (see 5b-bis) - Country harmonization audit: validate
.countries.jsonagainst canonical regions (flag provider regions not yet in the regions dataset →/add-provider-regions), audit.excluded_countries.json, scan garden log for missing/unused/unknown warnings - Region-provider drift: if this dataset's aggregates are in
regions.yml(defined_by: <provider>), check whether the new version changed the provider's region set or country membership; if so, updateregions.ymland re-propagate via/add-provider-regions - Grapher step: run + verify (skip diffs), or explicitly mark N/A
- Re-evaluate each catalogued
# NOTE:/# TODO:against fresh data; delete resolved workarounds + comments together, or record status in PR body - Check metadata: typos, Jinja spacing, style guide compliance
- Verify indicator-metadata coverage,
dataset.update_period_days, snapshot DVCdate_publishedandcitation_fullyear (etl updatecopies both verbatim — bump to the producer's real release date / year, or todate_accessed/ current year if the source doesn't publish one), and that all URLs resolve (HEAD-check) and every#fragmentmatches a real anchor in the target page (anchor pass, see 6c) - Always suggest the optional adversarial data & metadata review (
/adversarial-data-review) — verify metadata claims against the producer's fetched documentation and cross-check values against independent sources. Surfacing this offer to the user is mandatory every run (even when you recommend skipping it); only the run is opt-in — it's heavy (~25–45 web calls), so skip by default and run on user opt-in or visible red flags (see 6c-bis) - Scheduled-issue workflow check (owid-issues): locate the dataset's
update-*.yml(exact / fuzzy / group match), verify cron vs the observed release cadence +update_period_days, filename convention, and that the issue body says to run/update-dataset <short_name>; auto-fix body/title, ask before cron changes or new workflows — commits go straight to owid-issues main (see 6d) - Commit, push, and update PR description
- Run indicator upgrade on staging and persist report
- Run the hardcoded-time-bounds audit (
check-hardcoded-years) after all remaps — numericminTime/maxTime/timelineMin/MaxTime/map.timepins on every surface carrying the new indicators (charts, MDim/explorer views, narrative charts, articletime=links), graded against the new data's latest time; a pin below it means the update is invisible on that surface — propose"latest"fixes with user sign-off (see step 7) - Always suggest the optional empty-entity audit (
check-empty-entities) after all remaps — it sweeps every surface (charts, mapcolumnSlug, MDim views, explorer views, narrative charts, gdoccountry=references). Surfacing this offer to the user is mandatory every run (even when you recommend skipping it); only the run is opt-in — it can consume many tokens on widely-charted datasets, so run on user opt-in or when the remap touched many views; regressions vs production = fix, pre-existing = document (see step 7) - Run the referencing-prose audit (standard) — read the prose of every surface the sweep found (articles, data insights, key-chart blurbs) for quantitative claims this update invalidates; an unbounded claim ("has grown 1,300-fold", "now accounts for X%") goes stale; a time-bounded one ("by late 2025 it had reached $62B") does not on an append-only update, but does if this update revised the period it names. Read the sweep's coverage gaps too — a "nothing stale" verdict covers only the surfaces swept. Hand the recomputed numbers to content — never edit published prose yourself (see step 7)
- Update
update-context.ymlwith published chart count and 1–3 chart views for the public announcement - Render Slack announcement via
data-updates-comms, save to workbench, post@codex reviewas a separate PR comment, and notify user to post it to #data-updates-comms - Draft the public-facing "Data update" post for OWID /latest via
data-update-announcement(Mode A) — two versions, user picks, then the Google Doc in /Data updates, and hand the user the link (not added to the PR). The skill declines when we posted about this data less than six months ago; a declined post is a completed item — record the eligible date, don't override - Address Codex review comments (fix valid ones + resolve all threads)
- Run downstream-dependency check (
rg "<namespace>/<old_version>/<short_name>" dag/ -g "*.yml" | grep -v "^dag/archive"); for each consumer outside the dataset's own chain, decide with the user whether to bump in this PR or document under "Downstream dependencies" for a follow-up PR (see "Downstream dependency check" section below for details) - Run the silent-breakage check whenever downstream consumers were repointed in this PR: confirm the
buildkite/etl-automated-staging-environmentPR check is green (red = a consumer crashed on staging, and the report under-reports until it's fixed;.venv/bin/etlr --modified --continue-on-failureis the optional local equivalent for small fan-outs), then triage the data-diff report — every red "− lost N data point(s)" entry in its Top-changes list and every 🔴-tier dataset (see "Silent-breakage check" section) and run the full-report audit probes (structural / World / raw-country / >30% / wipe-vs-edge per loss) - Ask the user whether to remove the old version; if yes, remove+archive its DAG entries now and relocate the new entries into the old slot, but KEEP the old step files until review sign-off — the consecutive-version review diffs them from disk; deleting the files is the final commit before merge (see "Removing the old version & reordering the DAG") — don't forget this step
- Hand off the QA links to the user (Anomalist + Chart Diff on the staging branch, plus the data-diff report) — this is the final step
Persistence:
- After ticking each item, update
workbench/<short_name>/progress.mdwith the current checklist state and a timestamp. - Append every generalizable lesson to
workbench/<short_name>/lessons.mdthe moment you hit it, not at the end. One entry per lesson: what you expected, what actually happened, and the general rule — plus which skill file should own it. Reconstructing these from memory after a long session loses the specifics that make a lesson usable (the exact column name, the threshold that separated signal from noise, the check that would have caught it earlier), and the ones worth keeping are exactly the ones that cost time mid-run. Fold them into the skills when the user asks, followingfeedback_skill_lessons_writebackconventions: phrase each as a general pattern with the incident as a trailing one-liner, and re-read the target skill first — a long-running branch can be several skill revisions behindmaster, so the section you remember may already have been rewritten by someone else's session.
Checkpoints — when to pause
Default: keep going. Run through the full workflow without stopping unless one of the conditions below is met.
Stop and ask the user when:
- A step fails and the fix is ambiguous (multiple reasonable approaches, or you're unsure of the correct one)
- Data structure changed significantly (columns removed/renamed, large row count drops, schema changes that may affect charts)
- Country harmonization has new unmatched countries that need manual decisions
- The snapshot requires a manual download or credentials you don't have
- Indicator upgrade had imperfect matches (< 100% similarity) that need human review
- Anything that could silently break charts or lose data
Don't stop for:
- Routine assertion count updates (just update them and note in the summary)
- Clean step runs with only row increases
- Expected warnings (SettingWithCopyWarning, known unmapped territories)
- Straightforward filename/version reference updates
When you do stop, present a concise summary of the issue and what options exist.
When the update isn't a drop-in version bump
Some updates carry structural changes that make the standard rename-only flow the wrong tool. Recognise them up front and adjust the workflow.
Triggers — any of these means you're in restructure territory, not a version bump:
short_namechanges (producer rebranded the dataset).- File format/schema changes (wide → long, different file extension with a different column set, new dimensions).
- Policy/indicator set changes substantially (splits, dropped composites, newly added areas).
- Score semantics change (e.g. binary → continuous with subnational coverage).
Workflow adjustments:
- Skip
etl update. The rename-only flow copies the old step files into a new folder — useless when the schema is different. Author the new step chain by hand, using the old version as inspiration but not as a starting copy. - Add the new chain to the DAG before removing the old. Leave both chains active while you build and validate v2; remove the v1 entries only once v2 is on staging and the chart remap is queued or done.
- Decide on naming convention upfront. Ask the user whether to preserve v1 short_names where they map cleanly, or to adopt the source's fresh naming scheme. Fresh naming is cleaner but means the auto-Indicator-Upgrader can't help.
- Hand-curate the v1 → v2 indicator mapping. When short_names change entirely, the auto-upgrader has nothing to match on, but the Indicator Upgrader also matches on
title— so if v2 titles are descriptive (full sentences rather than the bare short_name), you can hand the user a table of v1 title → v2 title pairs and they can drive the chart remap from there. Generate this table from the v1 meta.yml + the v2 grapher catalog. - Before remapping a chart onto a successor indicator, diff the chart's config against the new indicator's shape. Three things silently break on a v1 → v2 remap and none of them fails a build: (a) the chart's pinned
selectedEntityNamesmay not exist in the successor (v1 steps often computed regional aggregates the producer doesn't publish — check the retired step's aggregation method viagit show <archive-marker-sha>:<path>and rebuild the aggregates in garden the same way, e.g. population-weighted means); (b) pinnedyAxismin/max sized to the old index's range can clip the new one; (c) the subtitle/FAUST may describe the old construction ("combines 18 policies") and become factually wrong. When the producer publishes several related successor indices, their codebook usually says which is the primary measure and which is a transparency baseline — cite that framing when picking the chart's indicator. (LGBTI: the v1 rights-index chart pinned World + six continents, an axis max of 13 against a successor peaking at 18.7, and a v1-specific subtitle — all three needed changes.) - Defer the Slack and
/latestannouncements until charts have been remapped. Both posts depend oncharts.published_countandcharts.selected_viewsfrom the v2 chain. Drafting them before the remap gives the wrong count (zero) and no representative views. Tell the user to ping you when the chart remap is done, then run steps 8 / 9 / 9b.
For the long-format with dimensions sub-case specifically (e.g. one row per (country, year, <dim1>, <dim2>)), use the modern OWID pattern:
- Meadow + garden:
tb.format(["country", "year", <dim1>, <dim2>, ...], sort_columns=True). - Aggregations:
paths.regions.add_aggregates(tb, index_columns=[...full key...], regions=REGIONS, aggregations={...}). - Grapher: pass long tables through unchanged; the framework auto-expands them into per-cell variables.
- Metadata: variables are keyed by the long-column name, with
<% if <dim> == "X" and <dim2> == "Y" %>...<% endif %>Jinja blocks insidetitle,description_short,display.name. Grep this repo fortb.format(["country", "year"with more than two index entries to find current reference examples. - Jinja coverage: after building the grapher dataset, verify every active
(dim1, dim2)cell renders a non-empty value — read every column from the built grapher dataset and assertmetadata.titleis non-empty. A dimension combination with no matching<% if %>branch ships an untitled indicator.
Workflow orchestration
-
Initial setup
- Check if
workbench/<short_name>/progress.mdexists to determine if continuing existing update - If starting fresh: delete
workbench/<short_name>directory if it exists - Create fresh
workbench/<short_name>directory for artifacts
- Check if
-
Run ETL update command (etl-update subagent)
- Inputs:
<namespace>/<old_version>/<short_name>plus any required flags - Pick the URI that matches what's actually changing:
- If the source data is changing — new source files, modified extractor, anything that affects the snapshot output — run from the snapshot URI with
--include-usages. This bumps the whole chain (snapshot → meadow → garden → grapher) to the new version together:etl update snapshot://<ns>/<old_v>/<short>.<ext> --include-usages - Foundational / widely-used datasets (e.g.
wb/*/income_groups,regions,population): add--direct-only. Plain--include-usagesfollows usages transitively and would try to version-bump every downstream consumer (income_groups has ~85 across 15 dag files).--direct-onlyrestricts the bump to steps sharing the dataset's ownnamespace/version/short_name, i.e. just its chain. Caveat:--direct-onlyexcludes sibling steps with a different short_name that belong to the same chain (e.g.income_groups_aggregations, which the grapher step also depends on) — pass those as extra seed steps so the grapher doesn't end up mixing a new-version garden with an old-version sibling. Dry-run and confirm the proposed set is exactly the chain before executing:etl update snapshot://<ns>/<old_v>/<short>.<ext> data://garden/<ns>/<old_v>/<sibling> --include-usages --direct-only --dry-run - If only garden logic / metadata is changing and the source data is unchanged, run from the garden URI. This bumps garden and grapher only; snapshot and meadow stay on the old version.
- If the source data is changing — new source files, modified extractor, anything that affects the snapshot output — run from the snapshot URI with
- Either way, run
etl updateonce. Don't call it separately per channel — that leaves stale version references in the DAG (e.g., new garden pointing to old meadow). - Perform help check, dry run, approval, then real execution; capture summary for later PR notes
- After running, always verify the dag file: grep for the old version and confirm all internal references between the new steps point to the new version (e.g., garden depends on new meadow, not old meadow).
etl updatewrites the new entries in the flat DAG form — convert them to the nested (compact) form now, while you're in the file, rather than leaving it until archiving (otherwise the flat block tends to survive the whole update unnoticed). See the example andload_dag()parse-check under "DAG archiving & reordering" step 4.
- Inputs:
1a-bis) Add yourself to dataset.owners in the new garden .meta.yml
You've just become a contributor to this dataset, so add your canonical OWID name to its owners: list. Don't reorder — keep the existing primary first; append yourself at the end. Skip if you're already there.
Your canonical name must match an entry in the schema enum (schemas/dataset-schema.json). Resolve it from git config user.name via etl.owners.resolve_owner; if that returns None, add a mapping in etl/owners.py and a row in the schema enum before continuing.
Edit the YAML in place, preserving comments and the existing # review / # backport / # fasttrack markers on other entries.
1b) Check for outdated practices (check-outdated-practices skill)
- After
etl updatecreates new step files, run the/check-outdated-practicesskill on every new step file — including helper modules thatetl updatedoesn't generate but you copied by hand (e.g.*_omms.py), since those carry legacy patterns too - The skill reads the extension as the source of truth for the full pattern set (the
geo.add_*aggregation/population helpers are flagged, not justgeo.harmonize_countries) — don't rely on a remembered subset - Fix any findings before proceeding — this avoids propagating legacy patterns into new versions
geo.harmonize_countries→paths.regions.harmonize_namesis mechanical and safe.geo.add_regions_to_table→paths.regions.add_aggregateschanges the aggregation core — prove equivalence with a controlled A/B test, not a diff against the old feather. Build the new garden both ways against the same current catalog (swap the call, rebuild, save output; revert, rebuild, save output) and diff the two. Do NOT conclude "the helper shifts aggregates across all years" from a new-vs-old-feather diff — that conflates the helper with upstream-dataset drift (see step 5). In practice the two helpers are equivalent bar tiny historical edge cases (e.g. one region-year's population residual); if so, modernize.add_aggregatesalso auto-resolves income groups from the DAG, so it's the right tool when you later need WB income-group aggregates.
1c) Catalog # NOTE: / # TODO: comments in the copied step files (don't resolve yet)
- Run
rg -n "#\s*(NOTE|TODO|FIXME|HACK|XXX):" snapshots/<namespace>/<new_version>/ etl/steps/data/{meadow,garden,grapher}/<namespace>/<new_version>/. - Filter out generic boilerplate (e.g.
# NOTE: To learn more about the fields, hover over their names.at the top of.meta.yml). - Save the remaining actionable items to
workbench/<short_name>/notes_to_check.md— one entry per annotation, recording file path, line number, which step it lives in (meadow/garden/grapher), and what the workaround does. - Don't act on them yet. Resolution requires fresh data and happens after each step's run — see step 6a.
1d) Detect sanity-check logic in the copied step files Sanity checks live in two different forms — detect both:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 156
- Forks
- 30
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
update-dataset- Source
- github.com/owid/etl