Agent skill

Create Explorer

by owid in owid/etl

Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via viz://explorer/<ns/latest/<short).

MITAuto-check passedData & Analytics

Install Create Explorer

skills CLI
$ npx skills add owid/etl --skill create-explorer -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install owid/etl create-explorer --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/create-explorer .claude/skills/create-explorer && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
create-explorer
GitHub stars
158
Token cost
~7.2k tokens
SKILL.md length
2,595 words
Files
1
Skills in repo
35
Repo updated
First seen
Licence
MIT

At a glance

Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via viz://explorer/<ns/latest/<short).

  • Works in 8 steps: Files & directories → Pick a construction style → The Python step → …
  • The user wants to build a new explorer
  • SKILL.md covers When to use this skill, Step 1 — Files & directories, Step 2 — Pick a construction… and Step 3 — The Python step, plus 8 more sections
  • Calls make; reaches assets.ourworldindata.org

What it does

Create Explorer is an agent skill from owid/etl. Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via viz://explorer/<ns/latest/<short). Trigger when the user wants to build a new explorer, add/remove views or dimensions on an existing one, change the explorer's chart text or selection defaults, or finish an explorer migration once the snapshot/garden/grapher chain is already in place.

Its SKILL.md is about 7.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Data & Analytics, covering Data pipelines and ETL. The repository describes itself as: A compute graph for loading and transforming OWID's data. The licence is MIT.

When your agent uses it

  • The user wants to build a new explorer
  • Add/remove views
  • Dimensions on an existing one
  • Change the explorers chart text

Example prompts

  • “/create-explorer”

Requirements

  • Python 3

Workflow steps

8 steps, taken from the step headings in SKILL.md.

  1. Files & directories
  2. Pick a construction style
  3. The Python step
  4. The config YAML
  5. Push FAUST upstream (recommended for single-indicator views)
  6. Post-processing the chart (table-driven only)
  7. DAG entry
  8. Verify

What it can do on your machine

Read from SKILL.md and the folder at commit 69ab20e. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • make

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • assets.ourworldindata.org

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Create Explorer loads about 7.2k tokens when it runs. Until then it costs about 108 tokens; SKILL.md has 2,595 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~108
When it runs · the whole SKILL.md, loaded when a task matches
~7.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from owid/etl at commit 69ab20e, republished under its MIT licence (© owid). 2,595 words, ~7,211 tokens.

Download SKILL.mdSave it as .claude/skills/create-explorer/SKILL.md (or your agent's skills folder).
name
create-explorer
description
Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via `viz://explorer/<ns>/latest/<short>`). Trigger when the user wants to build a new explorer, add/remove views or dimensions on an existing one, change the explorer's chart text or selection defaults, or finish an explorer migration once the snapshot/garden/grapher chain is already in place.
metadata.internal
true
metadata.owner
lucasrodes

Creating an Explorer

Explorers are OWID's multi-dimensional dashboards (e.g. ourworldindata.org/explorers/food-prices). They're authored as YAML in this repo and published by ETL at viz://explorer/<ns>/latest/<short>.

This skill is the explorer-flavored sibling of /create-chart. They use the same engine (paths.create_chart for charts and multidims, paths.create_explorer for explorers) and the same YAML schema for dimensions / views / definitions.common_views. The differences are:

Chart / multidimExplorer
Channelviz://chart/...viz://explorer/...
Step file locationetl/steps/viz/chart/<ns>/latest/etl/steps/viz/explorer/<ns>/latest/
PathFinder methodpaths.create_chart(...)paths.create_explorer(...)
Top-level config blocktitle:, default_selection:, default_dimensions:config: block carrying legacy explorer settings (explorerTitle, explorerSubtitle, selection, subNavId, entityType, …)
Slug conventionunderscores in file paths and short_nameunderscores in file path, hyphens in URL slug and short_name argument
Verificationpreview URL on stagingpreview URL on staging + diff against owid-grapher/explorers/<slug>.explorer.tsv
Save callc.save()c.save(tolerate_extra_indicators=True) (upstream grapher datasets usually have more indicators than the explorer references)

If you're modifying an existing explorer (adjusting chart text, swapping a catalogPath, adding a dimension choice, reordering views), most of the deeper sections below don't apply — find the existing <short>.config.yml, edit, run etlr, done. The full structure is documented below for new explorers and substantial reshapes.

