Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dfinity avatar

Migrating Motoko Enhanced

  • 65 installs
  • 28 repo stars
  • Updated August 4, 2026
  • dfinity/icskills

Manage a chain of Motoko canister state migrations across multiple upgrades using a migrations/ directory and the --enhanced-migration flag.

About

Manages multi-step Motoko actor state migration through a migrations/ directory and the --enhanced-migration flag, with the compiler verifying the entire chain. A developer uses it when evolving canister state across multiple deployments.

  • Multi-step migration chain via a migrations/ directory and --enhanced-migration
  • Compiler verifies the whole chain; forbids stable keyword and pre/postupgrade

Migrating Motoko Enhanced by the numbers

  • 65 all-time installs (skills.sh)
  • Ranked #188 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dfinity/icskills --skill migrating-motoko-enhanced

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs65
repo stars28
Last updatedAugust 4, 2026
Repositorydfinity/icskills

What it does

Manage a chain of Motoko canister state migrations across multiple upgrades using a migrations/ directory and the --enhanced-migration flag.

Files

SKILL.mdMarkdownGitHub ↗

Enhanced Multi-Migration

Manage canister state evolution through a chain of migration modules. Each migration captures one logical change (add, rename, drop, transform a field) and the compiler verifies the entire chain is consistent.

When to Use

  • Adding, removing, or renaming persistent actor fields
  • Changing a field's type
  • Restructuring state across canister upgrades
  • Project has [canisters.<name>.migrations] configured in mops.toml

Critical Rules

  • Never use stable keyword, preupgrade/postupgrade, or inline (with migration = ...)
  • Actor variables are declared without initializers — values come from the migration chain
  • The actor body must be static (no top-level side effects except <system> calls like timers)
  • Each migration file exports public func migration({...}) : {...}
  • Files are applied in lexicographic order — use timestamp prefixes

Directory Layout

backend/
├── main.mo
├── types.mo
├── lib/
├── mixins/
└── migrations/
    ├── 20250101_000000_Init.mo
    ├── 20250315_120000_AddProfile.mo
    └── 20250601_090000_RenameField.mo

Actor Syntax

With enhanced migration, actor variables have no initializer:

actor {
  var name : Text;       // value comes from migration chain
  var balance : Nat;     // likewise
  let frozen : Bool;     // let bindings can also be uninitialized

  public func greet() : async Text {
    "Hello, " # name # "! Balance: " # debug_show balance;
  };
};

Migration Module Structure

Each migration module takes a record of input fields and returns a record of output fields:

// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { name : Text; balance : Nat } {
    { name = ""; balance = 0 }
  }
}

Input / Output Field Semantics

Field appears inEffect
Input and outputField is transformed (old value read, new value produced)
Output onlyNew field added to state
Input onlyField consumed and removed from state
NeitherField carried through unchanged

Given state {a : Nat; b : Text; c : Bool} and migration:

module {
  public func migration(old : { a : Nat; b : Text }) : { a : Int; d : Float } {
    { a = old.a; d = 1.0 }
  }
}
  • a: transformed Nat → Int
  • b: consumed (removed)
  • c: carried through unchanged
  • d: newly introduced
  • Result: {a : Int; c : Bool; d : Float}

Common Patterns

Initialize state (first migration, always required)

// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { count : Nat; header : Text } {
    { count = 0; header = "default" }
  }
}

Add a field

// migrations/20250201_000000_AddEmail.mo
module {
  public func migration(_ : {}) : { email : Text } {
    { email = "" }
  }
}

Add an optional field

module {
  public func migration(_ : {}) : { assignee : ?Principal } {
    { assignee = null }
  }
}

Change a field's type

// migrations/20250301_000000_CountToInt.mo
module {
  public func migration(old : { count : Nat }) : { count : Int } {
    { count = old.count }
  }
}

Rename a field

// migrations/20250401_000000_RenameHeader.mo
module {
  public func migration(old : { header : Text }) : { title : Text } {
    { title = old.header }
  }
}

Remove a field

// migrations/20250501_000000_DropEmail.mo
module {
  public func migration(_ : { email : Text }) : {} {
    {}
  }
}

Transform data (split a field)

// migrations/20250601_000000_SplitName.mo
import Text "mo:core/Text";

