Migrating from dfx to icp-cli
dfx is deprecated. Its successor is icp-cli (icp), and icpp-pro 5.5.0 and higher
use icp-cli exclusively. Use 5.5.1 or later - see the note on query methods below.
This page is for developers with an existing icpp-pro project built against dfx. If you are starting fresh, follow Installation and Getting Started instead — they already use icp-cli.
Scope
This page covers what changes in an icpp-pro project. For icp-cli itself — the full command reference, recipes, environments — see the official documentation at cli.internetcomputer.org and the dfinity/icskills agent skills, which stay current as icp-cli evolves.
Behaviour below was verified against icp-cli 1.2.0.
The short version
| dfx | icp-cli | |
|---|---|---|
| project file | dfx.json |
icp.yaml |
| canister IDs | canister_ids.json |
.icp/data/mappings/<env>.ids.json |
| target selection — CLI | --network local\|ic |
--environment local\|ic (-e) |
| target selection — pytest | --network local\|ic |
--network local\|ic — unchanged, see below |
| local network | one global replica | one per project |
| JS bindings | dfx generate |
didc bind (optional) |
| smoketest arg | dfx_json_path= |
icp_yaml_path= |
-e vs --network — two different programs
Both flags survive the migration, but they belong to different tools:
icpCLI → always-e/--environment, naming an entry underenvironments:inicp.yaml. This guide uses-eeverywhere.pytest→ still--network, spelled exactly as with dfx. The flag name did not change, but its meaning did: it now names an environment inicp.yaml.
So pytest --network=local and icp canister call … -e local select the same target.
(Some icp commands also accept -n / --network, which takes a network name or a
raw URL and conflicts with -e. You do not need it for this migration — stick to -e.)
Your Python test code barely changes: every icpp.smoketest function kept its name and
signature. The two things that do change are the Candid text your assertions compare against,
and where your canister IDs live.
1. Install icp-cli & create an identity
npm install -g @icp-sdk/icp-cli
icp --version # must be >= 1.2.0
Unlike dfx, icp-cli does not create a default identity for you, and it keeps identities
in a separate keyring — your dfx identities are not visible to icp:
# create a fresh one
icp identity new default --storage plaintext
icp identity default default
# or bring an existing dfx identity across
dfx identity export my-identity > /tmp/my-identity.pem
icp identity import my-identity --from-pem /tmp/my-identity.pem
rm /tmp/my-identity.pem
Prefer icp <cmd> --identity <name> over switching the global default — it applies to that
one command, so there is nothing to reset afterwards.
2. Replace dfx.json with icp.yaml
An icpp-pro canister is a pre-built wasm: icpp build-wasm produces it, and icp only has
to pick it up. The Candid interface travels inside the wasm, in the icp:public
candid:service custom section that icpp build-wasm writes — so no separate .did entry is
needed.
# icp.yaml (replaces dfx.json)
canisters:
- name: my_canister
build:
steps:
- type: pre-built
path: build/my_canister.wasm
networks:
- name: local
mode: managed
gateway:
port: 0 # let the OS pick a free port — see section 5
environments:
# these names are what you pass as `icp ... -e local|ic`
# and as `pytest --network=local|ic` (yes, pytest still spells it --network)
- name: local
network: local
- name: ic
network: ic
Check it parses:
icp project show
Then delete dfx.json — but not before doing section 3.
3. Migrate your canister IDs — do this before deleting anything
This step prevents orphaning a live canister
icp-cli does not read canister_ids.json. If you delete it without migrating, the
next icp deploy -e ic creates a brand-new canister instead of upgrading your
existing one — leaving your deployed canister orphaned and its cycles stranded.
Convert each entry. Note canister_ids.json is keyed canister → network, while icp's store
is one file per environment, keyed canister → id:
// old: canister_ids.json
{ "my_canister": { "ic": "aaaaa-bbbbb-ccccc-ddddd-cai" } }
// new: .icp/data/mappings/ic.ids.json
{ "my_canister": "aaaaa-bbbbb-ccccc-ddddd-cai" }
mkdir -p .icp/data/mappings
# ...write the file as above...
rm -f canister_ids.json
git add .icp/data/ # this MUST be committed
Verify by calling the canister by name — that proves the mapping resolves:
icp canister status my_canister -e ic --id-only # prints the id, works offline
icp canister call my_canister <a_query_method> '()' -e ic --query
4. .gitignore
Only the cache is disposable. .icp/data/ holds the mainnet canister-ID mappings you
just created and must be committed:
# icp-cli
**/.icp/cache/
Use **/, not .icp/cache/
A gitignore pattern containing a slash is anchored to the directory of the .gitignore
file. If your canisters live in subdirectories (canisters/foo/, test/canisters/…),
a bare .icp/cache/ at the repo root silently matches nothing, and every canister's
local replica state gets committed.
Confirm both directions:
# must match
git check-ignore -v path/to/canister/.icp/cache/networks
# must NOT match
git check-ignore -v path/to/canister/.icp/data/mappings/ic.ids.json
Never gitignore .icp or .icp/data wholesale.
Never rm -rf .icp - only .icp/cache
.icp/data/mappings/ holds your mainnet canister ids. Deleting it orphans the deployed
canisters on the next icp deploy. This is easy to do by accident: a cleanup like
# WRONG - takes data/ with it
find . -name '.icp' -type d -exec rm -rf {} +
removes the whole tree. Clean only the cache:
# safe
rm -rf .icp/cache
5. The local network
icp-cli runs one local network per project, so projects no longer share a single global replica.
icp network start --background
icp deploy --environment local --yes
icp network stop
Set gateway.port: 0 (as in the icp.yaml above). The gateway otherwise defaults to a
fixed port 8000, so two projects still collide on it. With port: 0 the OS assigns a free
ephemeral port and projects — or several agents — can run side by side.
The consequence is that the port changes on every start. Never hardcode it; read it back:
icp network status --environment local --json # -> .gateway_url / .api_url
There is no --clean flag. A managed network keeps its replica state and its
canister-ID mappings under .icp/cache, so a clean start is:
icp network stop && rm -rf .icp/cache && icp network start --background
6. Update your tests
icpp.smoketest and icpp.conftest_base kept every function name, signature and fixture, so
most test files need only a path change:
# before
DFX_JSON_PATH = Path(__file__).parent / "../dfx.json"
response = call_canister_api(dfx_json_path=DFX_JSON_PATH, ...)
# after
ICP_YAML_PATH = Path(__file__).parent / "../icp.yaml"
response = call_canister_api(icp_yaml_path=ICP_YAML_PATH, ...)
dfx_json_path= still works for one release as a deprecated alias (it resolves to the
sibling icp.yaml and emits a DeprecationWarning), so your suite will not break the moment
you upgrade.
Run pytest from your project directory so icp can find icp.yaml.
The pytest flag is still --network — do not change it to -e. That is the icp CLI's
flag; pytest keeps its own spelling. What changed is only what the value refers to: an
environment in icp.yaml instead of a dfx network.
pytest --network=local # unchanged from dfx; `local` is now an icp.yaml environment
The one real breaking change: Candid text formatting
icp's Candid pretty-printer differs from dfx's. Scalars are byte-identical, so the majority of assertions are untouched:
"(true)" # same
"(0 : int)" # same
'("some text")' # same
Composite values differ:
| dfx | icp-cli | |
|---|---|---|
| unit response | (nothing) | () |
| vec | (vec { 1; 2;}) |
(vec { 1; 2 }) — no trailing ; |
| record / long string | one line | wrapped over indented lines, trailing , |
icpp-pro 5.5.0 ships a helper so your assertions do not depend on that line wrapping:
from icpp.smoketest import flatten_candid_text
assert flatten_candid_text(response) == '( record { greeting = "Hello!"; }, )'
flatten_candid_text collapses runs of whitespace, so do not use it when the whitespace
inside a string is what you are testing.
For reference, migrating icpp-pro's own suite touched 7 of about 130 assertions.
Query methods are auto-detected
dfx inspected the canister's Candid interface and sent a query request for methods declared
query. call_canister_api does the same: it resolves the interface (from the .did next
to your wasm, falling back to the canister's candid:service metadata) and picks query or
update automatically. You do not have to do anything.
Pass query= only to override it:
call_canister_api(..., query=True) # force a query request
call_canister_api(..., query=False) # force an update request
If you used icpp-pro 5.5.0
5.5.0 always sent update requests unless you passed query=True. That is semantically
correct - update requests work for query methods too - but roughly 2x slower, and it
breaks tests that poll a counter or assert on timing. 5.5.1 restores the dfx
behaviour, so upgrade to 5.5.1 or later and remove any query=True you added purely
to work around it.
7. Javascript bindings
icp-cli has no equivalent of dfx generate. icpp build-wasm --generate-bindings yes
(the default) now uses didc — the same Candid
code generator dfx used internally — to write <canister>.did.js and <canister>.did.d.ts
into src/declarations/<canister>/.
If didc is not installed the step is skipped with a notice. Install it (see
Installation), or pass --generate-bindings no.
8. If you run CI
icp network start downloads the network launcher from the GitHub API. Unauthenticated that
is 60 requests/hour shared across all GitHub-hosted runners, so CI jobs fail at random
with 403 rate limit exceeded. icp-cli reads a token and authenticates with it:
jobs:
my-job:
runs-on: ubuntu-latest
env:
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Command reference
| dfx | icp-cli |
|---|---|
dfx start --clean --background |
icp network stop && rm -rf .icp/cache && icp network start --background |
dfx stop |
icp network stop |
dfx ping local |
icp network ping -e local |
dfx info webserver-port |
icp network status -e local --json → gateway_url |
dfx deploy |
icp deploy --environment local --yes |
dfx deploy --network ic |
icp deploy --environment ic --yes |
dfx canister call C m '(args)' |
icp canister call C m '(args)' -e local (add --query for a query) |
dfx canister id C |
icp canister status C -e local --id-only |
dfx canister status C |
icp canister status C -e local |
dfx identity whoami |
icp identity default |
dfx identity use NAME |
icp identity default NAME |
dfx identity get-principal |
icp identity principal |
dfx generate |
didc bind <did> --target js |
Two differences worth knowing:
icp canister callrequires the arguments.dfx canister call c greetbecomesicp canister call c greet '()' -e local.- Update calls do not prompt for confirmation, so nothing needs piping into them.
Checklist
- [ ]
icp --version>= 1.2.0, and adefaultidentity exists - [ ]
icp.yamlper canister;icp project showparses it - [ ] canister IDs moved to
.icp/data/mappings/<env>.ids.jsonand committed - [ ]
canister_ids.jsonanddfx.jsondeleted - [ ]
.gitignorehas**/.icp/cache/, andgit check-ignoreconfirms.icp/datais not ignored - [ ] tests use
icp_yaml_path=, composite assertions useflatten_candid_text() - [ ]
pytest --network=localgreen - [ ] nothing hardcodes a local port