When to use this skill

  • After the /migrate-explorer-to-etl skill has produced (or already located) the upstream snapshot/meadow/garden/grapher chain, and now needs the explorer step.
  • For a brand-new explorer where the data is already in ETL (skip directly to step 1).
  • When porting an existing explorer's view layout (e.g. full-YAML → table-driven, or moving FAUST text from per-view YAML up into indicator metadata).

Step 1 — Files & directories

Every explorer is exactly two files plus a DAG entry:

etl/steps/viz/explorer/<ns>/latest/
├── <short>.py              # Python uses snake_case
└── <short>.config.yml
bash
mkdir -p etl/steps/viz/explorer/<ns>/latest

Hyphens vs underscores (recurring source of confusion):

  • The Python file path uses underscores: food_footprints.py, crop_yields.py.
  • The explorer slug used in the URL and the short_name= argument keeps hyphens: food-footprints, crop-yields.
  • paths.create_explorer(short_name="<short-with-hyphens>") — pass the hyphenated slug.

Step 2 — Pick a construction style

The two ends of the spectrum, plus everything in between:

Full-YAMLProgrammatic / table-driven
Where views liveHand-listed in <short>.config.yml under views:Auto-expanded by paths.create_explorer(tb=tb, ...) from columns whose m.dimensions is set
Where chart text (FAUST) livesPer-view view.config.{title, subtitle, note} in YAMLpresentation.{title_public, grapher_config} on each indicator's garden metadata; common defaults via definitions.common.presentation.grapher_config
Where chart-level config lives (hasMapTab, tab, yAxis, chartTypes)Per-view view.configIndicator's presentation.grapher_config — single source of truth, same as for any standalone chart on that indicator
Map color scaleview.indicators.y[i].display.{colorScaleScheme, colorScaleNumericBins} (semicolon-string form, explorer-flavored override)presentation.grapher_config.map.colorScale.{baseColorScheme, binningStrategy, customNumericValues} (canonical grapher form, inherited at chart render time)
Python step contentTrivial: paths.create_explorer(config=config, short_name=...).save(...)Loops columns to set m.dimensions, optionally post-processes (sort_choices, group_views, per-view display tweaks)

It's a spectrum, not a switch. Mix freely: use table-driven for the bulk of views, hand-list a handful of bespoke ones; or stay full-YAML but still push title/subtitle for the single-indicator views into garden metadata to remove duplication.

Strong fit for table-driven:

  • Single-indicator views dominate. Each view is a thin wrapper around one indicator → that indicator's metadata is the right home for chart text. Avoids duplication between explorer YAML and the equivalent standalone chart, and keeps both in sync forever.
  • Many views (>20) following the cartesian product of a few dimensions. Hand-listing them is repetitive; auto-expansion plus YAML dimensions is significantly less code.
  • Upstream is a dimensional table (one row per country/year × dim_a × dim_b × …) with one indicator. create_explorer(tb=tb, indicator_names=..., dimensions=...) matches this shape directly — model: migration/latest/migration_flows.py.
  • Same indicators back standalone grapher charts. Pushing FAUST upstream means explorer view and standalone chart inherit the same text — no drift over time.

Stick with full-YAML when:

  • Few views (<10), all bespoke (different chart types / data sources / hand-tuned text).
  • Multi-indicator views dominate. FAUST cannot live on any single indicator when a view shows multiple indicators — you have to write it explicitly per view in the explorer YAML (or build views via c.group_views(...)).
  • Single-shot migration with no plan to maintain. The duplication of full-YAML doesn't matter if no one will edit it again.

Step 3 — The Python step

Full-YAML variant
python
"""<one-line description of what this explorer surfaces>."""

from etl.helpers import PathFinder

paths = PathFinder(__file__)


def run() -> None:
    config = paths.load_config()
    c = paths.create_explorer(
        config=config,
        short_name="<short-with-hyphens>",  # explorer slug
    )
    c.save(tolerate_extra_indicators=True)

tolerate_extra_indicators=True is the common case: the upstream grapher dataset usually carries more indicators than the explorer references, and without this flag c.save() errors on the unused ones.

Table-driven variant
python
"""<one-line description>."""

from etl.helpers import PathFinder

paths = PathFinder(__file__)

