---
name: writing-motoko
description: >-
  Motoko language reference, architecture patterns, and dependency tooling
  (mops). Load when writing or modifying backend .mo files.
version: 0.2.6
compatibility:
  toolchain:
    moc: ">=1.11.2"
    mops: "3.x"
  mops:
    core: ">=2.6.0"
caffeineai-subscription: [none]
---

# Writing Motoko

Motoko is an under-represented language for the Internet Computer Protocol, so your pre-training data is likely to be outdated — always favour this skill and its documentation for the most up-to-date information.

## Critical Requirements

**NEVER use these:**

- `stable` keyword -- Not needed in enhanced orthogonal persistence mode
- `mo:base` library -- Deprecated. Use `mo:core` instead
- `.vals()` -- The deprecated `mo:base` iterator name. Always `.values()`. On arrays `.vals()` still compiles, so nothing flags it; on core collections it fails with M0072
- `system func preupgrade/postupgrade` -- Not needed with enhanced orthogonal persistence
- `(with migration = ...)` actor-attached migration syntax -- Use the mops-managed migration chain in `migrations/`
- Inline initializers on stable actor fields -- Initial values come from the migration chain (see `migrating-motoko-actors`)
- Module function style for `self` parameters -- Don't write `List.add(list, item)` or `Map.get(map, key)`
- Manual field-by-field record copying for immutable records -- Use record spread (`{ self with ... }`). For records with `var` fields, do not use record spread; mutate the `var` field directly or rebuild the record explicitly.
- Single-file monolithic actors -- Use the multi-file architecture: types.mo, lib/, mixins/, main.mo
- Stable state in a `mixin` block -- a bare `let`/`var` is silently stable and traps at runtime (`IC0503`). Pass state in as a parameter and keep constants in a module
- Any Motoko reserved keyword as a declared identifier -- Before writing, check parameter, variable, function, type, field, and label names against the full list in [references/reserved-keywords.md](references/reserved-keywords.md). `query` and `label` are reserved and must never be identifiers. Rename a colliding domain term instead of relying on its position or inferred meaning.
- Type annotations on an inline `func` passed as a **call argument** -- write `xs.filter(func x = x > 1)`, not `xs.filter(func(x : Nat) : Bool { x > 1 })`. The call supplies the types. If a generic cannot be inferred, instantiate the call (`map<In, Out>`), never the lambda. This applies only in argument position — named declarations still carry full signatures. **One exception:** keep `: async ()` on an async callback (`func() : async () { ... }`) — it is what makes the body async, and removing it fails with M0096

**ALWAYS use:**

- `mo:core` library version 2.6.0+ (compiler `moc` 1.11.2+)
- Contextual dot notation -- `list.add(item)`, `map.get(key)`
- An import of the key type's module in every file that operates on a `Map`/`Set` -- the implicit `compare` is resolved from the imported module (`import Nat "mo:core/Nat"` for a `Map.Map<Nat, _>`), never from the type alone. A missing key-module import is the usual cause of M0230; a record or variant key needs its own module with a `compare` (see Implicit Parameters)
- Null coalesce `??` for unwrap-or-default and unwrap-or-trap (`opt ?? default`, `opt ?? Runtime.trap(...)`) -- prefer over a two-arm `switch` on `?T` (requires `moc >= 1.7.0`)
- Plain `break` / `continue` to exit or skip a loop iteration -- they work inside `for`, `while`, and `loop` just like in other languages
- Enhanced orthogonal persistence (state persists without `stable` keyword)
- Principled Motoko Architecture -- `types.mo` (types), `lib/` (domain logic), `mixins/` (API endpoints), `main.mo` (composition root, NO public methods)
- **API reference for uncertain APIs**: Use [api-reference.md](api-reference.md) to verify exact method signatures when you are about to use an unfamiliar `mo:core` API or when a compile diagnostic points at an API mismatch. It lists only non-deprecated APIs — a symbol that is not there should not be written. Do NOT guess API shapes — a targeted lookup of a symbol you are unsure about is always worth the step; skipping it to save steps ships hallucinated APIs and costs far more in compile repair.

**When encountering compilation errors:** Re-check [api-reference.md](api-reference.md) for exact method signatures.

**Before changing actor state shape, introducing new stable fields, or upgrading canisters:** load `migrating-motoko-actors`. This guidance assumes the **mops-managed migration chain** — when a change requires a migration, it goes in a NEW file in `src/backend/migrations/`. Introducing stable state for the first time always needs one (no inline initializers); trivial stable-compatible upgrades do not. See the skill. If a migration or compatibility diagnostic still does not match what the source says, or a migration file cannot be written, load `troubleshooting-motoko-migrations`.