module {
  public func migration(old : { name : Text }) : { firstName : Text; lastName : Text } {
    let parts = old.name.split(#char ' ');
    let first = switch (parts.next()) { case (?f) f; case (null) "" };
    let last = switch (parts.next()) { case (?l) l; case (null) "" };
    { firstName = first; lastName = last }
  }
}

Bool to variant

module {
  public func migration(old : { var completed : Bool }) : { var status : { #pending; #completed } } {
    { var status = if (old.completed) { #completed } else { #pending } }
  }
}

Map over a collection

import Map "mo:core/Map";

module {
  type OldTask = { id : Nat; title : Text; var completed : Bool };
  type NewTask = { id : Nat; title : Text; var status : { #pending; #completed } };

  public func migration(old : { var tasks : Map.Map<Nat, OldTask> })
    : { var tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) {
        {
          id = task.id;
          title = task.title;
          var status = if (task.completed) { #completed } else { #pending };
        }
      }
    );
    { var tasks }
  }
}

Add field to each record in a Map

import Map "mo:core/Map";

module {
  type OldUser = { name : Text; email : Text };
  type NewUser = { name : Text; email : Text; bio : Text };

  public func migration(old : { users : Map.Map<Nat, OldUser> })
    : { users : Map.Map<Nat, NewUser> } {
    let users = old.users.map<Nat, OldUser, NewUser>(
      func(_, u) { { u with bio = "" } }
    );
    { users }
  }
}

How Migrations Compose

Migrations form a chain. The compiler verifies each migration's input is compatible with the state produced by all preceding migrations.

MigrationInputOutputEffect
Init{}{name : Text; balance : Nat}Initializes both fields
AddProfile{}{profile : Text}Adds a new field
RenameField{name : Text}{displayName : Text}Renames name → displayName

After the full chain: {displayName : Text; balance : Nat; profile : Text}. The actor must declare fields compatible with this final state.

Lifecycle Example: Todo App

Shows how patterns combine across four deployments.

// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { var nextId : Nat } {
    { var nextId = 0 }
  }
}
// migrations/20250201_000000_AddTasks.mo
import Map "mo:core/Map";
module {
  type Task = { id : Nat; text : Text; completed : Bool };
  public func migration(_ : {}) : { tasks : Map.Map<Nat, Task> } {
    { tasks = Map.empty<Nat, Task>() }
  }
}
// migrations/20250301_000000_TaskStatus.mo — transform Bool → variant
import Map "mo:core/Map";
module {
  type OldTask = { id : Nat; text : Text; completed : Bool };
  type NewTask = { id : Nat; text : Text; status : { #pending; #inProgress; #completed } };
  public func migration(old : { tasks : Map.Map<Nat, OldTask> })
    : { tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) {
        { id = task.id; text = task.text;
          status = if (task.completed) #completed else #pending }
      }
    );
    { tasks }
  }
}
// migrations/20250401_000000_AddDueDate.mo — add field to each record
import Map "mo:core/Map";
module {
  type Status = { #pending; #inProgress; #completed };
  type OldTask = { id : Nat; text : Text; status : Status };
  type NewTask = { id : Nat; text : Text; status : Status; due : Int };
  public func migration(old : { tasks : Map.Map<Nat, OldTask> })
    : { tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) { { task with due = 0 } }
    );
    { tasks }
  }
}

Final state: { var nextId : Nat; tasks : Map.Map<Nat, { id : Nat; text : Text; status : { #pending; #inProgress; #completed }; due : Int }> }

Runtime Behavior

  • On fresh deploy: all migrations run in order
  • On upgrade: only not-yet-applied migrations run (already-applied are skipped)
  • Fast-forward: safe to skip intermediate deployments — all unapplied migrations run sequentially
  • If a migration traps, the upgrade is aborted and the canister stays on the old version

mops.toml Setup

[moc]
args = ["--default-persistent-actors"]

[canisters.backend]
main = "src/backend/main.mo"

[canisters.backend.migrations]
chain = "src/backend/migrations"

When [canisters.<name>.migrations] is configured, mops auto-injects --enhanced-migration into check/build/check-stable. Do not add --enhanced-migration to [canisters.<name>].args — mops will error.

--enhanced-orthogonal-persistence is on by default.

Then mops check --fix and mops build work as usual. Add new migration files directly under migrations/ with timestamp prefixes.

Restrictions

  • Cannot combine --enhanced-migration with inline (with migration = ...)
  • Requires enhanced orthogonal persistence
  • Actor variables must not have initializers
  • Actor body must be static (no top-level side effects except <system> calls)
  • State after each migration must be compatible with the next migration's input
  • Final state must match the actor's declared fields
  • Fields in last migration's output not declared in the actor are rejected

Checklist

  • [ ] migrations/ directory exists next to actor source
  • [ ] First migration initializes all fields (Init.mo with empty input)
  • [ ] Files named with timestamp prefixes for correct ordering
  • [ ] Each file exports public func migration({...}) : {...}
  • [ ] Actor variables declared without initializers
  • [ ] [canisters.<name>.migrations] configured in mops.toml (mops injects --enhanced-migration)
  • [ ] Run mops check --fix to verify chain consistency
  • [ ] Run mops build to compile

Additional References

  • Load motoko for general Motoko language reference and mo:core APIs
  • Load migrating-motoko for inline migration without --enhanced-migration
  • Load mops-cli for mops check, mops build, and toolchain setup

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.