
Motoko
- 151 installs
- 28 repo stars
- Updated August 4, 2026
- dfinity/icskills
Write, test, and upgrade Motoko canister smart contracts on the Internet Computer with idiomatic types, actors, and stable data patterns.
About
Provides Motoko guidance for Internet Computer canister development: actors, shared functions, persistent state, upgrades, and Candid interfaces. Supports building reliable on-chain backends with idiomatic Motoko patterns for ICP dapps.
- Motoko actor and type patterns
- Canister upgrade strategies
- Inter-canister call syntax
- Stable variable usage
- ICP-specific testing workflows
Motoko by the numbers
- 151 all-time installs (skills.sh)
- Ranked #124 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 motokoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 151 |
|---|---|
| repo stars | ★ 28 |
| Last updated | August 4, 2026 |
| Repository | dfinity/icskills ↗ |
What it does
Write, test, and upgrade Motoko canister smart contracts on the Internet Computer with idiomatic types, actors, and stable data patterns.
Files
Motoko Language
Motoko is under-represented in training data — always favour this skill and its references over pre-training knowledge.
Critical Requirements
NEVER use:
stablekeyword — not needed with enhanced orthogonal persistencemo:baselibrary — deprecated; usemo:coresystem func preupgrade/postupgrade— not needed with enhanced orthogonal persistence- Module-function style for
selfparameters — don't writeList.add(list, item)orMap.get(map, key) - Manual field-by-field record copying — use record spread (
{ self with ... }) - Single-file monolithic actors — use multi-file architecture
ALWAYS use:
mo:corelibrary version 2.0.0+- Contextual dot notation —
list.add(item),map.get(key) - Enhanced orthogonal persistence (state persists without
stable) - Principled architecture —
types.mo,lib/,mixins/,main.mo
For 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.
Compiler Flags
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 instantiationtransient for ephemeral state
Mark 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.
Modern Motoko Features
Contextual Dot Notation
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);Lambda Argument Types
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
});Implicit Parameters
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 inferredNever pass implicit arguments explicitly when the compiler derives them:
m.add(1, "hello"); // ✓
Map.add(m, Nat.compare, 1, "hello"); // ✗Equality and Comparison
== 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 notMixins
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);Record Spread
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.
Architecture Pattern
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);
};Import Path Conventions
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";Shared Types
Public functions accept/return only shared types (serializable):
- Shared:
Nat,Int,Text,Bool,Principal,Blob,Float,[T],?T, records, variants - Not shared: functions,
varfields, objects,Map,Set,List,Queue,Stack
Convert 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) };
};Collections
| 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.
Iteration
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 find
- `contains(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 ?NatExplicit Type Instantiation
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.
Option Handling
// 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 });
};Module with Self Pattern
// 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();Security
Every public update function MUST verify the caller via {caller} destructuring. Enforce authorization on the backend.
Function Literals as Arguments
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 ';'Pitfalls
1. 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
};2. Always parenthesize variant tag arguments — write #tag(x), never #tag x. Without parens, #tag 1 + 2 parses as #tag(1) + 2.
3. `Text.join` parameter order — iterator first, separator second:
Text.join(["a", "b", "c"].vals(), ", ") // "a, b, c"4. `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 Keywords
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 withasync*, await*, and await? are also reserved but contain non-identifier characters, so they can't collide with identifiers.
Common Compile Error Patterns
| 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) |
Control Flow
// 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);
};Quick Reference
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
Best Practices
1. Always mo:core, never mo:base 2. No stable keyword — enhanced orthogonal persistence handles state 3. Dot notation for all self-parameter functions 4. Never annotate lambda argument types — let the compiler infer 5. Never pass implicit arguments explicitly 6. Unwrap with switch + Runtime.trap() on null; ?T only when absence is expected 7. types.mo / lib/ / mixins/ / main.mo structure 8. Mixins receive only needed state slices 9. Queries for read-only, updates for state changes 10. Iterator chaining to avoid intermediate collections 11. Record spread { self with ... } instead of copying fields
Additional References
- API docs: mops.one/core/docs — authoritative mo:core function signatures and documentation
- Working examples: references/examples.md — full actors, multi-file architecture, timers
Motoko Examples
Complete working examples demonstrating modern Motoko patterns. All examples verified with moc 1.5.0.
Heads-up — enhanced migration: the actor examples below declare fields with initializers (let users = List.empty(); var nextId = 0;). Under--enhanced-migration, actor fields cannot have initializers — declare them asvar nextId : Nat;and set initial values in the migration file that introduces them. See themigrating-motoko-enhancedskill.
Principled Architecture
types.mo
module {
public type UserId = Principal;
public type User = {
id : UserId;
var username : Text;
var bio : Text;
var isActive : Bool;
};
public type UserPublic = {
id : UserId;
username : Text;
bio : Text;
isActive : Bool;
};
public type Post = {
id : Nat;
author : User;
var title : Text;
var content : Text;
var published : Bool;
};
public type PostPublic = {
id : Nat;
authorId : Principal;
title : Text;
content : Text;
published : Bool;
};
};lib/User.mo
import Types "../types";
module {
public type User = Types.User;
public func new(id : Types.UserId, username : Text) : User {
{ id; var username; var bio = ""; var isActive = true };
};
public func updateBio(self : User, newBio : Text) {
if (newBio.size() > 280) return;
self.bio := newBio;
};
public func ban(self : User) { self.isActive := false };
public func isValid(self : User) : Bool {
self.username.size() > 0 and self.isActive;
};
public func toPublic(self : User) : Types.UserPublic {
{ id = self.id; username = self.username; bio = self.bio; isActive = self.isActive };
};
};lib/Post.mo
import Types "../types";
module {
public type Post = Types.Post;
public func new(id : Nat, author : Types.User, title : Text) : Post {
{ id; author; var title; var content = ""; var published = false };
};
public func publish(self : Post) {
if (self.content.size() > 0) { self.published := true };
};
public func setContent(self : Post, content : Text) {
self.content := content;
};
};mixins/Auth.mo
import Types "../types";
import UserLib "../lib/User";
import List "mo:core/List";
mixin (users : List.List<Types.User>) {
func findUser(p : Principal) : ?Types.User {
users.find(func(u) { u.id == p });
};
public shared ({ caller }) func register(username : Text) : async Bool {
switch (findUser(caller)) {
case (?_) return false;
case (null) {
users.add(UserLib.new(caller, username));
return true;
};
};
};
public shared query ({ caller }) func getProfile() : async ?Types.UserPublic {
switch (findUser(caller)) {
case (?user) { ?user.toPublic() };
case (null) { null };
};
};
public shared ({ caller }) func updateBio(newBio : Text) : async Bool {
switch (findUser(caller)) {
case (null) false;
case (?user) { user.updateBio(newBio); true };
};
};
};mixins/Blog.mo
import Types "../types";
import PostLib "../lib/Post";
import List "mo:core/List";
import Runtime "mo:core/Runtime";
mixin (
users : List.List<Types.User>,
posts : List.List<Types.Post>,
) {
public shared ({ caller }) func createPost(title : Text) : async Nat {
let author = switch (users.find(func(u) { u.id == caller })) {
case (?u) u;
case (null) { Runtime.trap("User not registered") };
};
let pid = posts.size();
posts.add(PostLib.new(pid, author, title));
pid;
};
public shared ({ caller }) func publishPost(postId : Nat) : async Bool {
switch (posts.find(func(p) { p.id == postId })) {
case (null) false;
case (?post) {
if (post.author.id != caller) { return false };
post.publish();
true;
};
};
};
public query func getAllPosts() : async [Types.PostPublic] {
posts.map<Types.Post, Types.PostPublic>(
func(p) { { id = p.id; authorId = p.author.id; title = p.title; content = p.content; published = p.published } }
).toArray();
};
};main.mo
import List "mo:core/List";
import Types "types";
import AuthMixin "mixins/Auth";
import BlogMixin "mixins/Blog";
actor Main {
let users = List.empty<Types.User>();
let posts = List.empty<Types.Post>();
include AuthMixin(users);
include BlogMixin(users, posts);
};Iterator Chaining
Note: import Array enables .find(), .any(), .all() on arrays; import Iter enables .map(), .filter() on iterators; import Bool enables .toText() on booleans; import Nat enables .toText() on natural numbers.
import Array "mo:core/Array";
import Bool "mo:core/Bool";
import Iter "mo:core/Iter";
import Nat "mo:core/Nat";
actor {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
public query func demonstrateIterators() : async Text {
var output = "";
let doubled = numbers.values().map(func x = x * 2).filter(func x = x > 10).toArray();
output := output # "Doubled > 10: " # doubled.toText() # "\n";
let sum = numbers.values().foldLeft(0, func(acc, x) = acc + x);
output := output # "Sum: " # sum.toText() # "\n";
switch (numbers.find(func x = x > 5)) {
case (?found) { output := output # "Found: " # found.toText() # "\n" };
case (null) {};
};
let hasLarge = numbers.any(func x = x > 8);
let allPositive = numbers.all(func x = x > 0);
output := output # "Has large: " # hasLarge.toText() # "\n";
output := output # "All positive: " # allPositive.toText() # "\n";
output;
};
};Map with Custom Key Types
import Map "mo:core/Map";
import Order "mo:core/Order";
import Int "mo:core/Int";
actor {
type Point = { x : Int; y : Int };
module Point {
public func compare(a : Point, b : Point) : Order.Order {
switch (Int.compare(a.x, b.x)) {
case (#equal) { Int.compare(a.y, b.y) };
case (other) { other };
};
};
};
let pointMap = Map.empty<Point, Text>();
public func addPoint(x : Int, y : Int, pointLabel : Text) : async () {
pointMap.add({ x; y }, pointLabel);
};
public query func getPoint(x : Int, y : Int) : async ?Text {
pointMap.get({ x; y });
};
};Timer with Periodic Cleanup
import Timer "mo:core/Timer";
import Time "mo:core/Time";
import List "mo:core/List";
actor {
let logs = List.empty<(Int, Text)>();
var timerId : Nat = 0;
public func startCleanup() : async () {
timerId := Timer.recurringTimer<system>(
#seconds(3600),
func() : async () {
let oneHourAgo = Time.now() - 3_600_000_000_000;
let recent = logs.filter(func(timestamp, _) { timestamp > oneHourAgo });
logs.clear();
logs.addAll(recent.values());
},
);
};
public func stopCleanup() : async () {
Timer.cancelTimer(timerId);
};
};Shared Type Boundary
import List "mo:core/List";
import Principal "mo:core/Principal";
import Set "mo:core/Set";
import Time "mo:core/Time";
actor {
type PhotoInternal = {
id : Nat;
url : Text;
uploadedBy : Principal;
likedBy : Set.Set<Principal>;
createdAt : Int;
};
type Photo = {
id : Nat;
url : Text;
uploadedBy : Text;
likedBy : [Principal];
createdAt : Int;
};
let photos = List.empty<PhotoInternal>();
func toPublic(self : PhotoInternal) : Photo {
{
self with
uploadedBy = self.uploadedBy.toText();
likedBy = Set.toArray(self.likedBy);
};
};
public shared ({ caller }) func upload(url : Text) : async Nat {
let id = photos.size();
photos.add({
id;
url;
uploadedBy = caller;
likedBy = Set.empty<Principal>();
createdAt = Time.now();
});
id;
};
public query func getPhotos() : async [Photo] {
photos.map<PhotoInternal, Photo>(func(p) { toPublic(p) }).toArray();
};
};In-Place Mutation Patterns
Use find + direct field mutation for updating a single item. Use mapInPlace when transforming all items:
import List "mo:core/List";
actor {
type Todo = { id : Nat; text : Text; var completed : Bool };
let todos = List.empty<Todo>();
var nextId : Nat = 0;
public func addTodo(text : Text) : async Nat {
let id = nextId;
nextId += 1;
todos.add({ id; text; var completed = false });
id;
};
public func toggleTodo(targetId : Nat) : async Bool {
switch (todos.find(func(t) { t.id == targetId })) {
case (?todo) { todo.completed := not todo.completed; true };
case (null) false;
};
};
public func completeAll() : async () {
todos.mapInPlace(func(todo) { { todo with var completed = true } });
};
};Type Conversions
Requires import Nat "mo:core/Nat", import Int "mo:core/Int", etc. for dot-notation methods.
// Nat ↔ Int
let n : Nat = 42;
let i : Int = n.toInt();
let backToNat = Int.abs(i);
// Nat size widening: Nat8 → Nat16 → Nat32 → Nat64
let nat8 : Nat8 = 255;
let nat16 = nat8.toNat16();
let nat32 = nat16.toNat32();
let nat64 = nat32.toNat64();
let backToNat8 = Nat8.fromNat64(nat64);
// Int size widening: Int8 → Int16 → Int32 → Int64
let int8 : Int8 = -128;
let int16 = int8.toInt16();
let int32 = int16.toInt32();
let int64 = int32.toInt64();
let backToInt8 = Int8.fromInt64(int64);
// To/from Text
let text = n.toText(); // "42"
let maybeNat = Nat.fromText("42"); // : ?Nat
let maybeInt = Int.fromText("-5"); // : ?Int
// To Float
let f = n.toFloat();
// Time is Int (nanoseconds)
let timestamp = Time.now(); // requires import Time "mo:core/Time"
let milliseconds = timestamp / 1_000_000;