# Map column → dimension tuple. "na" is the conventional empty slot for conditional dimensions
# (e.g. cost_metric is meaningful only when type=cost; affordability views set cost_metric="na").
COLUMN_DIMENSIONS: dict[str, dict[str, str]] = {
    "<col_a>": {"dim1": "value_a1", "dim2": "value_a2"},
    "<col_b>": {"dim1": "value_b1", "dim2": "value_b2"},
    # ...
}


def run() -> None:
    config = paths.load_config()

    ds = paths.load_dataset("<grapher_dataset>")
    tb = ds.read("<table>", load_data=False)  # metadata only — faster, we don't need values

    for column, dims in COLUMN_DIMENSIONS.items():
        tb[column].m.dimensions = dims
        tb[column].m.original_short_name = "<unifying_indicator_name>"

    c = paths.create_explorer(
        config=config,
        tb=tb,
        indicator_names=["<unifying_indicator_name>"],
        dimensions={
            "dim1": ["value_a1", "value_b1", ...],   # explicit choice order
            "dim2": ["value_a2", "value_b2", ...],
        },
        # common_view_config={...},                   # only if not in indicator metadata
        short_name="<short-with-hyphens>",
    )

    # Optional post-processing — see "Post-processing" below.
    # c.sort_choices({"dim1": lambda x: sorted(x)})
    # c.group_views([...])

    c.save(tolerate_extra_indicators=True)

Key APIs (see etl/viz/chart/core/expand.py and etl/viz/chart/core/create.py):

  • tb[col].m.dimensions: dict[str, str] — required per column. Each entry says "this column represents the (dim1=value, dim2=value) cell." Columns without m.dimensions are ignored by the expander.
  • tb[col].m.original_short_name: str — the unifying indicator name. With indicator_names=[that_name] and a single name, the expander treats all N columns as one logical indicator with N dimension combinations and drops the auto-added "indicator" pseudo-dimension.
  • dimensions= accepts:
    • None → all dimensions found, arbitrary order.
    • list[str] → restricts and orders dimensions, all values shown.
    • dict[str, list[str] | "*"] → restricts and orders both dimensions and choices. Use "*" for "all values, arbitrary order."
  • common_view_config= is applied uniformly to every auto-expanded view. Use it for fields that are truly shared and don't live at indicator level. Prefer indicator-level presentation.grapher_config for anything that should also flow to standalone charts.

Step 4 — The config YAML

Always block style. Mappings and lists in explorer config YAML must use block style — one key per line, list items on their own line under -. Never use flow style ({ key: value, ... } or [a, b, c]) even for tiny per-view dimensions: blocks. PR review on a 45-view file is unreadable when half the views collapse to a single flow line. The only exception is markdown links inside a quoted-scalar subtitle:/note: (those [text](url) brackets are content, not YAML structure).

yaml
config:
  # Explorer settings rows — keys map verbatim from the legacy TSV settings section.
  explorerTitle: ...
  explorerSubtitle: ...
  isPublished: true
  hasMapTab: false
  hideAlertBanner: true
  hideAnnotationFieldsInTitle: true
  entityType: country         # or "food", "region", etc.
  thumbnail: https://assets.ourworldindata.org/uploads/...
  wpBlockId: "12345"
  subNavId: explorers
  subNavCurrentId: <slug>
  selection:
    - <default selected entity>
    - <another>
  pickerColumnSlugs: []        # an empty list is OK; non-empty must be block-style
  yAxisMin: 0
  # ...

definitions:
  # Shared config applied to all views. Use this list (with optional `dimensions:`
  # filter per entry) — NOT YAML anchors and `<<:` merge keys. The framework merges
  # entries at expansion time; per-view `config:` blocks override anything here.
  common_views:
    - config:
        type: DiscreteBar
        hasMapTab: false
    # Dimension-filtered overrides apply only to matching views:
    # - dimensions:
    #     metric: share
    #   config:
    #     note: "Share values sum to 100%"

dimensions:
  # one entry per dropdown / radio / checkbox the user toggles
  - slug: <snake_case>          # e.g. "metric"
    name: <human label>          # e.g. "Metric"
    presentation:
      type: dropdown             # or radio / checkbox
    choices:
      - slug: <choice_snake>
        name: "<as shown in widget>"
      - slug: <another>
        name: "..."

