Skip to content
Development
Skill

/app-db-migrations

Add, test, run and roll back application-database (Liquibase) migrations. Use when adding a changeset, changing a table or column in the Metabase app DB, writing a custom (Clojure) migration, or rolling migrations back in development.

BOOST
From plugin
metabase
50k30 skills11 agents23 commands
Install
$ npx -y skills add metabase/metabase --skill app-db-migrations --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/app-db-migrations

Context preview

The summary Claude sees to decide when to auto-load this skill.

Add, test, run and roll back application-database (Liquibase) migrations. Use when adding a changeset, changing a table or column in the Metabase app DB, writing a custom (Clojure) migration, or rolling migrations back in development.

SKILL.md

app-db-migrations.SKILL.md
name: app-db-migrations
description: Add, test, run and roll back application-database (Liquibase) migrations. Use when adding a changeset, changing a table or column in the Metabase app DB, writing a custom (Clojure) migration, or rolling migrations back in development.

Application DB migrations

Schema changes to the Metabase application database are Liquibase changesets in YAML under `resources/migrations/`. Every changeset in the repository is linted by `./bin/lint-migrations-file.sh` (`mage lint-migrations`); run it before pushing.

Where a new changeset goes

New changesets are **version-less** and live in a year directory. Create one with `./bin/mage new-migration <snake_case_name>`, which writes:

resources/migrations/2026/20261012_short_snake_case_name.yaml   # changeset id: a random one, e.g. k3x9qa
  • The directory is the current year; the file name is `<yyyymmdd>_<snake_case_name>.yaml` (lower-case letters,

digits and underscores only). One file per change or per closely related group of changes; several changesets in one file are fine.

  • The changeset **id is a random identifier with no version**, e.g. `k3x9qa`: `new-migration` generates one for the

first changeset, checking it is unused in `resources/migrations`. For more changesets in the same file, make up another the same way and grep to confirm it is unused. The lint accepts lower-case letters, digits and underscores with at least one letter -- not an ISO date or timestamp like the old ids carried (`2026-09-18T12:00:00`), not `v66.…`, not all digits, and no leading `v`: releases from before version-less support read the schema's version from the highest `id LIKE 'v%'`.

  • The Metabase version a changeset ships in is decided by the branch it is merged into, not by anything in the file.

Do **not** start new work in the versioned files (`migrations/0NN/…` with `vNN.` ids, or `0NN_update_migrations.yaml`). The lint still accepts `vNN.` ids up to v66, so changesets already in flight for 66 can land, and refuses them from v67 on. Don't add versioned changesets on release branches either: changelog order puts every versioned file before the year directories, so a versioned changeset added after a version-less one would run before it on a fresh install but after it on an upgraded one.

Backporting a changeset

Version-less support is backported to patch releases of every supported major back to v58, so a version-less changeset is backported like any other change: cherry-pick the **same file** onto each release branch. Liquibase identifies a changeset by file path, id and author, so the upgraded customer's database already has it as run. A few rules follow from the same changeset meeting different histories:

  • **The file must be byte-identical on every branch.** Liquibase checksums ran changesets, and a different body on

another branch fails checksum validation when a customer upgrades onto it. Fix a mistake with a new changeset.

  • **Ship it on every branch from the oldest target up to master.** A customer can upgrade from any patch of an older

major to any later one.

  • **Use only schema that exists in the oldest major it targets.**
  • **It runs at different points in different histories.** On a fresh install of a newer major it runs after that

major's `vNN.` changesets; on a database upgraded from an older major's patch it already ran before them. Make sure none of the later majors' changesets conflict with it either way round, e.g. by both creating the same object.

  • Customers on a patch from before the backport still boot against a database it ran on: those releases ignore

changelog rows they don't know, and Metabase keeps a marker row last in `databasechangelog` so they read the schema's version correctly (see `metabase.app-db.liquibase.versions/record-legacy-version-tracking!`).

Writing the changeset

databaseChangeLog:
  - changeSet:
      id: k3x9qa
      author: yourgithubhandle
      comment: Add entity_id to glossary
      changes:
        - addColumn:
            tableName: glossary
            columns:
              - column:
                  name: entity_id
                  type: varchar(21)
                  remarks: NanoID for serialization
      rollback:
        - dropColumn:
            tableName: glossary
            columnName: entity_id

Rules the lint enforces (and reviewers expect):

  • **One change per changeset** (several `sql` changes are allowed only when each targets a different `dbms`).
  • `comment` is required; `createTable` needs `remarks`, and by convention so does every column.
  • Use the portable types: `${boolean.type}`, `${timestamp_type}`, `${text.type}`, `${blob.type}`. Never bare

`boolean`, `text`, `blob`, `datetime` or `timestamp without time zone`.

  • **Rollback**: include a `rollback` key unless the change type supports Liquibase auto-rollback (`createTable`,

`addColumn`, `createIndex`, `addForeignKeyConstraint`, `renameColumn`, …). `sql`, `dropColumn`, `dropTable`, `modifyDataType` and the like need an explicit rollback (an empty `rollback:` is allowed when there is genuinely nothing to undo).

  • Preconditions that check a constraint or index must be scoped to a table (`indexExists` / `primaryKeyExists` /

`uniqueConstraintExists` need `tableName`, `foreignKeyConstraintExists` needs `foreignKeyTableName`), or Liquibase snapshots the whole schema to answer them. Preconditions are rarely needed at all: rely on Liquibase's changeset tracking.

  • No `deleteCascade` on an `addColumn` constraint (add the foreign key with `addForeignKeyConstraint` instead).
  • Migrations must work on H2, PostgreSQL and MySQL/MariaDB; anything database-specific gets a `dbms:` filter.
  • **Shipped changesets are immutable.** Fix a mistake with a new changeset; never edit or delete one that a release

contains. `runOnChange: true` changesets (views) are the one exception and may be edited in place.

Retiring a shipped changeset

Never j

Read more
Ships withmetabase

Metabase is the easy, open-source way for everyone in your company to ask questions and learn from data.

Get the whole plugin

Other skills on metabase.