
Migrating Motoko
- 67 installs
- 28 repo stars
- Updated August 4, 2026
- dfinity/icskills
Migrate Motoko canister actor state across an upgrade using the inline `(with migration = ...)` syntax for field renames or type changes.
About
Handles inline actor state migration for Motoko canisters using the `(with migration = ...)` syntax when renaming fields, changing types, or restructuring state on upgrade. A developer uses it for a single one-shot migration without the enhanced-migration flag.
- Migrates actor state with `(with migration = ...)` inline syntax
- Distinguishes implicit compatible upgrades from explicit migrations for renames/type changes
Migrating Motoko by the numbers
- 67 all-time installs (skills.sh)
- Ranked #183 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-motokoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 28 |
| Last updated | August 4, 2026 |
| Repository | dfinity/icskills ↗ |
What it does
Migrate Motoko canister actor state across an upgrade using the inline `(with migration = ...)` syntax for field renames or type changes.
Files
Inline Actor Migration
Migrate actor state across canister upgrades using a migration expression attached to the actor. Each upgrade has at most one migration function.
For multi-migration with a `migrations/` directory, load migrating-motoko-enhanced instead.
When to Use
Implicit migration (no code needed)
The runtime allows the upgrade if the new program is compatible with the old:
- Adding actor fields
- Removing actor fields
- Changing mutability (
var↔let) - Adding variant constructors
- Widening types (
Nat→Int)
Explicit migration required
- Renaming fields
- Changing a field's type (e.g.
Bool→ variant,Int→Float) - Restructuring state (splitting/merging fields)
- Transforming collection values
Syntax
Parenthetical expression immediately before the actor:
import Migration "migration";
(with migration = Migration.run)
actor {
var newState : Float = 0.0;
};Or inline:
import Int "mo:core/Int";
(with migration = func(old : { var state : Int }) : { var newState : Float } {
{ var newState = old.state.toFloat() }
})
actor {
var newState : Float = 0.0;
};Or using the shorthand when the imported module exports a migration field:
import { migration } "migration";
(with migration)
actor { ... };Migration Function Rules
- Type:
func (old : { ... }) : { ... }— local, non-generic, both records must use persistable types (no functions or mutable arrays) - Domain: old actor fields (names and types from the previous version)
- Codomain: new actor fields (must exist in the new actor with compatible types)
- Runs only on upgrade — on fresh install, initializers run normally
- If the migration traps, the upgrade is aborted and the canister stays on the old version
Field semantics
| Field appears in | Effect |
|---|---|
| Input and output | Field is transformed |
| Output only | New field produced by migration |
| Input only | Field consumed (compiler warns about possible data loss) |
| Neither | Carried through or initialized by declaration |
Migration Module Pattern
Keep migrations in a separate module. Define old types inline — do not import them from old code paths:
// migration.mo
import Types "types";
import Map "mo:core/Map";
module {
type OldTask = { id : Nat; title : Text; completed : Bool };
type OldActor = {
var tasks : Map.Map<Nat, OldTask>;
var nextId : Nat;
};
type NewActor = {
var tasks : Map.Map<Nat, Types.Task>;
var nextId : Nat;
};
public func run(old : OldActor) : NewActor {
let tasks = old.tasks.map<Nat, OldTask, Types.Task>(
func(_, task) {
{
id = task.id;
title = task.title;
due = 0;
var status = if (task.completed) #completed else #pending;
}
}
);
{ var tasks; var nextId = old.nextId };
};
};// main.mo
import Map "mo:core/Map";
import Types "types";
import Migration "migration";
(with migration = Migration.run)
actor {
var tasks = Map.empty<Nat, Types.Task>();
var nextId : Nat = 0;
};Fields must have initializers — the migration function runs only on upgrade. On fresh install the initializers are used.
Common Patterns
Add field with default
old.users.map<Nat, OldUser, NewUser>(
func(_, u) { { u with zipCode = "" } }
)Add optional field
{ task with var assignee = null : ?Principal }Bool to variant
var status = if (task.completed) #completed else #pending;Rename a field
Consume old name, produce new name:
func(old : { var state : Int }) : { var value : Int } {
{ var value = old.state }
}Drop a field
Consume it in the input, omit from output. Compiler warns — ensure the loss is intentional.
Checklist
- [ ] Decide: implicit (compatible change) or explicit (migration function)
- [ ] If explicit: define old types inline in
migration.mo - [ ] Migration type:
func (old : RecordIn) : RecordOutwith persistable types - [ ] Attach with
(with migration = Migration.run)before the actor - [ ] Do not use
preupgrade/postupgradefor data migration - [ ] Verify with
mops check --fixandmops build
Additional References
- Load
motokofor general Motoko language reference and mo:core APIs - Load
migrating-motoko-enhancedfor multi-migration with--enhanced-migration - Load
mops-cliformops check,mops build, and toolchain setup