views:
  # one entry per (dim1=x, dim2=y, …) tuple
  - dimensions:
      <dim_slug>: <choice_slug>
      # ...
    indicators:
      y:
        - catalogPath: <table>#<short>      # short form — see "catalogPath — short forms accepted" below
          display:                          # per-view, per-indicator overrides
            colorScaleNumericBins: 0;1;2
            colorScaleScheme: PuBu
    config:
      # Only per-view overrides here. Common stuff lives in definitions.common_views.
      # No `<<:` merge keys, no `&anchor`s.
      title: ...
      subtitle: ...
      type: <chart type>         # LineChart, DiscreteBar, "LineChart DiscreteBar", StackedArea, …
      hasMapTab: false
      minTime: 1990
      yAxisMin: 0

For table-driven explorers, views: should still be present but is typically views: [] — the explorer JSON schema requires the key, and create_explorer(tb=tb, ...) populates the views at runtime.

catalogPath — short forms accepted

The Indicator.is_a_valid_path check (etl/viz/chart/model/view.py:62) accepts three forms; pick the shortest one that still unambiguously resolves:

FormExampleWhen to use
table#indicatorglobal_carbon_budget#emissions_totalDefault. Resolved against the explorer's DAG dependencies via tables_by_name — fine as long as no two dependencies expose a table with the same name.
dataset/table#indicatorglobal_carbon_budget/global_carbon_budget#emissions_totalWhen two upstream datasets happen to expose tables with the same short_name.
grapher/<ns>/<v>/<dataset>/<table>#<indicator>grapher/gcp/2025-11-13/global_carbon_budget/global_carbon_budget#emissions_totalOnly when you need to pin a specific dataset version separate from the one in the DAG — almost never the right form to write by hand.

Short forms are expanded at c.save() time by Indicator.expand_path(tables_by_name). If the table name doesn't exist in any dependency it raises Table name '<x>' not found in dependency tables; if multiple dependencies expose the same table name, it raises and asks you to disambiguate with the medium form.

Default to table#indicator when authoring YAML. The full path is verbose, drifts when upstream versions bump, and is only needed for genuinely ambiguous cases.

Top-level config: settings — the most common keys
KeyTypeNotes
explorerTitlestringPage title above the explorer.
explorerSubtitlestringOne-liner under the title.
isPublishedbooltrue to publish; false keeps it draft.
hasMapTabboolWhether any view shows the map tab by default.
entityTypestringcountry (default), or food, region, species, etc. — controls picker labels.
selectionlist[str]Default selected entities.
pickerColumnSlugslist[str]Picker columns shown alongside the entity name.
subNavIdstringAlmost always explorers.
subNavCurrentIdstringThe slug — appears as the active nav item.
wpBlockIdstringWordPress block ID for embedding (legacy). Stringify even when numeric.
thumbnailstringURL of preview image.
hideAlertBannerboolSuppress the OWID-wide banner.
hideAnnotationFieldsInTitleboolDrop time/entity from auto-titles.
yAxisMinnumber/stringDefault Y-axis floor.
yScaleToggleboolAllow user to toggle linear/log.
originUrlstringPath back to the topic page (e.g. /environmental-impacts-of-food).
Dimension presentation types
  • dropdown: shown as <select>. Use for >4 choices or when the choices have long labels.
  • radio: shown as a row of pills. Use for ≤4 mutually-exclusive choices.
  • checkbox: shown as a single toggle. Two choices only — usually "off" (slug like combined/absolute/no) and "on" (slug like the field name). Pair with presentation.choice_slug_true: <on_slug> so the framework knows which slug means "checked."
yaml
- slug: by_stage
  name: By stage of supply chain
  presentation:
    type: checkbox
    choice_slug_true: stages
  choices:
    - slug: combined
      name: ""
    - slug: stages
      name: By stage of supply chain
Conditional dimensions (the "na" pattern)

When a dimension is only meaningful for some rows (e.g. cost_metric matters only when type=cost, not when type=affordability), include the dimension everywhere with an "na" slot:

  • Tag the columns/views that don't use it with <dim>: "na".
  • Declare the na choice with name: "" so the widget renders as empty when applicable.
  • Model: agriculture/latest/food_prices.{py,config.yml}.
yaml
- slug: cost_metric
  name: Cost metric
  presentation:
    type: radio
  choices:
    - slug: na
      name: ""
    - slug: dollars_per_day
      name: $ per day

For single-indicator views, the rendered chart inherits the indicator's stored grapher_config from MySQL at render time (both standalone-chart and explorer-view paths). Push:

