Guidelines for AI agents (Claude, Codex, Cursor, Copilot, etc.) working in this repository.
ICP skills are live reference documents maintained by DFINITY. Always fetch the relevant skills before making changes — do not rely on cached or training-data versions.
Index: https://skills.internetcomputer.org/.well-known/skills/index.json
Fetch the skill content at its url field. Skills relevant to this repo:
| Task | Skill to fetch |
|---|---|
Any ICP project work, icp.yaml, canister lifecycle |
icp-cli |
| Migrating an example from dfx to icp-cli | icp-cli + its references/dfx-migration.md file |
| Motoko canister code | motoko |
mops.toml, toolchain pinning, moc flags |
mops-cli |
| Internet Identity integration | internet-identity |
| Frontend asset canister | asset-canister |
Skills take precedence over general knowledge when both cover the same topic.
This repo contains canonical ICP examples, each available in both Motoko and Rust. Every example lives under two sibling directories:
motoko/<example_name>/
rust/<example_name>/
Both implement the same Candid interface so readers can compare language implementations side by side.
Reference examples to study first:
hello_world— canonical full-stack example (Motoko backend + Vite frontend); use as the structural template for new exampleswho_am_i— Internet Identity integration; reference for II-authenticated examples
- Always use
icp-clifor all ICP operations. Never usedfx. - CLI docs: https://cli.internetcomputer.org
- ICP developer docs: https://docs.internetcomputer.org
Follow the hello_world layout. New examples and migrations should use this pattern:
<language>/<example_name>/
├── icp.yaml # canister definitions (icp-cli project file)
├── test.sh # executable bash test script
├── README.md
├── package.json # npm workspaces root pointing to frontend/
├── mops.toml # Motoko only
├── Cargo.toml # Rust only (workspace)
├── rust-toolchain.toml # Rust only
├── backend/
│ ├── app.mo or lib.rs # canister entry point
│ └── backend.did # Candid interface (source of truth)
└── frontend/
├── index.html
├── package.json
├── vite.config.js
├── src/
│ ├── actor.js # icp-sdk actor wiring
│ ├── App.jsx
│ └── main.jsx
└── dist/ # build output (gitignored, rebuilt by icp deploy)
Note:
who_am_iusessrc/backend/andsrc/frontend/for historical reasons. New examples and migrations should use the flatbackend//frontend/layout above.
dfx.json— dfx project file, not used with icp-cliBUILD.md— ICP Ninja artifact.dfx/— dfx state directorysrc/bindings/— auto-generated by the bindgen Vite plugin, must be gitignored- Committed
dist/output — built byicp deploy; never commit pre-built assets
.icp/data/— always commit this. It holds canister ID mappings (e.g.local.ids.json) that map canister names to on-chain principals. Losing this means losing the link between the example's code and its deployed canisters.
The root .gitignore already has **/.icp/cache/ which correctly ignores only the ephemeral build cache. Do not add .icp/ to per-example .gitignore files — it would incorrectly hide data/ as well.
When migrating an example, also remove any existing .dfx/ entries from per-example .gitignore files — dfx is no longer used and these entries are dead weight.
networks: # omit if no Internet Identity needed
- name: local
mode: managed
ii: true
canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v5.0.0"
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.2.1"
configuration:
dir: frontend/dist
build:
- npm install --prefix frontend
- npm run build --prefix frontendcanisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
candid: backend/backend.did # omit for backend-only examples (no frontend)
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.2.1"
configuration:
dir: frontend/dist
build:
- npm install --prefix frontend
- npm run build --prefix frontend- With
candid:specified: the recipe reads the committed.didfile and embeds it as WASM metadata (nocandid-extractorneeded). - Without
candid::candid-extractorextracts the interface directly from the compiled WASM. For backend-only examples, omitcandid:and do not commitbackend.did.
Canister names are always backend and frontend. Never use names like <example>_backend, internet_identity_app_backend, etc.
When an example interacts with a well-known external canister whose principal differs by environment (e.g. the ICP ledger mainnet vs. TESTICP on staging), use the environments block to inject the right value per deployment rather than hardcoding it:
canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.3.0"
environments:
- name: local
network: local
settings:
backend:
environment_variables:
ICP_LEDGER_CANISTER_ID: "ryjl3-tyaaa-aaaaa-aaaba-cai"
- name: staging
network: ic
settings:
backend:
environment_variables:
ICP_LEDGER_CANISTER_ID: "xafvr-biaaa-aaaai-aql5q-cai" # TESTICP
- name: production
network: ic
settings:
backend:
environment_variables:
ICP_LEDGER_CANISTER_ID: "ryjl3-tyaaa-aaaaa-aaaba-cai"icp-cli applies these as canister settings at deploy time. Read them at runtime via ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID") (Rust) or Runtime.envVar<system>("ICP_LEDGER_CANISTER_ID") (Motoko) — not via env!() or std::env::var().
When to apply this pattern: judge whether staging/production environments make sense for the specific example. Apply it when the example hardcodes an external canister principal that has a test counterpart (TESTICP, testnet URLs, staging governance canisters, etc.). Skip it for self-contained examples that don't call external canisters.
- Top-level actor: name it after its logical role, not generically — e.g.
actor TodoList,actor CanisterFactory, notactor Backend. - Supporting module files: use PascalCase matching the type they export — e.g.
Counter.moexportingactor class Counter,Types.moexportingtype X. - Entry point file: always
backend/app.moregardless of actor name.
[toolchain]
moc = "1.9.0"
[dependencies]
core = "2.5.0"
[moc]
# M0236: use context dot notation
# M0237: redundant explicit implicit arguments
# M0223: redundant type instantiation
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]
[canisters.backend]
main = "backend/app.mo"
candid = "backend/backend.did" # omit for backend-only examples (no frontend)[canisters.<name>] replaces the main, candid, and args fields that were previously in icp.yaml. The @dfinity/motoko@v5.0.0 recipe reads this section directly, so the Motoko canister needs no configuration: block in icp.yaml — the <name> must match the canister name in icp.yaml.
After writing or editing Motoko source files, always run:
mops check # type-check all canister entry points
mops check --fix # auto-fix style warnings (M0236 dot notation, M0237, M0223)Both commands must pass with no errors before committing.
--default-persistent-actors makes the main actor persistent by default, so the persistent keyword can be omitted on the top-level actor declaration. However, persistent actor class declarations that hold mutable state must still carry the persistent keyword explicitly — the flag does not propagate into actor class sub-WASMs.
Use the mo:ic mops package instead of ic:aaaaa-aa or inline actor("aaaaa-aa") definitions:
[dependencies]
ic = "4.0.0"import { ic } "mo:ic";Breaking change in mo:ic v4.0.0: CanisterSettings gained two new required fields. Always include environment_variables = null and snapshot_visibility = null in settings records passed to ic.create_canister.
Root workspace:
[workspace]
members = ["backend"]
resolver = "2"backend/Cargo.toml:
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
candid = "0.10"
ic-cdk = "0.20"Always use ic-cdk = "0.20" unless a dependency forces a lower version. When using ic-ledger-types, use "0.16" (requires ic-cdk = "^0.19") — version 0.15 pins ic-cdk = "^0.18" which conflicts with 0.20 via an ic-cdk-executor links constraint.
ic_cdk::export_candid!() is required at the end of every Rust canister lib.rs. Without it, candid-extractor cannot find the get_candid_pointer export and the build fails.
CI image: use ghcr.io/dfinity/icp-dev-env-rust:1.0.1 or later. Earlier images bundle candid-extractor 0.1.4 which fails with Error: unknown import: ic0::cost_call for any WASM compiled with ic-cdk ≥ 0.19.
Use ic_cdk::api::env_var_value("VAR_NAME") to read canister environment variables at runtime. These are canister settings applied by icp-cli at deploy time, not WASM metadata. This is the Rust equivalent of Runtime.envVar<system> in Motoko:
fn ledger_principal() -> Principal {
Principal::from_text(ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID"))
.expect("invalid ICP_LEDGER_CANISTER_ID")
}Do not use env!() (compile-time macro, fails if the var is not set during cargo build) or std::env::var() (no OS environment in WASM).
Use the ic-cdk-management-canister crate instead of ic_cdk::api::management_canister (removed in ic-cdk 0.17+):
[dependencies]
ic-cdk-management-canister = "0.1.1"use ic_cdk_management_canister::raw_rand;
#[ic_cdk::update]
async fn get_randomness() -> Vec<u8> {
raw_rand().await.expect("raw_rand failed")
}Every example must have a test.sh bash script that exercises the deployed canister via icp canister call. Write a numbered test for every public function. Include state-assertion tests for mutating operations: call the mutating function, then call a read function and assert the stored value changed.
Using test.sh instead of Makefile avoids Make-specific syntax pitfalls ($$, \\ continuations, @ prefix) and works natively in Git Bash on Windows.
#!/usr/bin/env bash
set -e
echo "=== Test 1: <description of what is tested> ==="
result=$(icp canister call backend <method> '<args>')
echo "$result"
echo "$result" | grep -q '<expected>' && echo "PASS" || (echo "FAIL" && exit 1)
echo "=== Test 2: <mutating operation returns expected value> ==="
result=$(icp canister call backend <mutating_method> '<args>')
echo "$result"
echo "$result" | grep -q '<expected_return>' && echo "PASS" || (echo "FAIL" && exit 1)
echo "=== Test 3: <state persisted after mutation> ==="
result=$(icp canister call backend <read_method> '()')
echo "$result"
echo "$result" | grep -q '<expected_persisted_value>' && echo "PASS" || (echo "FAIL" && exit 1)- Tests must call the
backendcanister by that name. - Always pass explicit Candid args, including
'()'for zero-argument calls — omitting args triggers an interactive prompt that blocks CI. - Use
grep -qto assert on output content. - Number each test (
=== Test N: ... ===) so CI logs are easy to scan. - For examples that create child canisters, capture the returned principal and call the child directly by ID.
- Query functions: always pass
--querytoicp canister callforpublic query funcandpublic composite query funcmethods. - Balance checks: use delta-based assertions (record before, act, assert delta) rather than checking absolute values — this keeps tests idempotent across re-runs regardless of prior state:
before=$(icp canister call backend get_balance '()' | grep -oE '[0-9_]+' | tr -d '_' | head -1)
icp token transfer 1 "$account_hex"
after=$(icp canister call backend get_balance '()' | grep -oE '[0-9_]+' | tr -d '_' | head -1)
delta=$((after - before))
[ "$delta" -eq 100000000 ] && echo "PASS" || (echo "FAIL: expected +100000000 e8s" && exit 1)- Async/time-dependent behavior: if the observable result depends on timers, heartbeats, or polling, use a polling loop:
echo "=== Polling for <condition> (up to 60s) ==="
secs=0
while [ "$secs" -lt 60 ]; do
result=$(icp canister call --query backend <method> '()')
echo "$result"
echo "$result" | grep -q '<expected>' && echo "PASS" && exit 0
sleep 3
secs=$((secs + 3))
done
echo "FAIL: condition not met within 60s"; exit 1- Canister settings: if the example requires non-default canister settings (e.g.
wasm_memory_limit,wasm_memory_threshold), apply them at the top oftest.shbefore the tests:
icp canister settings update backend --<flag> <value> -f- If
icp deploy --cycles 30tis required (see below), documenticp canister top-up --amount 30t backendin the README so users can replenish cycles when needed. (30tis an example amount — adjust to the example's actual consumption.)
A single root devcontainer at .devcontainer/devcontainer.json covers local VS Code usage across all examples. It uses ghcr.io/dfinity/icp-dev-env-all:<version> (Motoko + Rust) with workspaceFolder set to /workspaces/examples. No per-example devcontainer configs exist — do not add them.
Copy .github/workflow-template.yml to .github/workflows/<example_name>.yml and fill in the placeholders. A single workflow file covers both language variants:
name: <example_name>
on:
push:
branches: [master]
pull_request:
paths:
- motoko/<example_name>/**
- rust/<example_name>/**
- .github/workflows/<example_name>.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
motoko-<example_name>:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: motoko/<example_name>
run: |
icp network start -d
icp deploy
bash test.sh
rust-<example_name>:
runs-on: ubuntu-24.04
container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Deploy and test
working-directory: rust/<example_name>
run: |
icp network start -d
icp deploy
bash test.sh- Linux only, no macOS runners.
- No provision scripts — toolchain comes from the container image.
- Always include the
concurrencyblock to cancel superseded runs. - Pin the
actions/checkoutSHA and annotate it with the version tag.
When migrating only one language variant, only include the corresponding job. Do not add a stub for the other language — it will be added when that variant is migrated. Example: migrating motoko/hello_cycles → add only motoko-hello_cycles job; leave rust-hello_cycles for a separate PR.
Deleting old workflows during migration: the repo may contain legacy dfx-based workflow files (e.g. motoko-hello_cycles-example.yaml). When migrating an example, delete the old workflow file for the language being migrated. Do not delete workflow files for the other language:
# Migrating Motoko — delete only the Motoko legacy workflow:
git rm .github/workflows/motoko-<example_name>-example.yaml # or .yml
# Do NOT touch rust-<example_name>-example.ymlEach example's README should follow this structure:
# <Example Title>
<2-3 sentences describing what the example demonstrates>
## Build and deploy from the command line
### Prerequisites
- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`
- ic-mops: `npm install -g ic-mops`
### Install
<git clone + cd>
### Deploy and test
<icp network start -d && icp deploy && bash test.sh && icp network stop>
If the example has a frontend with a Vite dev server: `npm run dev` (hot reload during frontend development)
> If tests fail with an out-of-cycles error, run `icp canister top-up --amount 30t backend` to replenish cycles and retry (`30t` is an example amount). Only relevant for examples that create child canisters.
## Updating the Candid interface
<instructions to regenerate backend.did>
Motoko: `$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo`
Rust: `icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did`
## Security considerations and best practices
<standard disclaimer linking to https://docs.internetcomputer.org/guides/security/overview>- Security best practices URL:
https://docs.internetcomputer.org/guides/security/overview - Each README links to its counterpart in the other language.
- Backend-only examples: omit the
## Updating the Candid interfacesection — there is no frontend consuming the.didfile, so regeneration instructions add no value. - If the original example has a frontend, the migration must include it. Never drop a frontend that existed in the dfx version — check
dfx.jsonfor"type": "assets"canisters. Port the frontend to Vite +@icp-sdk/bindgenfollowing thehello_worldtemplate. If the original frontend contains important educational logic (e.g. certificate verification, custom cryptography), keep that logic intact and only update the canister interaction layer. - Preserve important domain knowledge: the original README may contain non-obvious details about how the example works (memory limits, fee calculations, network-specific behavior, address type explanations, etc.). Read the original README carefully before migrating and carry forward any content that helps a reader understand why the example is structured the way it is — not just how to deploy it. Do not silently drop explanatory paragraphs, even if they reference dfx commands that need updating.
- Broken links: do not copy anchor links from the original README unless you have verified they resolve on the current docs site. When in doubt, link to the top-level page (e.g. the spec index) rather than a specific anchor.
Images are published at ghcr.io/dfinity/icp-dev-env-{motoko,rust,all}. Current pinned version: 1.0.1. All devcontainer configs and CI workflows reference the pinned tag. When a new release is cut, update the tag in:
.devcontainer/devcontainer.json.github/workflows/*.yml
Source: https://github.com/dfinity/icp-dev-env
When migrating an existing example:
Before you start: read the original README, Makefile, and any deploy scripts in full. Note any non-obvious configuration (canister settings, special deploy steps, memory limits, key names, etc.) that must be carried forward.
- Replace
dfx.jsonwithicp.yamlusing the canonical structure above - Rename canisters to
backendandfrontend - Rename
src/<old_name>/tobackend/(orsrc/backend/if keeping thesrc/layout) - Rename
src/<old_frontend>/tofrontend/(orsrc/frontend/) - Rename
.didfile tobackend.did - Update
Cargo.tomlpackage name tobackend(Rust) - Update
Cargo.lockpackage name entry (Rust) - Update workspace member path in root
Cargo.toml(Rust) - Update
vite.config.js: canister name,didFilepath, env var names, remove dfx fallback - Update
actor.js: import path,PUBLIC_CANISTER_ID:backend,CANISTER_ID_BACKEND - Update root
package.jsonworkspace path tofrontend/ - Update
.gitignorebindings path tofrontend/src/bindings/ - Update
mops.tomlto current toolchain versions (Motoko) - Run
mops check --fixin the example directory and commit any auto-fixes (Motoko) - If the example uses the management canister: add
ic = "4.0.0"dependency and replaceic:aaaaa-aa/actor("aaaaa-aa")withimport { ic } "mo:ic"(Motoko) - If the Rust example uses the management canister: add
ic-cdk-management-canister = "0.1.1"dependency and replaceic_cdk::api::management_canisterwith the appropriate function from that crate - Judge whether per-environment configuration makes sense: if the example calls an external canister whose principal differs by environment (e.g. ICP ledger vs. TESTICP), add an
environmentsblock toicp.yaml. Skip for self-contained examples. - If the example creates child canisters: use
icp deploy --cycles 30tin the CI workflow and README - If the example requires non-default canister settings (memory limits, freezing threshold, etc.): apply them via
icp canister settings updateat the top oftest.sh - Delete
dfx.json,BUILD.md,.dfx/,.env(dfx-generated) - Delete
.devcontainer/inside the example folder if one exists (only the repo-root devcontainer is kept)
- Add
test.sh(executable,#!/usr/bin/env bash,set -e) with numbered tests - Use
--queryfor allpublic query funcandpublic composite query funccalls - Use a polling loop for any behavior that depends on timers, heartbeats, or async system hooks
- Use delta-based balance assertions (before/after) rather than absolute values for idempotency
- For backend-only examples, no
## Updating the Candid interfacesection is needed
- Add CI workflow under
.github/workflows/<example_name>.yml - Include only the job(s) for the language being migrated — do not add stubs for the other language
- Delete the old dfx-based workflow for the language being migrated (e.g.
motoko-<name>-example.yaml) - Do not delete workflow files for the other language variant — they will be handled in a separate PR
- Update deploy instructions to use
icp-cli - Preserve important domain-specific content from the original README (memory limits, fee behaviour, address type explanations, network-specific notes, etc.) — update the commands but keep the explanations
- Omit
## Updating the Candid interfacefor backend-only examples (no frontend bindings) - Verify that all links resolve — do not copy anchors from the old README without checking them