cf migrate converts a Wrangler configuration file to cloudflare.config.ts
and adds cf to your project. It handles most of the conversion, then lists
the items that you finish by hand. This page covers the whole migration:
preview, run, resolve follow-up items, finish the project, validate, and
deploy.
The examples on this page migrate orders-api, a Worker with a D1 database, a
queue, a cron trigger, a route, and a Durable Object:
{
"name": "orders-api",
"main": "src/index.ts",
"account_id": "<ACCOUNT_ID>",
"compatibility_date": "2026-08-24",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"ENVIRONMENT": "production",
},
"d1_databases": [
{
"binding": "DB",
"database_name": "orders-db",
"database_id": "<DATABASE_ID>",
},
],
"queues": {
"producers": [{ "binding": "JOBS", "queue": "orders-jobs" }],
"consumers": [{ "queue": "orders-jobs", "max_batch_size": 10 }],
},
"routes": [{ "pattern": "api.example.com/*", "zone_name": "example.com" }],
"triggers": {
"crons": ["0 * * * *"],
},
"durable_objects": {
"bindings": [{ "name": "COUNTERS", "class_name": "Counter" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }],
}name = "orders-api"
main = "src/index.ts"
account_id = "<ACCOUNT_ID>"
compatibility_date = "2026-08-24"
compatibility_flags = [ "nodejs_compat" ]
[vars]
ENVIRONMENT = "production"
[[d1_databases]]
binding = "DB"
database_name = "orders-db"
database_id = "<DATABASE_ID>"
[[queues.producers]]
binding = "JOBS"
queue = "orders-jobs"
[[queues.consumers]]
queue = "orders-jobs"
max_batch_size = 10
[[routes]]
pattern = "api.example.com/*"
zone_name = "example.com"
[triggers]
crons = [ "0 * * * *" ]
[[durable_objects.bindings]]
name = "COUNTERS"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Counter" ]- Install
cfand meet its requirements. You do not need to sign in until you deploy. - Commit or stash every change, including untracked files.
cf migratedoes not write files whengit statusreports changes anywhere in the repository. Files that.gitignoreexcludes do not count. - Install the project's dependencies. With the Wrangler bundler, the Worker's
package needs Wrangler 4.136.0 or later installed.
cf migrateuses it to writewrangler.config.ts, andcf dev,cf build, andcf deployrun it. - Do not run
cf dev,cf build, orcf deployin the project first. In an unmigrated project they write their own configuration or fail.
In the directory that contains the Wrangler configuration file, run:
cf migrate --dry-runWithout a path, cf migrate looks for exactly one wrangler.json,
wrangler.jsonc, or wrangler.toml file in the current directory. For a
configuration file elsewhere, such as a package in a monorepo, pass its path:
cf migrate packages/api/wrangler.jsonc --dry-runcf migrate writes its files next to the configuration file.
The dry run lists the files it would change and the follow-up items. It does
not show file contents. For orders-api, it prints:
Using the Wrangler bundler because @cloudflare/vite-plugin is not declared. Pass --bundler vite to override.
Would update 4 file(s):
├─ cloudflare.config.ts
├─ wrangler.config.ts
├─ package.json
└─ package-lock.json
Follow-up work:
├─ [required] durable_objects.bindings.0: Durable Object bindings require manual review after migration.
│ └─ https://developers.cloudflare.com/workers/runtime-apis/context/#exports
└─ [required] migrations: Wrangler Durable Object migrations are unsupported. Replace them with an exports lifecycle declaration, for example `exports: { MyDurableObject: exports.durableObject({ storage: "sqlite" }) }`.
└─ https://developers.cloudflare.com/workers/runtime-apis/context/#exports
⚠ Migration requires follow-up work.Like a real run, the dry run exits with status 1 while [required] items
remain. It does not check whether the Git worktree is clean.
cf migrate chooses a bundler for the migrated project from the package.json
file beside the Wrangler configuration file:
| Bundler | Chosen when | Builds with | Build settings live in |
|---|---|---|---|
| Vite | @cloudflare/vite-plugin is declared |
Vite and the Cloudflare Vite plugin | vite.config.ts |
| Wrangler | @cloudflare/vite-plugin is not declared |
Wrangler, installed in the project | wrangler.config.ts, which cf migrate writes |
When you do not pass --bundler, the first line of output explains the
choice, as in the orders-api preview. To override the choice, pass
--bundler vite or --bundler wrangler.
cf migrate does not install Vite or create vite.config.ts. To move a
project that does not use Vite yet to the Vite bundler, pass --bundler vite,
then install Vite and the Cloudflare Vite plugin beta that cf uses:
npm i -D vite @cloudflare/vite-plugin@betayarn add -D vite @cloudflare/vite-plugin@betapnpm add -D vite @cloudflare/vite-plugin@betabun add -d vite @cloudflare/vite-plugin@betaThen create vite.config.ts:
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [cloudflare()],
});If the project already uses the Cloudflare Vite plugin, install
@cloudflare/vite-plugin@beta in the same way. The beta reads
cloudflare.config.ts and has no configPath option. Remove configPath if
vite.config.ts passes it to cloudflare().
Run cf migrate with the same arguments as the dry run, without --dry-run:
cf migratecf migrate accepts these arguments:
| Argument | Description |
|---|---|
[path] |
Path to the Wrangler configuration file. Defaults to the only wrangler.json, wrangler.jsonc, or wrangler.toml in the current directory. |
--bundler |
vite or wrangler. Chosen from package.json when omitted. |
--dry-run |
Lists the files that would change without writing them. |
--force |
Runs even if the Git worktree is not clean. It does not bypass any other check. |
--no-install |
Skips adding cf to the project. |
Review the changes with git status and git diff. cf migrate changes these
files:
| File | Change |
|---|---|
cloudflare.config.ts |
Created next to the Wrangler configuration file |
wrangler.config.ts |
Created with the Wrangler bundler only. It holds Wrangler build settings. |
package.json and the lockfile |
Updated to add cf as a development dependency |
| The Wrangler configuration file | Unchanged |
Package scripts, vite.config.ts, .gitignore, tsconfig.json, and source |
Unchanged |
The project needs its own cf dependency because cloudflare.config.ts
imports from cf/config. cf migrate installs it with the package manager
that the project uses, based on the packageManager field or the lockfile.
With npm, the install also rewrites package.json with two-space indentation.
If the only package.json is in a parent directory, such as a workspace root,
cf migrate does not change it. Install cf in the package that contains the
Worker instead.
cf migrate never overwrites files. It stops if cloudflare.config.ts
exists, or if wrangler.config.ts exists and you use the Wrangler bundler.
--force does not change this.
Each follow-up item has one of two levels:
[required]: you must resolve it before the project builds. While any required item remains,cf migrateexits with status1. This does not mean that the migration failed.[info]: context that needs no change.
cf migrate also writes each required item into cloudflare.config.ts as a
TODO(@cloudflare) comment. When required items remain, it adds a throw
statement at the top of the file. Until you delete that statement, cf dev,
cf build, and cf deploy fail with this error:
Error: Migration incomplete. Resolve every cf migrate TODO in `cloudflare.config.ts`.For orders-api, cf migrate generates this file:
import { bindings, defineConfig, triggers } from "cf/config";
/**
* This migration needs manual work. Resolve every TODO in this file, then remove the error below.
*/
/**
* TODO(@cloudflare): cf migrate: durable_objects.bindings.0: Durable Object bindings require manual review after migration.
* @see https://developers.cloudflare.com/workers/runtime-apis/context/#exports
*/
/**
* TODO(@cloudflare): cf migrate: migrations: Wrangler Durable Object migrations are unsupported. Replace them with an exports lifecycle declaration, for example `exports: { MyDurableObject: exports.durableObject({ storage: "sqlite" }) }`.
* @see https://developers.cloudflare.com/workers/runtime-apis/context/#exports
*/
throw new Error("Migration incomplete. Resolve every cf migrate TODO in `cloudflare.config.ts`.");
export default defineConfig({
accountId: "<ACCOUNT_ID>",
worker: {
name: "orders-api",
compatibilityDate: "2026-08-24",
compatibilityFlags: [
"nodejs_compat",
],
entrypoint: "src/index.ts",
triggers: [
triggers.fetch({
pattern: "api.example.com/*",
zone: "example.com",
}),
triggers.scheduled({
schedule: "0 * * * *",
}),
triggers.queue({
maxBatchSize: 10,
name: "orders-jobs",
}),
],
env: {
ENVIRONMENT: bindings.text("production"),
DB: bindings.d1({
name: "orders-db",
id: "<DATABASE_ID>",
}),
JOBS: bindings.queue({
name: "orders-jobs",
}),
COUNTERS: bindings.durableObject({
worker: "orders-api",
exportName: "Counter",
}),
},
/**
* TODO(@cloudflare): cf migrate: Durable Object bindings require manual review after migration.
* @see https://developers.cloudflare.com/workers/runtime-apis/context/#exports
*/
/**
* TODO(@cloudflare): cf migrate: Wrangler Durable Object migrations are unsupported. Replace them with an exports lifecycle declaration, for example `exports: { MyDurableObject: exports.durableObject({ storage: "sqlite" }) }`.
* @see https://developers.cloudflare.com/workers/runtime-apis/context/#exports
*/
},
});Some items link to Wrangler or Workers runtime documentation. The next section
describes how to resolve each item for cf. When you have resolved every item,
delete the TODO(@cloudflare) comments, the comment that starts with
This migration needs manual work, and the throw statement.
A project with Durable Objects always has required items: one for each
Durable Object binding and one for the migrations history. cf migrate
converts each binding, but it does not convert migrations.
-
Import
exportsfromcf/config. Underworker, add anexportsentry for each Durable Object class that is live today. Match the storage that the class already uses:"sqlite"for classes created withnew_sqlite_classes, and"legacy-kv"for classes created withnew_classes.exports: { Counter: exports.durableObject({ storage: "sqlite" }), }, -
Review each generated binding. In
bindings.durableObject({ worker, exportName }),workeris the name of the Worker that defines the class andexportNameis the class name. -
Delete the TODO comments for these items.
Do not copy renames or deletions that have already been applied. For classes that you still need to rename, delete, or transfer, refer to Convert Durable Object migrations.
cf migrate converts each env.<NAME> block into a case of a
switch (ctx.mode) statement. Each case returns a complete configuration. It
follows Wrangler's inheritance rules: bindings, vars, and secrets from the
top level are not copied into environments. An environment without its own
name gets <NAME>-<ENVIRONMENT>. The generated configuration has this shape:
export default defineConfig((ctx) => {
switch (ctx.mode) {
case "staging": {
return {
worker: {
name: "orders-api-staging",
// ...
},
};
}
default: {
return {
worker: {
name: "orders-api",
// ...
},
};
}
}
});Replace --env <NAME> with --mode <NAME>:
cf build --mode staging
cf deploy --mode stagingA required item that applies to the top-level configuration appears again for each environment that inherits it.
Without --mode, the Wrangler bundler uses the default branch. The Vite
bundler uses the development mode for cf dev and production for
cf build and cf deploy. With the Vite bundler, an environment named
production or development would be selected without --mode, so
cf migrate marks it as required. Rename that case, for example to "prod",
and pass the new name with --mode.
For mode defaults across all commands, refer to Convert environments to modes.
Wrangler build fields, such as build, minify, alias, and
assets.directory, do not belong in cloudflare.config.ts.
With the Wrangler bundler, cf migrate moves them to wrangler.config.ts with
camelCase keys, and no follow-up item is needed. For example:
import { defineWranglerConfig } from "wrangler/experimental-config";
export default defineWranglerConfig({
alias: {
lodash: "lodash-es",
},
minify: true,
uploadSourceMaps: true,
build: {
command: "npm run build:css",
},
dev: {
port: 8788,
},
types: {
generate: false,
},
assetsDirectory: "./public",
});wrangler.config.ts is experimental and can change during the beta.
With the Vite bundler, cf migrate lists these fields in a required item and
does not move them. Move each setting to its Vite equivalent in
vite.config.ts, such as alias to resolve.alias. For every field, refer to
Build settings.
A custom build command does not carry over to the Vite bundler. cf build
runs Vite directly, so run the command yourself first, for example
npm run build:css && cf build.
With the Vite bundler, cf migrate lists upload_source_maps in its required
item for build settings. To keep uploading Worker source maps, turn on
build.sourcemap for the Worker's Vite environment, then delete the TODO
comment. By default, the Cloudflare Vite plugin beta names the Worker's
environment ssr:
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [cloudflare()],
environments: {
ssr: {
build: {
sourcemap: true,
},
},
},
});How cf migrate reports source maps can change during the beta.
cf migrate does not convert the migrations_dir, migrations_pattern, or
migrations_table fields of a D1 binding. Pass them to
cf d1 migrations apply instead:
| Wrangler field | cf d1 migrations apply option |
|---|---|
migrations_dir |
--dir, which defaults to ./migrations |
migrations_pattern |
--pattern |
migrations_table |
--table, which defaults to d1_migrations |
For example:
cf d1 migrations apply <DATABASE_ID> --dir db/migrationscf d1 migrations apply takes the database ID. Unlike Wrangler, it applies
migrations to the remote database unless you add --local.
Resolve the remaining items as follows, then delete each TODO comment:
| Item | What to do |
|---|---|
Preview resources: preview_id, preview_bucket_name, preview_database_id |
cloudflare.config.ts has no preview resource fields. During cf dev, bindings use local resources unless you set dev: { remote: true } on the binding. |
A route that uses zone_id |
cf migrate copies the zone ID into the zone option of triggers.fetch(), which accepts a zone name or a zone ID. Confirm the value. |
| A service binding to a legacy service environment | cf migrate rewrites the target to <SERVICE>-<ENVIRONMENT>. Confirm that this is the Worker to bind to. |
Workers Sites (site) |
Not supported. Move the site to Workers Static Assets. |
previews |
Converted to a branch on ctx.isPreview. Review the branch. For more information, refer to Deploy a preview. |
A missing name or compatibility_date |
Replaced with the placeholders "TODO" and "YYYY-MM-DD". Set real values. |
Unsupported or unknown fields, such as keep_vars |
Remove them, or find an equivalent in the configuration reference. |
Installing cf |
Appears with --no-install, after a failed install, or when no package.json is beside the configuration file. Install cf in the Worker's package. |
To install cf as a development dependency, run:
npm i -D cfyarn add -D cfpnpm add -D cfbun add -d cfcf migrate does not convert Workflow bindings or Containers configuration.
It reports both as required items. Add them to cloudflare.config.ts by hand:
- For a Workflow, declare the class with
exports.workflow({ name })in the Worker that defines it. Bind to it withbindings.workflow({ name, worker, exportName }). Refer to Declare exports. - For a Container, define it with
defineContainer(), add it to the top-levelcontainersarray, and reference it from anexports.durableObject({ storage: "sqlite", container })entry. Refer to Attach a Container.
Then delete the TODO comment for each item.
cf migrate never reads secret files. It lists any .dev.vars or .env
files that it finds as an [info] item. cf dev still loads .dev.vars for
local development.
Entries in secrets.required become bindings.secret() values. Declare any
other secret that the Worker reads the same way:
env: {
API_TOKEN: bindings.secret(),
},To upload secrets with a new version, pass --secrets-file <PATH> to
cf deploy or cf workers versions create. The file can be JSON or .env
format.
cf migrate leaves the rest of the project to you:
-
Replace Wrangler commands in the
package.jsonscripts. Wrangler commands read the Wrangler configuration file and ignorecloudflare.config.ts.package.jsonjson { "scripts": { "dev": "cf dev", "build": "cf build", "deploy": "cf deploy" } }cf buildruns Vite or Wrangler directly, not yourbuildscript. To run extra steps, chain them yourself, for exampletsc -b && cf build. -
Add
.cloudflare/to.gitignore. The directory holds Build Output, generated types, and other files thatcfgenerates. -
Set
"type": "module"inpackage.json. Without it, Node.js prints aMODULE_TYPELESS_PACKAGE_JSONwarning each timecfloadscloudflare.config.ts. -
Generate types:
- With the Vite bundler,
cf devandcf buildwrite.cloudflare/types/index.d.ts. - With the Wrangler bundler,
cf migratesetstypes: { generate: false }inwrangler.config.ts. Change it totrue, or runcf workers types.
Then add the generated types to
tsconfig.json:tsconfig.jsonjson { "include": ["src", "cloudflare.config.ts", ".cloudflare/types"] } - With the Vite bundler,
-
(Optional) Replace the string
entrypointwith an import that uses thecf-workerattribute:cloudflare.config.tsts import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };Under
worker, replaceentrypoint: "src/index.ts"withentrypoint. With a string path,Envbinding types are still inferred, but the types of your Worker module exports are not. To type-check a.tsimport path, setallowImportingTsExtensionsintsconfig.json.
For a finished version of orders-api, refer to
Complete example.
Start the project locally with cf dev and check that it responds. Then stop
the development server, build the project, and run a dry-run deployment:
cf build
cf deploy --dry-runNone of these commands need you to sign in. cf deploy --dry-run builds the
project, prints the bindings it would deploy, and uploads nothing.
If the project has environments, also validate each mode:
cf build --mode staging
cf deploy --dry-run --mode stagingSign in, then deploy:
cf auth login
cf deploycf deploy builds the project, uploads a new Worker Version, and deploys it.
For deployment options, refer to Develop, build, and deploy.
To deploy from CI, refer to Use cf in CI.
Once cloudflare.config.ts exists, cf ignores the Wrangler configuration
file. Delete the Wrangler file after you deploy with cf and your scripts and
CI use cf.
Wrangler does not read cloudflare.config.ts. If you still run Wrangler
commands in the project, such as wrangler tail, keep the Wrangler
configuration file until you no longer need them.
No Wrangler config found in <DIRECTORY>. Pass its path to cf migrate.Run cf migrate in the directory that contains the Wrangler configuration
file, or pass the file path. If the directory contains more than one Wrangler
configuration file, cf migrate reports
Multiple Wrangler configs found in <DIRECTORY>. Pass the exact path.
Git worktree is not clean. Commit or stash your changes before running a codemod, or rerun with `--force` to bypass this safety check.Commit or stash every change, including untracked files, then run the migration again.
Cannot migrate because <PATH>/cloudflare.config.ts already exists. Inspect and finish the existing migration; it will not be overwritten. Automated agents should read its TODOs and ask the user about unresolved choices.If an earlier cf migrate run created the file, finish that migration. If
cf dev, cf build, or cf deploy created it, undo their changes as
described in
Run project commands only after you migrate,
then run cf migrate.
With the Wrangler bundler, cf migrate needs a local Wrangler installation,
even for a dry run. Without one, it reports:
Generating wrangler.config.ts requires wrangler 4.100.0 or newer because earlier versions do not export wrangler/experimental-config. No local Wrangler installation was found. Update Wrangler and retry the migration.After the migration, cf dev, cf build, and cf deploy need Wrangler
4.136.0 or later. With an older version, their error includes:
cf requires wrangler@4.136.0 or newer for cf dev, cf build, cf deploy, and cf previews deploy.Install the project's dependencies or update Wrangler, then run the command again.
Error: Migration incomplete. Resolve every cf migrate TODO in `cloudflare.config.ts`.Resolve the follow-up items, then delete the
throw statement at the top of cloudflare.config.ts.
Error: cloudflare.config.ts is required when --experimental-new-config is enabled.cf dev, cf build, or cf deploy ran in a Wrangler project that you have
not migrated. Run cf migrate.
wrangler is declared in <PATH>/package.json but is not installed.Install the project's dependencies. cf looks for Wrangler only in the Worker
package's own node_modules directory. In a monorepo, a copy hoisted to the
workspace root does not count.
If package.json declares neither package, for example because you use a
global Wrangler installation, the error starts with
No Cloudflare dev-server is installed in this project. Add Wrangler as a
development dependency, or install Vite and the Cloudflare Vite plugin as
described in Choose a bundler.
A global cf runs the copy of cf installed in the project. If the project
pins an older cf without cf migrate, update the project's cf dependency,
or run the latest version once with npx cf@latest migrate.