Per-view config→ indicator garden metadata
titlepresentation.grapher_config.title
subtitlepresentation.grapher_config.subtitle
notepresentation.grapher_config.note
map.colorScalepresentation.grapher_config.map.colorScale.{baseColorScheme, binningStrategy, customNumericValues}
hasMapTab, tab, yAxis, chartTypes, hideRelativeToggle, selectedFacetStrategy, …presentation.grapher_config.<field>

For the chart-heading flow specifically, the priority is grapher_config.title > title_public > display.name > title (see docs/architecture/metadata/faqs.md). When migrating a chart-wrapping explorer, the chart's bespoke heading text belongs in grapher_config.title — title_public is the human-readable replacement for a dimensional indicator.title, not the chart's heading.

Cross-cutting baselines (e.g. hasMapTab: true for every indicator in a dataset) go under definitions.common.presentation.grapher_config in the garden .meta.yml. The catalog merge is recursive on presentation and grapher_config (lib/catalog/owid/catalog/core/yaml_metadata.py:_merge_variable_metadata), so each indicator inherits the common defaults plus its own overrides without manual <<: *anchor repetition.

DRY for repeated text fragments via dynamic-yaml interpolation:

yaml
definitions:
  prefix: "Long shared phrase about diet X."
  suffix: "Common closing sentence about methodology."

# In the indicator block:
subtitle: "{definitions.prefix} {definitions.suffix}"

This composes N unique full strings from a handful of building blocks. Verified via dynamic_yaml_to_dict (lib/catalog/owid/catalog/core/utils.py).

Show full SKILL.md (1,117 more words)Show less

Step 6 — Post-processing the chart (table-driven only)

After paths.create_explorer() returns the explorer c, you can mutate it before c.save():

  • c.sort_choices({dim_slug: lambda x: sorted(x)}) — control the order of dimension dropdowns. Useful when slugs sort poorly alphabetically.

  • c.group_views(groups=[...]) — bundle multiple existing views into a new multi-indicator view. Each entry in groups:

    • dimension: the dimension whose choices are being collapsed.
    • choices: the choice slugs to combine (omit for all).
    • choice_new_slug: name for the new collapsed choice (e.g. combined, total, breakdown).
    • view_config: chart-level config for the new view (chartTypes, title, subtitle, selectedFacetStrategy, …). Title can be a template like "Population aged {age}" evaluated against params.
    • view_metadata: data-page metadata (description_key, etc.) for the new view.
    • replace=True to drop the originals; default keeps both.
    • overwrite_dimension_choice=True if choice_new_slug collides with an existing choice and you want grouped views to win.
    • Use cases: an explorer with sex={female, male} views — group_views adds sex=combined showing both timeseries on one chart. Same pattern works for age brackets, region groups, conflict types, or any dimension where users may want a single multi-line chart.
  • c.edit_views([...]) — apply chart-level config to many views at once, optionally scoped by dimension. Each entry is {"dimensions": <filter>, "config": {...}, "metadata": {...}}; the framework merges entries by specificity (more dimensions in the filter = wins on conflicts). Use this in preference to set_global_config whenever you have per-slice overrides:

    python
    c.edit_views([
        # No filter → applies to every view (defaults).
        {"config": {"type": "LineChart DiscreteBar", "hasMapTab": True}},
        # Scoped override — only this exact (gas, accounting, fuel, count) cell gets a Slope tab.
        {
            "dimensions": {"gas": "co2", "accounting": "territorial", "fuel": "all_fossil", "count": "per_capita"},
            "config": {"type": "LineChart SlopeChart DiscreteBar"},
        },
    ])

    Callable values inside config (e.g. "title": lambda v: ...) are evaluated against each matching view, so dimension-aware text templates still work. set_global_config is just a one-entry shortcut for edit_views; reach for edit_views once you have more than one slice to address.

  • Manual loop over c.views — for things edit_views can't reach: per-indicator display blocks (numDecimalPlaces, colorScaleScheme, colorScaleNumericBins, display.color, …). These live on view.indicators.y[i].display, not on view.config. Match views via the view.matches(**kwargs) helper, which accepts a single value or a list (list = OR semantics):

    python
    for view in c.views:
        if view.matches(gas=["methane", "all_ghg"], count="per_capita"):
            decimals = 1
        elif view.matches(gas="warming_impact", fuel=["land_use", "fossil_plus_land_use"]):
            decimals = 3
        else:
            continue
        for indicator in view.indicators.y or []:
            indicator.display = {**(indicator.display or {}), "numDecimalPlaces": decimals}

    Pattern: migration_flows.py's add_display_settings(c). Avoid this loop when the setting can live on the indicator's garden metadata instead.

  • choice_renames={dim: {slug: display_name, ...}} (passed directly to create_explorer) — map slug → display name when you need to derive the display label programmatically. Model: chart/minerals/latest/minerals.py.

  • Sidecar <short>.dims.yaml — when the column → dimensions map exceeds ~50 entries, lift it out of the Python step into a sidecar YAML loaded at module-import time. Keeps <short>.py focused on logic and turns dim-tagging changes into a 1-line YAML edit. Model: etl/steps/viz/explorer/emissions/latest/co2.{py,dims.yaml}:

    python
    from pathlib import Path
    import yaml
    COLUMN_DIMENSIONS = yaml.safe_load((Path(__file__).parent / "co2.dims.yaml").read_text())