## Toolchain (mops)

All configuration is in `mops.toml`. Only consult https://docs.mops.one/ if you encounter an unfamiliar mops error.

### Dependency management

- Never hand-edit dependency entries in `mops.toml`, and never touch `mops.lock`; use the `mops` CLI so dependency metadata and the lockfile stay atomic. (`[toolchain]` has a CLI too: `mops toolchain use <tool> [version]`.)
- Leave `[moc] args` alone. Compiler flags are a one-time project-setup concern, and many platforms own `mops.toml` and set them for you — do not inspect or change them while writing code. If you are setting up a project yourself, see [references/project-setup.md](references/project-setup.md).
- `mops add <pkg>` installs and exact-pins a published package. Use `@x.y.z` for a specific version, `<url>[#ref]` for GitHub, `./path` for a local package, and `--dev` for development dependencies.
- `mops add` accepts exactly one package name. To install several packages, chain one-package commands with `&&`; never run multiple `mops add` invocations in parallel — they race on `mops.toml` and `mops.lock`.
- `mops update [pkg]` updates a package and rewrites its exact pin.
- `mops sync` reconciles imports after bulk `.mo` changes by adding missing dependencies and removing unused ones.
- `mops.lock` is rewritten only by `mops add`, `mops update`, `mops sync`, `mops install`, and related supported mops commands.
- On a lock or integrity failure, run `mops install` — it regenerates a `mops.lock` that is missing, stale, or inconsistent with `mops.toml`. `mops verify` reports a file-hash mismatch it cannot repair; `mops cache clean` forces a verified re-download. Never `chmod`, remove, or text-edit `mops.lock`.

### Check and build

- **`mops install`** — Install dependencies and reconcile `mops.lock`, regenerating it when it is missing or no longer matches `mops.toml`.
- **`mops check --fix`** (fast — use for iteration) — Reports compile errors and auto-fixes the style warnings, where the project has them enabled (dot-notation, redundant type instantiation, redundant implicit arguments). Follow this skill's rules whether or not `--fix` enforces them. Exit 0 = success. Error format: `file:startLine.startCol-endLine.endCol: severity [code], message`. Iterate on this until it passes.
- **`mops build`** (slow — run ONCE at the end) — Produces the compiled `.wasm` and the candid interface file `.did`. Use only as final verification after `mops check --fix` passes; never put `mops build` inside the fix loop. The `.did` file drives generated client bindings — never edit it manually.

If `mops check --fix` fails: read stderr first. Do NOT call `moc` directly. Fix `.mo` source and rerun the check.

## Modern Motoko Features

### Contextual Dot Notation

**RULE:** When a function has a `self` parameter, ALWAYS use dot notation.
Dot notation is still type-specific: it only applies to APIs that the value's
module actually defines — verify against [api-reference.md](api-reference.md)
rather than inferring JavaScript-style helpers. `.some(...)` and `.every(...)`
do not exist in Motoko; the `mo:core` names are `.any(...)` and `.all(...)`.

The module a dot call resolves against is the module that **defines** the
function — usually the receiver's type's module (`map.get`, `list.add`), but
sometimes a different one: `arr.values().toList()` resolves against `List` (the
target), not the array's module. The import needed (if any) is the defining
module's, not the receiver's.

```motoko
map.get(key);
list.add(item);
array.filter(func x = x > 0); // CORRECT
Map.get(map, key);
List.add(list, item); // WRONG (M0236)

// Chaining
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);

```

Conversions are receiver calls too, but the `toX` functions are split across
modules: some are defined on the **source** module (`Nat8.toNat`), others on the
**target** (`"42".toNat()` and `"-5".toInt()` are defined by `Nat` and `Int`,
not `Text`). So `Text`-receiver conversions need the target imported (`import
Nat "mo:core/Nat"`, `import Int "mo:core/Int"`); without it, moc reports
M0070/M0072 and the call does not compile:

```motoko project=dot-notation filepath=src/backend/main.mo
import Principal "mo:core/Principal";
import Nat "mo:core/Nat";
import Text "mo:core/Text";

func conversions(caller : Principal, myNat : Nat) {
  ignore caller.toText();            // CORRECT
  ignore myNat.toText();             // CORRECT
  ignore "42".toNat();               // CORRECT (defined by Nat, imported above)
  ignore "hello".concat(" world");   // CORRECT
  ignore Principal.toText(caller);   // WRONG (M0236)
  ignore Nat.toText(myNat);          // WRONG (M0236)
};
```

