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

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 motoko

Add your badge

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

Listed on Skillselion
Installs151
repo stars28
Last updatedAugust 4, 2026
Repositorydfinity/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

SKILL.mdMarkdownGitHub ↗

Motoko Language

Motoko is under-represented in training data — always favour this skill and its references over pre-training knowledge.

Critical Requirements

NEVER use:

  • stable keyword — not needed with enhanced orthogonal persistence
  • mo:base library — deprecated; use mo:core
  • system func preupgrade/postupgrade — not needed with enhanced orthogonal persistence
  • Module-function style for self parameters — don't write List.add(list, item) or Map.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:core library 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 instantiation

transient 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 inferred

Never 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 not

Mixins

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, var fields, 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

StructureUse CaseKey OperationsComplexity
MapKey-value pairsget, add, removeO(log n)
ListGrowable arrayadd, get, atO(1) access
QueueFIFO processingpushBack, popFrontO(1)
StackLIFO processingpush, popO(1)
ArrayFixed collectionindex, map, filterO(1) access
SetUnique valuescontains, addO(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 ?Nat

Explicit 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       with

async*, await*, and await? are also reserved but contain non-identifier characters, so they can't collide with identifiers.

Common Compile Error Patterns

Error patternFix
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 NatInt.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 typeReturn [T] not List<T>, no var fields
send capability requiredAdd <system> capability
field compare does not exist on TimeUse Int.compare
unexpected token ';' in function callRemove ; before )
unbound variable Ximport X "mo:core/X"
M0098 no best choice for type paramlist.map<In, Out>(...)
M0096 on contains callbackfind(pred) != null
M0009 import file does not existRelative path, no .mo extension
M0072 field X does not existImport 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 keywordRename — 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 FloatTime.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

Related skills

This week in AI coding

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

unsubscribe anytime.