Step 7 — DAG entry

In dag/<ns>.yml:

yaml
viz://explorer/<ns>/latest/<short>:
  - data://grapher/<ns1>/<v1>/<dataset1>
  - data://grapher/<ns2>/<v2>/<dataset2>
  # ... one line per unique upstream grapher dataset

Place near related explorer entries (or alongside the upstream grapher steps) for discoverability.

Step 8 — Verify

Hand off to the user:

  1. .venv/bin/etlr viz://explorer/<ns>/latest/<short> --grapher — runs the step and upserts the explorer to the staging DB.
  2. Open http://staging-site-<branch>/admin/explorers/preview/<slug> and spot-check:
    • default view (no dimensions toggled)
    • every dimension switch
    • map tab (if applicable)
    • country/entity picker
    • default selection
  3. For migrations: diff the resulting TSV against the legacy owid-grapher/explorers/<slug>.explorer.tsv. Cosmetic differences (column ordering, whitespace) are acceptable; structural differences (missing views, swapped dimension orderings) are not. The Wizard's apps/wizard/app_pages/explorer_diff/ page does this comparison interactively for staging vs production.
  4. make check.

Hand the user the exact etlr command — don't run it yourself.

Common pitfalls

  • build_views does not propagate per-view display from indicator metadata — see TODO at etl/viz/chart/core/expand.py:313. Each auto-expanded view gets indicators.y[0] with only catalogPath, no display. If you need different color scales per view, either (a) put them in the indicator's presentation.grapher_config.map.colorScale so they apply at chart render time, or (b) post-process c.views in Python.
  • type: LineChart is not a valid grapher_config field when authoring via indicator metadata — use chartTypes: ["LineChart"] (the schema is an array). Per-view config.type in the explorer YAML still accepts strings.
  • tb.read(..., load_data=False) is essential when you only need column metadata to set dimensions; loading data unnecessarily slows the step.
  • YAML schema requires views: key in the explorer config even when empty. Pass views: [].
  • Indicator must be re-published to MySQL with the new grapher_config before the explorer view can inherit it. Re-run etlr --grapher data://grapher/... after editing garden metadata; the explorer step alone won't refresh the indicator's stored config.
  • YAML anchors / merge keys (&common_view, <<: *common_view) — don't use them. They can't filter by dimension and add per-view noise. Use definitions.common_views instead.
  • Hyphens in short_name — file is food_footprints.py but short_name="food-footprints". Mismatch produces a published explorer whose URL doesn't match the legacy slug.
  • tolerate_extra_indicators — usually want True for explorers since you're cherry-picking indicators from larger upstream datasets.
  • Checkbox dimensions must have exactly 2 choices. The na pattern (3 choices: na, off, on) works for radio and dropdown but not checkbox — the framework rejects it with Dimension choices for 'checkbox' must have exactly two choices. If you genuinely need a third "doesn't apply" state, either use a radio with three choices, or drop the na slot and let the framework hide/disable the toggle when the current dimension state has no matching <true> view.
  • YAML 1.1 booleanizes unquoted no/yes/on/off/true/false slugs. A view.dimensions: { relative_to_world: no } reads back as {"relative_to_world": False}, which won't match the string slug "no" declared in dimensions[*].choices. Use semantic slugs (total/relative, absolute/share) or quote the strings — but choosing different slug names is the more durable fix.
  • edit_views doesn't reach indicator-level display. Fields like numDecimalPlaces, colorScaleScheme, colorScaleNumericBins, and per-indicator color live on view.indicators.y[i].display, not on view.config. edit_views only writes view-level config/metadata. For these, fall back to a c.views loop (see Step 6) or push them into the indicator's garden presentation.grapher_config so they apply at chart render time.