**`equal` / `compare` vs `==`.** Collections take `equal` and `compare` as implicit arguments, so those are the functions to write for your own records and variants. `==` is compiler-generated structural equality and exists only for **shared** types — one `var` field takes a record out of shared and `==` stops compiling (M0060) — so do not build record comparisons on it. Comparing primitives and shared fields directly with `==` is fine, and on `Nat`, `Int`, `Float`, and the sized int types it is the only form: those declare `equal(x, y)` without a `self` parameter, so `myNat.equal(other)` fails with M0070. Other receiver methods on those types (`myNat.toText()`) are fine.

Your own records and variants get nothing derived — a record `compare` must be an explicit function, and custom variants need both `equal` and `compare` written out. See [references/equality.md](references/equality.md).

### Mixins

Composable actor services with granular state injection. Each mixin lives in its own file as a top-level `mixin` block:

```motoko project=mixin filepath=src/backend/types.mo
module {
  public type User = {
    principal : Principal;
    username : Text;
  };
};
```

```motoko project=mixin filepath=src/backend/mixins/Auth.mo
import List "mo:core/List";
import Principal "mo:core/Principal";
import Types "../types";

mixin (users : List.List<Types.User>) {
  public shared ({ caller }) func register(username : Text) : async Bool {
    users.add({ principal = caller; username });
    true
  };

  public query func listUsers() : async [Types.User] {
    users.toArray()
  };
};
```

```motoko project=mixin filepath=src/backend/main.mo
import List "mo:core/List";
import Types "types";
import AuthMixin "mixins/Auth";

actor {
  let users : List.List<Types.User>;
  include AuthMixin(users);
};
```

**Mixin Anti-Patterns — NEVER generate these:**

```motoko
// WRONG — mixin is NOT a function inside a module; wrapping in module {} is invalid
module {
  public func createMixin(state : ...) : actor { ... } { // M0001: unexpected token 'actor'
    actor { public func foo() { ... }; };
  };
}

// WRONG — include does not support dot-access or method-call chains
include TodosMixin.createMixin(state); // M0001: unexpected token '.'

// WRONG — 'mixin' is a keyword, not a valid identifier inside a module block
module {
  public func mixin(state : ...) { ... }; // M0001: unexpected token 'mixin'
}
```

Rules:
- A mixin file contains a bare `mixin (params) { ... };` block at the **top level** — not inside `module {}`, not returned from a function.
- `include` takes a bare name followed by arguments: `include MixinName(args)` — no dot-access, no chained calls.

**No stable state in Mixins.** Every top-level `let`/`var` in a `mixin` is implicitly **stable**. Only `transient` is ever allowed, but prefer putting static definitions (like literals) into modules instead!

**Sharing state between mixins — pass it as a parameter.** To share state between two or more mixins, declare that state once as an actor field and pass that same binding to each `include`. Every mixin that gets it reads and writes the same value. A mixin can take several parameters, so it can receive shared state plus its own private state.

```motoko
// types.mo:  public type GoogleState = { var connection : ?Conn; var config : ?Cfg };
let google : Types.GoogleState;          // declared once; initialized in the migration function
let bookings : Map.Map<Nat, Booking>;    // BookingsApi's own state
include GoogleApi(google);               // gets `google`
include BookingsApi(google, bookings);   // gets the SAME `google`, plus its own bookings
```

Pass the same binding to each mixin. Never build a new record at the `include` — that gives each mixin its own separate copy, so one mixin's writes never reach the others:

```motoko
include GoogleApi({ var connection = google.connection });   // WRONG: NEVER DO THIS!
```

### Null Coalesce (`??`)

Prefer `??` over a two-arm `switch` that only unwraps an option or supplies a default / trap. Requires `moc >= 1.7.0`.

```motoko
// Default when absent
let name = optName ?? "anonymous";

// Fail-fast unwrap — null means a bug / missing invariant
let user = users.find(func u = u.id == caller)
  ?? Runtime.trap("User not found");

// Nested options — chain instead of nested switches
let start = event.start.dateTime ?? event.start.date ?? "";

// RHS is lazy; may be a block. Bare record literals need extra braces/parens:
let n = opt ?? { let x = 1; x };
let rec = opt ?? ({ x = 0 });
```

**Use `switch` instead** when the `?v` arm transforms the value, runs side effects, or you are matching variants / multiple cases — `??` only unwraps or substitutes.

```motoko
// Keep switch: Some arm transforms / branches on the inner value
switch (users.get(caller)) {
  case (?u) { u.isAdmin };
  case null { false };
};

switch (result) {
  case (#ok value) { value };
  case (#err e) { Runtime.trap(e) };
};
```

