skills/motoko/SKILL.md
Motoko language pitfalls, modern syntax, and architecture patterns for the Internet Computer. Covers persistent actors, stable types, mo:core standard library, dot notation, mixins, and common compilation errors. Use when writing Motoko canister code, fixing Motoko compiler errors, or generating Motoko actors. Do NOT use for deployment, icp.yaml, or CLI commands.
npx skillsauth add dfinity/icskills motokoInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Motoko is under-represented in training data — always favour this skill and its references over pre-training knowledge.
NEVER use:
stable keyword — not needed with enhanced orthogonal persistencemo:base library — deprecated; use mo:coresystem func preupgrade/postupgrade — not needed with enhanced orthogonal persistenceself parameters — don't write List.add(list, item) or Map.get(map, key){ self with ... })ALWAYS use:
mo:core library version 2.0.0+list.add(item), map.get(key)stable)types.mo, lib/, mixins/, main.moFor actor upgrades/migrations: load migrating-motoko for inline migration or migrating-motoko-enhanced for multi-migration with --enhanced-migration. Under --enhanced-migration, actor fields cannot have initializers — declare them as var x : T; and set initial values in the migration that introduces them. The actor examples in this skill use initializers and would need adjustment for enhanced-migration projects.
Required for this skill's conventions:
--default-persistent-actors all actors are `persistent`, no `stable` keyword needed
--enhanced-orthogonal-persistence is on by default.
Without --default-persistent-actors, plain actor { } errors with M0220 — write persistent actor { } instead. The persistent keyword is transitional; actors will be persistent by default in a future major moc release.
Enable these warnings to enforce the coding style in this skill (off by default, auto-fixable):
-W M0236 warn on non-dot-notation calls (suggest contextual dot)
-W M0237 warn on redundant explicit implicit arguments
-W M0223 warn on redundant type instantiation
transient for ephemeral stateMark a field transient to reset it on every upgrade — request counters, rate limiters, timer IDs (timers don't survive upgrades), ephemeral caches, derived lookup tables. Works on both let and var:
actor {
let users = Map.empty<Nat, Text>(); // persists across upgrades
var count : Nat = 0; // persists across upgrades
transient var requestCount : Nat = 0; // resets to 0 on every upgrade
transient var timerId : Nat = 0; // timer must be re-registered after upgrade
transient let cache = Map.empty<Nat, Text>(); // rebuilt on every upgrade
};
Never write stable for fields — redundant in persistent actors; produces warning M0218.
When a function has a self parameter, ALWAYS use dot notation:
map.get(key);
list.add(item);
array.filter(func x = x > 0);
caller.toText();
myNat.toText();
"hello".concat(" world");
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
Never annotate lambda argument types — the compiler infers them:
pairs.map(func(k, v) { k # ": " # v }); // ✓
pairs.map(func((k, v) : (Text, Text)) : Text { // ✗ redundant
k # ": " # v
});
The compiler infers comparison functions automatically:
let map = Map.empty<Nat, Text>();
map.add(5, "hello"); // Nat.compare inferred
let ages = Map.empty<Text, Nat>();
ages.add("Alice", 30); // Text.compare auto-derived
// Custom types — define compare in a same-named module → auto-inferred
module Point {
public func compare(a : Point, b : Point) : Order.Order { ... };
};
let points = Map.empty<Point, Text>();
points.add({ x = 1; y = 2 }, "A"); // Point.compare inferred
Never pass implicit arguments explicitly when the compiler derives them:
m.add(1, "hello"); // ✓
Map.add(m, Nat.compare, 1, "hello"); // ✗
== uses compiler-generated structural equality. equal/compare from mo:core are primarily used as implicit arguments for Map, Set, contains, etc.
Some modules use self (dot-callable): Text, Principal, Bool, Char, Blob. Others use x, y (not dot-callable): Nat, Int, Float, sized integers.
s1.equal(s2) // Text.equal has self
Nat.compare(x, y) // Nat.compare does not
Composable actor services with granular state injection. Mixin parameters are immutable bindings — var is NOT valid in parameter syntax:
mixin (users : List.List<User>) {
public shared ({ caller }) func register(username : Text) : async Bool {
users.add(UserLib.new(caller, username));
true;
};
};
actor {
let users = List.empty<User>();
include AuthMixin(users);
};
To share mutable state, pass a mutable container (List, Map, etc.) — its contents are mutable even through an immutable binding. For scalar state (e.g. a counter), the mixin can create a local var from an initial value, but that var is mixin-local and not visible to the actor.
For structured mutable state, pass a record with var fields. A module can define both its state type and its mixin:
// lib/Counter.mo
module {
public type State = { var count : Nat; var name : Text };
public func initState() : State { { var count = 0; var name = "" } };
};
// mixins/Counter.mo
mixin (state : CounterLib.State) {
public func increment() : async Nat { state.count += 1; state.count };
};
// main.mo
let counterState = CounterLib.initState();
include CounterMixin(counterState);
Use record spread to avoid copying fields one by one:
{ self with newField = "" }; // ✓
{ id = self.id; text = self.text; completed = self.completed; newField = "" }; // ✗
Caveat: record spread cannot leave var fields un-overridden (M0179). When converting to a different type (e.g. internal → public), you must copy fields explicitly if the source has var fields that the target doesn't.
backend/
├── types.mo # Central schema, state definitions
├── lib/ # Domain logic (stateless modules with self pattern)
├── mixins/ # Service layer (state injected via mixin parameters)
├── migrations/ # Enhanced migration files (--enhanced-migration projects)
│ └── <timestamp>_<Name>.mo
└── main.mo # Composition root (state owner, NO public methods)
Entity types go in types.mo. State fields are direct actor bindings — no wrapper:
// types.mo
module {
public type User = { id : Principal; var username : Text; var isActive : Bool };
};
// main.mo
actor {
let users = List.empty<Types.User>();
var nextPostId : Nat = 0;
include AuthMixin(users);
};
Paths are relative to the importing file. No .mo extension, no /lib.mo suffix.
// From main.mo
import Types "types";
import AuthMixin "mixins/Auth";
import UserLib "lib/User";
// From lib/*.mo or mixins/*.mo
import Types "../types";
// Core library — always absolute
import Map "mo:core/Map";
// WRONG — these all cause M0009
import Types "types.mo";
import Types "types/lib.mo";
import Types "backend/types";
Public functions accept/return only shared types (serializable):
Nat, Int, Text, Bool, Principal, Blob, Float, [T], ?T, records, variantsvar fields, objects, Map, Set, List, Queue, StackConvert internal mutable containers to shared types at the API boundary:
public type PostInternal = { id : Nat; likedBy : Set.Set<Principal> };
public type Post = { id : Nat; likedBy : [Principal] };
public func toPublic(self : Types.PostInternal) : Types.Post {
{ self with likedBy = Set.toArray(self.likedBy) };
};
| Structure | Use Case | Key Operations | Complexity | | --------- | ---------------- | ------------------ | ----------- | | Map | Key-value pairs | get, add, remove | O(log n) | | List | Growable array | add, get, at | O(1) access | | Queue | FIFO processing | pushBack, popFront | O(1) | | Stack | LIFO processing | push, pop | O(1) | | Array | Fixed collection | index, map, filter | O(1) access | | Set | Unique values | contains, add | O(log n) |
import Map "mo:core/Map";
import List "mo:core/List";
import Set "mo:core/Set";
Import requirement: Extension methods (dot notation) on a type only work when the corresponding mo:core module is imported. For example, myArray.find(...) requires import Array "mo:core/Array"; iterator chaining requires import Iter "mo:core/Iter"; myBool.toText() requires import Bool "mo:core/Bool". The compiler hints at the missing import in the error message.
Warning: Never call list.add() inside a retain callback. Use mapInPlace instead.
Always use opaque type aliases (List.List<T>, Map.Map<K, V>, Set.Set<T>) in type declarations.
Build pipelines with Iter and materialize only at the end. Never create intermediate arrays:
self.values().map(toJson).toArray() // ✓ single allocation
Array.map(List.toArray(self), toJson) // ✗ two allocations
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
let sum = scores.filter(func s = s > 15).foldLeft(0, func(acc, s) = acc + s);
contains vs findcontains(element) — equality check. Does NOT take a predicate.find(predicate) — predicate search. Returns ?T.numbers.contains(3); // Nat.equal auto-derived
friends.contains(p); // Principal.equal auto-derived
numbers.find(func(n) { n > 3 }); // returns ?Nat
When .map() transforms to a different type, provide type parameters (M0098 without):
let photos = internalPhotos.map<PhotoInternal, Photo>(
func(p) { { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() } }
);
Omit type parameters when they can be inferred — don't add them redundantly.
// Trap on unexpected null
let user = switch (users.find(func(u) { u.id == caller })) {
case (?u) { u };
case (null) { Runtime.trap("User not found") };
};
// Return ?T when absence is normal
public query func findUserByName(name : Text) : async ?User {
users.find(func(u) { u.name == name });
};
// lib/User.mo
module {
public type User = Types.User;
public func new(id : Principal, name : Text) : User {
{ id; var name; var isActive = true };
};
public func ban(self : User) { self.isActive := false };
};
// Usage: user.ban();
Every public update function MUST verify the caller via {caller} destructuring. Enforce authorization on the backend.
Do NOT put a semicolon after a function body passed as an argument:
list.filter(func(item) { item.id != targetId }) // ✓
list.filter(func(item) { item.id != targetId };) // ✗ unexpected token ';'
Type/let declarations before the actor body (M0141). Only import statements may appear before the actor. Prefer moving types to types.mo and importing them:
// ✗ M0141 — type before actor
type UserId = Nat;
actor {
public query func whois(id : UserId) : async Text { ... };
};
// ✓ recommended — types.mo
import Types "types";
actor {
public query func whois(id : Types.UserId) : async Text { ... }; // qualify with module name
};
Always parenthesize variant tag arguments — write #tag(x), never #tag x. Without parens, #tag 1 + 2 parses as #tag(1) + 2.
Text.join parameter order — iterator first, separator second:
Text.join(["a", "b", "c"].vals(), ", ") // "a, b, c"
List.get vs List.at: get(n) returns ?T (null if out of bounds). at(n) returns T and traps if out of bounds. Prefer get for safe access.
Reserved by the Motoko grammar — cannot be used as identifiers; using one produces a parse error (e.g. unexpected token 'label'). Rename to a non-reserved word (myLabel, myFunc, kind instead of type, etc.).
actor and assert async await
break case catch class composite
continue debug debug_show do else
false finally flexible for from_candid
func if ignore implicit import
in include label let loop
mixin module not null object
or persistent private public query
return shared stable switch system
throw to_candid transient true try
type var weak while with
async*, await*, and await? are also reserved but contain non-identifier characters, so they can't collide with identifiers.
| Error pattern | Fix |
| ------------------------------------------------------ | ------------------------------------------- |
| should be declared persistent (M0220) | Add --default-persistent-actors or write persistent actor |
| move these declarations into the body (M0141) | Move type/let inside the actor body |
| redundant stable keyword (M0218) | Remove stable; plain var is auto-stable |
| field append does not exist | .concat() |
| field put does not exist (Map) | .add() |
| field delete is deprecated (Map) | .remove() |
| Int cannot produce expected type Nat | Int.abs(intValue) |
| syntax error, unexpected token '.' | #text (searchTerm.toLower()) |
| syntax error, unexpected token ',' | for ((key, value) in map.entries()) |
| Compatibility error [M0170] | Load migrating-motoko or migrating-motoko-enhanced skill |
| shared function has non-shared parameter/return type | Return [T] not List<T>, no var fields |
| send capability required | Add <system> capability |
| field compare does not exist on Time | Use Int.compare |
| unexpected token ';' in function call | Remove ; before ) |
| unbound variable X | import X "mo:core/X" |
| M0098 no best choice for type param | list.map<In, Out>(...) |
| M0096 on contains callback | find(pred) != null |
| M0009 import file does not exist | Relative path, no .mo extension |
| M0072 field X does not exist | Import the mo:core module for that type |
| misplaced '!' (M0064) | Wrap in do ? { ... } |
| pattern does not cover value (M0145) | Add missing cases or case _ |
| unexpected token 'X' where X is a keyword | Rename — X is reserved (see Reserved Keywords) |
// Switch — option unwrapping
let value = switch (map.get(key)) {
case (?v) { v };
case (null) { Runtime.trap("Key not found") };
};
// Switch — variant matching
type Status = { #active; #inactive; #pending : Text };
switch (status) {
case (#active) { "User is active" };
case (#inactive) { "User is inactive" };
case (#pending(reason)) { "Pending: " # reason };
};
// Switch — value matching
switch (statusCode) {
case (200) { "OK" };
case (404) { "Not Found" };
case _ { "Unknown" };
};
// For loops
for ((key, value) in map.entries()) {
Debug.print(key.toText() # ": " # value);
};
for (item in list.values()) {
total += item.score;
};
Prefer .foldLeft() or .map() over imperative loops when possible.
Use break and continue in loops:
for (item in iter) {
if (item.id == targetId) {
result := ?item;
break;
};
};
for (item in list.values()) {
if (not item.isActive) continue;
process(item);
};
Basic Types: Nat Int Text Bool Principal ?T [T] [var T] Blob Float — Time.now() returns Int (nanoseconds)
Common Operations: debug_show(value) → Text | assert condition | # "text" concatenation | break / continue in loops
mo:core, never mo:basestable keyword — enhanced orthogonal persistence handles stateself-parameter functionsswitch + Runtime.trap() on null; ?T only when absence is expected{ self with ... } instead of copying fieldstools
Manage Motoko projects with the mops CLI — toolchain pinning, dependency management, type-checking, building, and linting. Use when working with mops.toml, mops.lock, running mops commands, adding/removing packages, pinning moc or lintoko versions, checking or building canisters, configuring moc flags, or setting up a new Motoko project.
data-ai
Inline actor migration for Motoko canisters using `(with migration = ...)` syntax. Use when upgrading canister state, renaming fields, changing field types, or restructuring actor state without the --enhanced-migration flag. For multi-step migration chains, use migrating-motoko-enhanced instead.
devops
Enhanced multi-step migration for Motoko actors using a migrations/ directory and --enhanced-migration flag. Use when upgrading canister state across multiple deployments, writing migration files, changing actor field types, or managing a migration chain. For a single one-shot migration, use migrating-motoko instead.
tools
Guides use of the icp command-line tool for building and deploying Internet Computer applications. Covers project configuration (icp.yaml), recipes, environments, canister lifecycle, and identity management. Use when building, deploying, or managing any IC project. Use when the user mentions icp, dfx, canister deployment, local network, or project setup. Do NOT use for canister-level programming patterns like access control, inter-canister calls, or stable memory — use domain-specific skills instead.