Reference examples

Full-YAML (each view hand-listed):

  • etl/steps/viz/explorer/agriculture/latest/crop_yields.{py,config.yml} — large indicator-based explorer with many dimensions.
  • etl/steps/viz/explorer/agriculture/latest/food_prices.{py,config.yml} — small grapher-chart-based migration (12 chart IDs unwrapped to 12 single-indicator views) with conditional dimensions ("na" pattern).
  • etl/steps/viz/explorer/agriculture/latest/fertilizers.{py,config.yml} — checkbox dimension with choice_slug_true; multi-namespace dependencies.
  • etl/steps/viz/explorer/food/latest/food_footprints.{py,config.yml} — hybrid (16 grapher-chart views + 29 CSV-backed views) showing dimension-filtered common_views for differing sourceDesc per view-type.
  • etl/steps/viz/explorer/war/latest/countries_in_conflict_data.{py,config.yml} — uses na-named choices to model conditional dimensions.
  • etl/steps/viz/explorer/emissions/latest/ipcc_scenarios.{py,config.yml} — moderate-size YAML-driven.

Table-driven (views auto-expanded from a dimensional table):

  • etl/steps/viz/explorer/migration/latest/migration_flows.{py,config.yml} — passes tb=tb, indicator_names=[...], dimensions=[...] to create_explorer; YAML carries only the static config and dimension presentation. Includes add_display_settings(c) post-processing.
  • etl/steps/viz/explorer/emissions/latest/co2.{py,dims.yaml,config.yml} — sidecar .dims.yaml for the 54-entry column→dimensions map; uses c.edit_views([...]) with both an unscoped default and a 4-dim-filtered override; uses a c.views loop with view.matches(...) for per-indicator numDecimalPlaces overrides.
  • etl/steps/viz/explorer/emissions/latest/air_pollution.{py,config.yml} — table-driven with c.group_views(...) to add facet views, c.drop_views(...) to prune cross-products.
  • etl/steps/viz/chart/minerals/latest/minerals.py — same APIs in the multidim channel; useful read for choice_renames.

Follow-up

Once an explorer is on paths.create_explorer(), it's a candidate for the Track-B port to MDIM (viz://chart/...) once feature parity is reached. See umbrella issue #6014.

© owid, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/create-explorer of owid/etl.

Open the folder on GitHubat commit 69ab20e

Compare with similar skills

Create Explorer next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Create Explorer compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Create Explorer this skillowid/etl158—~7.2kAutomated safety check: PassMIT
Crawl4AI Web Scrapingsmallnest/goclaw5981 repos~2.5kAutomated safety check: PassMIT
Glue 09 10 Migrationaws-samples/aws-glue-samples1.5k—~2.4kAutomated safety check: PassMIT-0
Migrate Glue Devendpoint To Interactive Sessionsaws-samples/aws-glue-samples1.5k—~3.6kAutomated safety check: PassMIT-0
Dbt Databricks PR Readydatabricks/dbt-databricks379—~2.8kAutomated safety check: PassApache-2.0
Mz Dbt ReleaseMaterializeInc/materialize6.4k—~1.2kAutomated safety check: PassCustom licence

