About metabase-data-app-migrate
A procedural skill for migrating Metabase data apps between contract versions. It guides developers through sequential, checked upgrades, preserving versioning and commit safeguards.
Key features
- 01Sequential version-by-version upgrades
- 02Upgrade-guide checks at each step
- 03Controlled version and commit changes
- 04Final typecheck, resource, and build gates
- 05Resume handling for interrupted migrations
Use cases
- 01Updating an app after a contract change
- 02Resuming an interrupted version migration
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ metabase/ metabase --skill metabase-data-app-migrateAfter installing, type/metabase-data-app-migratein Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/maintainer-checklist.mdupgrade-guide-template.mdreferences/upgrades/README.mdDetails
From the project READMEMigrate a data app to the current contract version
A data app declares the contract version its code targets in data_app.yaml
(version: N; a manifest without the field is version 1). Metabase bumps the
version it serves only on a breaking change to that contract: the
@metabase/embedding-sdk-react/data-app API, the bundle factory, the manifest,
the queries/ and actions/ conventions, or what an app may declare. An app on
an older version is marked Outdated in the admin list, hidden from every other
user, and refuses to open until it is migrated.
Migration is a walk over upgrades: N -> N+1 -> ... -> M, one upgrade at a time,
each described by a guide in references/upgrades/. Nothing is skipped and nothing
is remembered between sessions: the app's own files and git history carry all
the state.
Four invariants
Every step below follows from these. Never break them.
versionis written last. An upgrade'sversion: N+1goes intodata_app.yamlonly after every check of that upgrade passes.- One commit per upgrade, locally. Push only after the final gates at the target version pass. The version line never moves by more than one per commit.
- Compiler and build gates run only at the target version. The installed SDK is the target, so an app halfway through several upgrades cannot type-check or build. Do not run them earlier and do not "fix" their failures earlier.
- Nothing generated is edited by hand.
dist/is written bynpm run buildandsrc/metabase.data.tsby the typed-schema export, never by you. The files of the app's collection undercollections/data_apps/and the entity IDs its definitions name (savedQuestionEntityId,copiedActionEntityId) change only where an upgrade guide says so, following the data-app guidance on writing the files of an app's collection (use skill discovery).
Step 0 - Locate the app and read its state
Work from the app directory, <repo>/data_apps/<slug>/. Resolve the repo root
with ROOT="$(git rev-parse --show-toplevel)". The instance checks need the
repo-root .env.local credentials (DATA_APP_MB_URL, DATA_APP_MB_API_KEY).
Check them by sourcing the file in a subshell and printing only whether both are
set; never print the file or its variables. The key must belong to an admin:
the API returns version and outdated only to admins and refuses an outdated
app's metadata to everyone else, so a non-admin key cannot prove anything here.
npm below stands for the app's own package manager, the one whose lockfile is
committed (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock).
Never introduce a second lockfile.
Read three numbers:
# version committed at HEAD (absent line means 1)
git show HEAD:data_apps/<slug>/data_app.yaml | grep -E '^version:' || echo "version: 1"
# version in the working tree
grep -E '^version:' data_app.yaml || echo "version: 1"
# target: the highest upgrade guide shipped with this skill (no guides means 1)
ls <skill-dir>/references/upgrades/ | sed -nE 's/^v[0-9]+-to-v([0-9]+)\.md$/\1/p' | sort -n | tail -1 | grep . || echo 1
<skill-dir> is the directory this SKILL.md was loaded from. The target must
equal the version: in the data-app scaffolding template installed alongside
this skill (<skills-dir>/*/template/data_app.yaml), when one is present; if
they differ, the skills come from different Metabase releases. Stop and tell
the user to reinstall all data-app skills with the command shown under
Admin > Data apps. Wait for the answer.
Then decide:
| HEAD version | working tree | do |
|---|---|---|
| equals target | clean | Nothing to migrate. Say so; run the final gates only if the user asked to verify. |
| above target | any | Stop. There is no downgrade. Either Metabase, the SDK, and the skills are older than the app, or a bump was committed against the wrong instance (git log -- data_apps/<slug>/data_app.yaml shows it). Wait for the answer. |
| below target | clean | Start at Step 1. |
| any | dirty under data_apps/<slug> | A previous session was interrupted. Go to Resuming first. |
Step 1 - Preflight, once
- Install the SDK that matches the target Metabase release with the app's
package manager (
npm install @metabase/embedding-sdk-react@<tag>or its equivalent); each upgrade guide names the dist-tag it was written against. The lockfile change is committed with the final upgrade. - Confirm the app is template-shaped at its current version:
vite.config.tsis the one-linerexport default dataAppConfig()andsrc/index.tsxdefault-exports aDataAppFactory. If not, Stop: this is not a version migration but structural drift. Offer to scaffold a fresh app under the same slug (a separate task: use skill discovery for creating a data app) and portsrc/,queries/, andactions/over, or to proceed at the user's risk. Wait for the answer. - Read every upgrade guide from the current version to the target before editing anything, so a later upgrade cannot surprise you halfway through an earlier one.
Step 2 - The upgrade loop
For each N from the HEAD version up to target - 1:
- Open
references/upgrades/v<N>-to-v<N+1>.mdand run its Preconditions. - Apply the guide's Steps in order. Each step is mechanical and ends with a Done when command. Run the command first: if it already passes, skip the step; if not, apply the edit and run it again. A step's command is the only proof it happened; never mark a step done by reading.
- Apply the Instance steps, if any. Their Done when is an API response
from the connected Metabase, fetched with the credentials from
.env.local:( source "$ROOT/.env.local"; curl -s -H "x-api-key: $DATA_APP_MB_API_KEY" "$DATA_APP_MB_URL/api/apps/<slug>" ) - Run the guide's Done-check summary. Every line must print
ok. - Only now set
version: N+1indata_app.yaml. This is the one edit to that line. - If
N+1 < target: from the repo root,git add data_apps/<slug> collections/data_apps && git commit -m "Migrate <slug> data app to data-app version N+1" -- data_apps/<slug> collections/data_apps. The pathspec keeps anything staged outside the app out of the commit. No push, no typecheck, no build. If the user asks why, say the app cannot compile until the last upgrade. - If
N+1 == target: run Step 3 before committing.
Do not batch steps across upgrades. Do not touch version before item 5.
Step 3 - Final gates, at the target version only
npm run typecheck. Fix failures inside the app's own source. A failure that names an SDK symbol no upgrade guide mentions is a gap in the guides, not a user problem: surface the exact error, say which guide should have covered it, and stop. Otherwise, at most three fix rounds, then stop and ask.npm run check-resources, thennpm run build, which refuses to bundle when the app's collection files don't back the definitions. Follow their messages; neither calls Metabase.npm run dev, then take the preview URL from theLocal:line Vite prints. The template asks for port 5174, but Vite moves to the next free port when that one is taken, so never assume it. Open that URL, then read the diagnostics feed once from the same origin:
Expectcurl -s "<preview-url>/__data-app/diagnostics?startEventId=0"clients: 1and no entry with"alert": true.clients: 0means no preview tab is open, so an empty feed proves nothing.git statusfrom the repo root must show only: the upgrade edits,data_app.yaml, the built bundle, the app's collection files if an upgrade changed them, and the package lockfile if the SDK was re-pinned. Anything else inside the app directory is a user edit; say so before committing.- From the repo root,
git add data_apps/<slug> collections/data_apps && git commit -m "Migrate <slug> data app to data-app version M" -- data_apps/<slug> collections/data_apps, push, and tell the user to Pull changes under Admin > Data apps. - Prove it. After the pull:
must show( source "$ROOT/.env.local"; curl -s -H "x-api-key: $DATA_APP_MB_API_KEY" "$DATA_APP_MB_URL/api/apps/<slug>" )"version": Mand"outdated": false. A response holding onlynameanddisplay_namemeans the key is not an admin's: ask for one, or ask the user to read the admin list, and say the proof is still pending. Report the command and the result. Do not claim the migration is done without it.
Resuming an interrupted migration
A fresh session has no memory of the previous one and needs none. Read the
three numbers from Step 0 and git status --porcelain -- data_apps/<slug>,
then:
| tree | working-tree version | meaning | do |
|---|---|---|---|
| clean | equals HEAD | every finished upgrade is a commit; nothing half done | continue the loop from the HEAD version |
| dirty | equals HEAD | upgrade HEAD -> HEAD+1 was in progress and its checks had not all passed | re-run every Done when of that upgrade; apply only the steps whose command fails; continue from loop item 4 |
| dirty | equals HEAD + 1 | all checks passed and the bump was written, but the commit is missing (or the final gates failed after the bump) | re-run that upgrade's Done-check summary (all ok, or someone edited after the bump: stop and show git diff); if it is the target, run the final gates; commit |
| any | more than HEAD + 1, or below HEAD | an invariant was broken | Stop. Show git diff -- data_apps/<slug>/data_app.yaml and ask whether to take that one file back to HEAD and re-derive, or to stash the tree. Wait for the answer. |
Files dirty outside data_apps/<slug> are not yours: never git add them.
Reporting
One line per upgrade, in order: v1 -> v2: done (commit abc1234) or
v2 -> v3: blocked - <the exact Done-when command that fails and its output>.
Never soften a failing check. Keep tsc output grouped by root cause with a
few representative diagnostics rather than pasting it whole.
Never do
- Set or edit
versionbefore the upgrade's checks pass, or by more than one at a time. - Skip an upgrade, or apply two upgrades' steps at once.
- Run
npm run typecheckornpm run buildbefore the last upgrade, or push before the final gates pass. - Hand-edit
dist/orsrc/metabase.data.ts, or change the app's collection files or a definition's entity ID beyond what an upgrade guide says. - Copy a step's outcome from the previous session's chat instead of re-running its Done when.
- Migrate with a checklist of your own when an upgrade guide exists; when one does not exist for an upgrade you need, stop and say so.
Upgrade guides
Guides live in references/upgrades/, one per upgrade, named v<N>-to-v<N+1>.md. They
are written by Metabase when the contract version is bumped, from the template
in references/upgrade-guide-template.md; references/maintainer-checklist.md
describes that process. A missing guide for an upgrade you need is a bug in the
skill install, not something to improvise around.