Ecto Migrations

SkillDatabases & data

Rules for Operately schema migrations (app/priv/repo/migrations/) and data migrations (app/lib/operately/data/change_*.ex). Use when adding, renaming, reviewing, or generating database migrations, ecto.gen.migration, Operately.Data.Change* modules, backfills, schema_migrations version collisions, mix ecto.migrate, or make gen.migration.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Ecto Migrations skill

What this skill tells your AI

The instructions your AI receives, as published by operately/operately in .agents/skills/ecto-migrations/SKILL.md and read by ahel’s review.

Two layers work together:

LayerLocationRole
Schema migrationapp/priv/repo/migrations/*.exsDDL and/or calls a data change; tracked in schema_migrations
Data migrationapp/lib/operately/data/change_NNN_*.exBackfills and data fixes; invoked from a thin schema migration

Creating schema migration files

Always create files in app/priv/repo/migrations/ with:

make gen.migration NAME=add_foo_to_bars

That runs mix ecto.gen.migration inside devenv (see root Makefile gen.migration target) and assigns a unique timestamp version.

Never create or copy migration files by hand (including inventing a YYYYMMDDHHMMSS_*.exs name). Manual timestamps collide across PRs that land close together. Ecto keys migrations by the numeric prefix only, so duplicates cause skipped migrations, re-runs, and production failures (e.g. duplicate_column or missing columns after a rename).

After generating, edit only the body of the generated file. Do not rename the file to “fix” a conflict with another branch — coordinate timestamps via make gen.migration on an up-to-date main instead.

Data migrations

Backfills and one-off data fixes live under app/lib/operately/data/ as Operately.Data.ChangeNNN… modules. A thin schema migration (created with make gen.migration) calls them:

defmodule Operately.Repo.Migrations.BaselineDocumentVersions do
  use Ecto.Migration

  def up do
    Operately.Data.Change110BaselineDocumentVersions.run()
  end

  def down do
    :ok
  end
end

Examples: change_110_baseline_document_versions.ex20260720190200_baseline_document_versions.exs; change_106_backfill_document_names_from_nodes.ex20260720120100_backfill_document_names_from_nodes.exs.

Naming

  • Next unused number: look at the highest change_NNN_*.ex under app/lib/operately/data/ (currently in the 110s).
  • Module: Operately.Data.ChangeNNNDescriptiveName
  • File: change_NNN_descriptive_name.ex
  • Prefer a run/0 entry point (some older modules use up/0).

Do not depend on application modules

Data migrations must not alias live app schemas such as Operately.Goals.Goal or Operately.Activities.Activity. Those modules change over time; a migration that imports them can break on fresh installs months later.

Define minimal inline structs/modules inside the change module with only the fields and helpers the migration needs.

# ❌ DON'T: referencing application modules directly inside migrations
defmodule Operately.Data.Change155MyMigration do
  alias Operately.Repo
  alias Operately.Activities.Activity

  def run do
    from(a in Activity, where: a.action == "task_description_change")
    |> Repo.update_all(set: [content: %{}])
  end
end

# ✅ DO: declare minimal inline structs/modules needed by the migration
defmodule Operately.Data.Change155MyMigration do
  alias Operately.Repo
  alias __MODULE__.Activity

  def run do
    from(a in Activity, where: a.action == "task_description_change")
    |> Repo.update_all(set: [content: %{}])
  end

  defmodule Activity do
    use Operately.Schema

    schema "activities" do
      field :action, :string
      field :content, :map
    end
  end
end

References: change_082_populate_goal_description_changed_activity_goal_name.ex, change_080_create_subscriptions_list_for_tasks.ex, change_110_baseline_document_versions.ex.

(Same rule is summarized in root AGENTS.md under Data Migration Guidelines.)

Idempotency and tests

  • Prefer idempotent run/0 (safe if re-run or if some rows already match the target state).
  • Add tests under app/test/operately/data/change_NNN_*_test.exs (see change_110_baseline_document_versions_test.exs).

Checklist

  • Schema migration created via make gen.migration NAME=...
  • No hand-written or hand-copied app/priv/repo/migrations/*.exs filenames
  • Version prefix is unique among existing migrations before opening the PR
  • Data backfill lives in Operately.Data.ChangeNNN… with inline schemas only
  • Schema migration’s up/0 only delegates to ChangeNNN.run() (plus any DDL)
  • Data change is idempotent where practical and covered by a unit test

Signals

GitHub stars
550
Forks
69
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ecto-migrations
Source
github.com/operately/operately