Similar skills

  • Crawl4AI Web Scraping

    smallnest/goclaw

    Scrapes sites, handles JavaScript-heavy pages and extracts structured data with Crawl4AI, through its crwl CLI or Python SDK, including schema-based extraction without an LLM.

    598 GitHub starsUsed in 1 repo~2.5k tokens
    Data & AnalyticsAuto-check passed
  • Glue 09 10 Migration

    aws-samples/aws-glue-samples

    Official

    Upgrade an AWS Glue ETL job from Glue version 0.9 or 1.0 to Glue 4.0.

    1.5k GitHub stars~2.4k tokensUpdated 1 mo ago
    Data & AnalyticsAuto-check passed
  • Official

    Migrate a legacy AWS Glue development endpoint to a Glue interactive session, following the official AWS migration checklist.

    1.5k GitHub stars~3.6k tokensUpdated 1 mo ago
    Data & AnalyticsAuto-check passed
  • Dbt Databricks PR Ready

    databricks/dbt-databricks

    Official

    A skill your agent uses for an open dbt-databricks pull request, including your own PR or a fork PR, to assess merge readiness and optionally repair selected gaps on the PR head branch.

    379 GitHub stars~2.8k tokensUpdated today
    Data & AnalyticsAuto-check passed
  • Mz Dbt Release

    MaterializeInc/materialize

    Cut a dbt-materialize PyPI release: bump the version in version.py and setup.py, date the Unreleased CHANGELOG entry, and open the release PR with a Ship: <url body.

    6.4k GitHub stars~1.2k tokensUpdated today
    Data & AnalyticsAuto-check passed
  • Erd Studio Setup

    liam-machine/erd-studio

    Friendly, step-by-step setup for ERD Studio in an existing dbt project, for people who may be new to dbt or data modelling.

    165 GitHub stars~8.5k tokensUpdated 3 days ago
    Data & AnalyticsAuto-check passed

More from owid/etl

All 35 skills in this repo
  • Find every OWID surface that references a chart, indicator, MDIM, or explorer — articles (links vs embeds), explorers, narrative charts, data insights, static viz, key-chart slots, MDIM views.

    158 GitHub stars~4.9k tokensUpdated today
    Auto-check passed
  • Add a scatter view (with GDP per capita on x) to existing OWID charts via the admin API, mirroring the admin UI's "Add scatter type" defaults, then retire the old standalone "X vs.

    158 GitHub stars~19k tokensUpdated today
    Auto-check passed
  • Add new survey question codes (e.g. An agent skill from owid/etl.

    158 GitHub stars~11k tokensUpdated today
    Auto-check: notes
  • Build or refresh an OWID static visualization end to end — resolve what data it needs from an old static viz image, an indicator, or a grapher chart; check both the ETL catalog and the producer's…

    158 GitHub stars~8.3k tokensUpdated today
    Auto-check passed
  • Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.

    158 GitHub stars~9.6k tokensUpdated today
    Auto-check: notes
  • Take (soon-to-sunset) OWID explorers to redirected MDIMs, end to end.

    158 GitHub stars~7.3k tokensUpdated today
    Auto-check: notes

Questions about Create Explorer

What does Create Explorer do?

Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via viz://explorer/<ns/latest/<short). Create Explorer is an agent skill from owid/etl. Author or modify an Our World in Data explorer (multi-dimensional dashboard with dropdown selectors, published from ETL via viz://explorer/<ns/latest/<short).

When should I use Create Explorer?

Create Explorer fits situations like: the user wants to build a new explorer; add/remove views; dimensions on an existing one; change the explorers chart text.

How do I install Create Explorer in Claude Code?

Run `npx skills add owid/etl --skill create-explorer -a claude-code`. Or copy the skill folder (.claude/skills/create-explorer in owid/etl) into .claude/skills/create-explorer in your project. Claude Code loads it when a task matches its description.

How do I install Create Explorer in Codex?

Run `npx skills add owid/etl --skill create-explorer -a codex`. Or copy the skill folder (.claude/skills/create-explorer in owid/etl) into .agents/skills/create-explorer in your project. Codex loads it when a task matches its description.

Can I use Create Explorer in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add owid/etl --skill create-explorer -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/create-explorer, .gemini/skills/create-explorer, .github/skills/create-explorer and .opencode/skills/create-explorer in your project.

What does Create Explorer need to run?

Going by SKILL.md and its folder, Create Explorer needs the command-line tools its instructions call (make). Our summary lists: Python 3.

Does Create Explorer access the network?

SKILL.md names 1 domain. In commands or code: assets.ourworldindata.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Create Explorer safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Create Explorer use?

Create Explorer is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Create Explorer use?

About 7.2k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Create Explorer?

Skills that share tags, products or a category with Create Explorer: Crawl4AI Web Scraping (smallnest/goclaw, 598 stars), Glue 09 10 Migration (aws-samples/aws-glue-samples, 1.5k stars), Migrate Glue Devendpoint To Interactive Sessions (aws-samples/aws-glue-samples, 1.5k stars) and Dbt Databricks PR Ready (databricks/dbt-databricks, 379 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Create Explorer?

owid (a GitHub organization) maintains it in owid/etl, which has 158 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on October 7, 2026.

Source: owid/etl on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.