See [references/control-flow.md](references/control-flow.md).

### Implicit Parameters

`Map` and `Set` operations take the comparison function as an **implicit** argument. `Map.empty()` itself takes no arguments — the comparator is resolved at the operations that need it (`add`, `get`, `remove`, …), not at construction.

Inference works by finding a `compare` in the module imported **for the key type**. So the import is what makes it work:

```motoko
import Map "mo:core/Map";
import Nat "mo:core/Nat"; // this import is what supplies Nat.compare

let map = Map.empty<Nat, Text>();
map.add(5, "hello"); // compare resolved from the imported Nat
```

Without `import Nat`, the same code fails — the type is known, but there is no module to take `compare` from:

```text
type error [M0230], Cannot determine implicit argument `compare` of type (Nat, Nat) -> Order
note: Did you mean to import mo:core/Int or mo:core/Nat?
```

Do **not** pass the comparator explicitly when it can be inferred; that is M0237, which `mops check --fix` removes:

```motoko
let ages = Map.empty<Text, Nat>();   // Text.compare resolved at add, from the imported Text
ages.add("Alice", 30);               // CORRECT
ages.add(Text.compare, "Alice", 30); // WRONG (M0237)
```

A custom key type works the same way — give its module a `compare` and it is inferred:

```motoko
type Point = { x : Nat; y : Nat };
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
```

Type instantiation on `empty()` follows the usual rule — needed only when the binding is unannotated. `let m : Map.Map<Nat, Text> = Map.empty();` infers it, and `Map.empty<Nat, Text>()` there would be M0223.

## Architecture Pattern

```text
backend/
├── types.mo         # Central schema, state definitions
├── lib/             # Domain logic (stateless modules with self pattern)
├── mixins/          # Service layer (stateless, state injected via parameters)
├── types/           # Type definitions for mixins and lib modules
├── migrations/      # Mops-managed migration chain. See migrating-motoko-actors.
│                    #   Each file is YYYYMMDD_HHMMSS.mo (a UTC timestamp, not a feature name); files predating this build are FROZEN.
└── main.mo          # Composition root (state owner, NO public methods)
```

## Import Path Conventions

Paths are **relative to the importing file**. No `.mo` extension, no `/lib.mo` suffix.

```motoko
// From main.mo
import Types "types";
import AuthMixin "mixins/Auth";
import UserLib "lib/User";
// From lib/*.mo or mixins/*.mo
import Types "../types";
import UserLib "../lib/User";
// 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";

```

**Migration files** (`migrations/*.mo`) must be self-contained — they may only import from `mo:core/...`, never from `../types` or any project module. See `migrating-motoko-actors` for the full rules.

### The Actor Must Come Last

Imports and `type`/`let` declarations may precede the actor. Nothing may follow it — the actor is the file's result, so a trailing declaration makes the actor a non-`()` statement and fails with M0096 (`expression of type actor {...} cannot produce expected type ()`). Prefer keeping shared types in `types.mo` regardless.

### Import Hygiene

Add an import only to the file that uses the imported identifier. `Time.now()` usually belongs in a domain `lib/*.mo` implementation file, so `import Time "mo:core/Time";` belongs in that file, not `main.mo`, unless `main.mo` itself calls `Time.now()`. Every capitalized namespace call must have a matching import in the same file: if a mixin calls `TodosLib.listTodos(...)`, the file must import `TodosLib "../lib/todos"` (or use the alias it actually imported). Treat unused-import warnings as failures: remove stale `Debug`, `Time`, or helper-module imports before finishing.

## Query Functions

`query` marks a public function as a read-only call: it executes fast and unreplicated, and **every state change it makes is silently discarded when the call completes** — a write inside a query compiles, runs, and vanishes with no error or warning. Declare pure reads as `query func`; any function that must persist a change is a plain update func (no `query`). `public query func` is shorthand for `public shared query func`; both forms take `({ caller })` the same way.

```motoko
public query func getPosts() : async [Types.PostView] { ... };          // read: query
public shared ({ caller }) func addPost(t : Text) : async Nat { ... };  // persists: update
public query func resetAll() : async () { posts.clear() };              // WRONG: compiles, but the clear is discarded
```

### When await is allowed

A plain `query func` cannot call any other canister function, whether that callee is a query or an update. Writing `await someCall()` inside one fails with a paired `M0038` (misplaced await) + `M0188` (send capability required: "cannot call a `shared` function from a `query` function"), always on the same statement. Pick the function kind by what the body must call:

| Body must call                          | Declare                          