Let an AI agent run the migration Migrate this Turborepo workspace to Nx, following the instructions on the page below.
1. Run `npx nx@latest init` from the workspace root. It detects `turbo.json`, writes an equivalent `nx.json`, adds the `nx` dev dependency, and updates `.gitignore`. Leave `turbo.json` in place until the end.
2. Diff `turbo.json` against the generated `nx.json` and tell me every key that did not carry over. Expect package-specific task entries like `web#build`, per-package `turbo.json` files, `persistent: true`, and task-level `env`, and keep reading past those, since top-level keys such as `ui` map to CLI flags instead of config. Do not stop at a fixed count.
3. Port every `<package>#<task>` entry, and every per-package `turbo.json`, to an array entry under that task's `targetDefaults` key, with `"filter": { "projects": ["<package>"] }`. The unfiltered entry stays first as the baseline. A key set in a filtered entry replaces the baseline value outright rather than merging, and that includes arrays, so re-list the baseline `inputs` in any filtered entry that sets `inputs`. When one rule covers several packages, prefer a directory pattern such as `apps/*` over listing each name, and check that package names actually share a prefix before reaching for a name glob.
4. Set `"continuous": true` on every task that was `persistent: true`.
5. Move each task's `env` list into that target's `inputs` as `{ "env": "VAR_NAME" }` entries. Then add `"sharedGlobals"` to the `inputs` of every target that has an explicit `inputs` array, because `nx init` reaches `sharedGlobals` only through `default` and an explicit array bypasses it, which silently stops `globalEnv` and `globalDependencies` from invalidating those targets. Do not add `passThroughEnv` equivalents, because Nx does not block environment variables at runtime.
6. Verify. Run `nx show project <name>` on a project each filtered entry should hit and on one it should not. Then run `nx run-many -t build lint check-types` twice and confirm the second run is all cache hits. `run-many` exits 0 for a target no project defines, so compare the project count in the summary against `nx show projects`. If `globalDependencies` was set, edit one of those files and confirm the dependent targets miss the cache.
7. Move CI over. Replace `turbo run` calls with `nx affected -t <tasks>`, and tell me whether the workspace should run `nx connect` for remote caching.
8. Only once CI is green: delete `turbo.json` and any package-level `turbo.json`, remove the `turbo` dev dependency, drop `.turbo` from `.gitignore`, and rewrite root scripts from `turbo run <task>` to `nx run-many -t <task>`. Then grep the whole workspace for `turbo` and deal with what is left, including scripts in packages rather than at the root and `eslint-plugin-turbo` in the shared ESLint config.
Report what you changed in `nx.json`. Where a fix belongs in a project's own configuration instead, tell me rather than editing it silently.
Page: https://nx.dev/docs/kb/from-turborepo.md
Migrate this Turborepo workspace to Nx, following the instructions on the page below.
1. Run `npx nx@latest init` from the workspace root. It detects `turbo.json`, writes an equivalent `nx.json`, adds the `nx` dev dependency, and updates `.gitignore`. Leave `turbo.json` in place until the end.
2. Diff `turbo.json` against the generated `nx.json` and tell me every key that did not carry over. Expect package-specific task entries like `web#build`, per-package `turbo.json` files, `persistent: true`, and task-level `env`, and keep reading past those, since top-level keys such as `ui` map to CLI flags instead of config. Do not stop at a fixed count.
3. Port every `<package>#<task>` entry, and every per-package `turbo.json`, to an array entry under that task's `targetDefaults` key, with `"filter": { "projects": ["<package>"] }`. The unfiltered entry stays first as the baseline. A key set in a filtered entry replaces the baseline value outright rather than merging, and that includes arrays, so re-list the baseline `inputs` in any filtered entry that sets `inputs`. When one rule covers several packages, prefer a directory pattern such as `apps/*` over listing each name, and check that package names actually share a prefix before reaching for a name glob.
4. Set `"continuous": true` on every task that was `persistent: true`.
5. Move each task's `env` list into that target's `inputs` as `{ "env": "VAR_NAME" }` entries. Then add `"sharedGlobals"` to the `inputs` of every target that has an explicit `inputs` array, because `nx init` reaches `sharedGlobals` only through `default` and an explicit array bypasses it, which silently stops `globalEnv` and `globalDependencies` from invalidating those targets. Do not add `passThroughEnv` equivalents, because Nx does not block environment variables at runtime.
6. Verify. Run `nx show project <name>` on a project each filtered entry should hit and on one it should not. Then run `nx run-many -t build lint check-types` twice and confirm the second run is all cache hits. `run-many` exits 0 for a target no project defines, so compare the project count in the summary against `nx show projects`. If `globalDependencies` was set, edit one of those files and confirm the dependent targets miss the cache.
7. Move CI over. Replace `turbo run` calls with `nx affected -t <tasks>`, and tell me whether the workspace should run `nx connect` for remote caching.
8. Only once CI is green: delete `turbo.json` and any package-level `turbo.json`, remove the `turbo` dev dependency, drop `.turbo` from `.gitignore`, and rewrite root scripts from `turbo run <task>` to `nx run-many -t <task>`. Then grep the whole workspace for `turbo` and deal with what is left, including scripts in packages rather than at the root and `eslint-plugin-turbo` in the shared ESLint config.
Report what you changed in `nx.json`. Where a fix belongs in a project's own configuration instead, tell me rather than editing it silently.
Page: https://nx.dev/docs/kb/from-turborepo.md
Migrate this Turborepo workspace to Nx, following the instructions on the page below.
1. Run `npx nx@latest init` from the workspace root. It detects `turbo.json`, writes an equivalent `nx.json`, adds the `nx` dev dependency, and updates `.gitignore`. Leave `turbo.json` in place until the end.
2. Diff `turbo.json` against the generated `nx.json` and tell me every key that did not carry over. Expect package-specific task entries like `web#build`, per-package `turbo.json` files, `persistent: true`, and task-level `env`, and keep reading past those, since top-level keys such as `ui` map to CLI flags instead of config. Do not stop at a fixed count.
3. Port every `<package>#<task>` entry, and every per-package `turbo.json`, to an array entry under that task's `targetDefaults` key, with `"filter": { "projects": ["<package>"] }`. The unfiltered entry stays first as the baseline. A key set in a filtered entry replaces the baseline value outright rather than merging, and that includes arrays, so re-list the baseline `inputs` in any filtered entry that sets `inputs`. When one rule covers several packages, prefer a directory pattern such as `apps/*` over listing each name, and check that package names actually share a prefix before reaching for a name glob.
4. Set `"continuous": true` on every task that was `persistent: true`.
5. Move each task's `env` list into that target's `inputs` as `{ "env": "VAR_NAME" }` entries. Then add `"sharedGlobals"` to the `inputs` of every target that has an explicit `inputs` array, because `nx init` reaches `sharedGlobals` only through `default` and an explicit array bypasses it, which silently stops `globalEnv` and `globalDependencies` from invalidating those targets. Do not add `passThroughEnv` equivalents, because Nx does not block environment variables at runtime.
6. Verify. Run `nx show project <name>` on a project each filtered entry should hit and on one it should not. Then run `nx run-many -t build lint check-types` twice and confirm the second run is all cache hits. `run-many` exits 0 for a target no project defines, so compare the project count in the summary against `nx show projects`. If `globalDependencies` was set, edit one of those files and confirm the dependent targets miss the cache.
7. Move CI over. Replace `turbo run` calls with `nx affected -t <tasks>`, and tell me whether the workspace should run `nx connect` for remote caching.
8. Only once CI is green: delete `turbo.json` and any package-level `turbo.json`, remove the `turbo` dev dependency, drop `.turbo` from `.gitignore`, and rewrite root scripts from `turbo run <task>` to `nx run-many -t <task>`. Then grep the whole workspace for `turbo` and deal with what is left, including scripts in packages rather than at the root and `eslint-plugin-turbo` in the shared ESLint config.
Report what you changed in `nx.json`. Where a fix belongs in a project's own configuration instead, tell me rather than editing it silently.
Page: https://nx.dev/docs/kb/from-turborepo.md
nx init reads your turbo.json and writes the equivalent nx.json. Your package.json scripts stay as they are, and Nx runs them through the package manager you already use.
Why switch to Nx
Section titled “Why switch to Nx”Nx runs the same scripts, so what you gain is the work Turborepo leaves to you.
First-party plugins for Maven, Gradle, and .NET put those projects in the same graph as your JavaScript packages, and community plugins cover Python, Rust, and Go. Turborepo can wrap any CLI in a package.json script, and an experimental flag teaches it Cargo workspaces, but outside those it builds no dependency graph, so a Go service and the app calling it stay islands.
Turborepo scales past one machine through manual binning, where you assign tasks to runners by hand and rebalance as the workspace grows, while Nx Agents distributes at the task level using historical timings. On one workspace across four machines that was 9m 20s against Turborepo's 19m 18s. Nx Cloud also splits e2e suites by file, re-runs flaky tasks, and runs an agent that diagnoses and fixes broken tasks on CI.
Task sandboxing surfaces the files a task read or wrote without declaring them, and Turborepo has no equivalent. Generators run as code against the project graph and do AST-level transforms rather than filling in templates.
For the full comparison, see Nx vs Turborepo.
1. Run nx init
Section titled “1. Run nx init”From the workspace root:
npx nx@latest initNx detects the turbo.json, converts it, and installs:
.gitignore | Nx cache directoriesnx.json | The converted turbo.jsonpackage.json | The "nx" dev dependency<lockfile> |turbo.json stays where it is and Turborepo keeps working, so you can run both side by side until you remove it in step 8.
2. Read the generated nx.json
Section titled “2. Read the generated nx.json”Take the turbo.json from the create-turbo starter:
{ "$schema": "https://turborepo.dev/schema.json", "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**"] }, "lint": { "dependsOn": ["^lint"] }, "check-types": { "dependsOn": ["^check-types"] }, "dev": { "cache": false, "persistent": true } }}nx init turns it into this:
{ "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "build": { "dependsOn": ["^build"], "inputs": ["{projectRoot}/**/*", "{projectRoot}/.env*"], "outputs": ["{projectRoot}/.next/**", "!{projectRoot}/.next/cache/**"], "cache": true }, "lint": { "dependsOn": ["^lint"], "cache": true }, "check-types": { "dependsOn": ["^check-types"], "cache": true }, "dev": { "cache": false } }}Three differences to read closely:
- Turborepo paths are relative to a package. Nx paths are relative to the workspace root and use
{projectRoot}and{workspaceRoot}tokens to say which one you mean, so one entry can reference a workspace-wide file and a per-package file at the same time. - Turborepo caches a task unless you opt out, so an empty task entry is a cached task. Nx writes
cachefor each one. persistent: trueondevis gone. Step 4 puts it back under its Nx name.uiis gone as well, and it is not the only top-level key that ends up as a CLI flag rather than configuration. Check yours against the configuration mapping below.
3. Move package-specific task configuration
Section titled “3. Move package-specific task configuration”Turborepo lets you override a task for one package, either with a <package>#<task> key in the root turbo.json or with a turbo.json inside the package that extends the root. nx init skips both, so a config like this comes across with only the build entry:
{ "tasks": { "build": { "dependsOn": ["^build"] }, "web#build": { "outputs": [".next/**", "!.next/cache/**"] } }}In Nx, that key takes an array instead, with a base entry and an override scoped by filter:
{ "targetDefaults": { "build": [ { "dependsOn": ["^build"], "cache": true }, { "filter": { "projects": ["web"] }, "outputs": ["{projectRoot}/.next/**", "!{projectRoot}/.next/cache/**"] } ] }}An override replaces the base value for each key it sets, so a filtered entry that sets inputs needs the baseline inputs listed again. The target defaults reference covers the rest.
A per-package turbo.json gets the same treatment. Its task config becomes one more override keyed to that project, and the file itself goes away in step 8.
When one rule covers several packages, scope by directory rather than listing names, as in "projects": ["apps/*"]. filter.projects also takes name globs, tags that you set in each project's own configuration, and negation with !.
Confirm the scoping in both directions before moving on. Check a project the filter should hit, then one it should not:
nx show project webnx show project docs4. Mark long-running tasks as continuous
Section titled “4. Mark long-running tasks as continuous”persistent: true does not survive the conversion, and Nx calls the same thing continuous:
{ "targetDefaults": { "dev": { "cache": false, "continuous": true } }}A task that depends on a continuous task starts as soon as the server it needs is up, rather than waiting for a process that never exits. That is how you express "run the e2e tests against the dev server" as a task dependency.
5. Carry over task-level environment variables
Section titled “5. Carry over task-level environment variables”A task's own env list does not convert, so add those to the target's inputs:
{ "targetDefaults": { "build": { "inputs": [ "{projectRoot}/**/*", "{projectRoot}/.env*", { "env": "API_URL" } ] } }}Declaring an environment variable this way affects the cache only. Change API_URL and the build reruns. See environment variables as inputs for the wildcard and runtime forms.
Turborepo's passThroughEnv and globalPassThroughEnv have no counterpart, because Nx never blocked environment variables from reaching your tasks in the first place. Delete them.
6. Verify the workspace
Section titled “6. Verify the workspace”Run everything twice:
nx run-many -t build lint check-typesThe first run populates the cache and the second should report every task as a cache hit. If a task misses on the second run, its inputs include something that changes between runs.
run-many exits 0 for a target that no project defines, so a typo in a task name reports success without running anything. The run summary reports how many projects it covered, and comparing that against nx show projects catches the case where a target name silently matched nothing.
Open the project graph and check that the dependencies between your packages are the ones you expect:
nx graph7. Move CI over
Section titled “7. Move CI over”Replace the turbo run calls in your CI config with the Nx equivalents, and run only what your change touched:
nx affected -t build lint check-typesThen connect the workspace so CI and your team share one cache:
nx connectRemote caching on Nx Cloud is the direct replacement for Turborepo's Remote Cache. Distributing tasks across agents and splitting e2e runs by file have no Turborepo equivalent, and they speed up the runs a cache cannot help with, such as a change to a shared package. The CI setup guide walks through a full pipeline.
8. Remove Turborepo
Section titled “8. Remove Turborepo”Once CI is green on Nx, delete the root turbo.json and any package-level ones, remove the turbo dev dependency, and drop .turbo from .gitignore. Then point the root scripts at Nx:
{ "scripts": { "build": "nx run-many -t build", "dev": "nx run-many -t dev", "lint": "nx run-many -t lint", "check-types": "nx run-many -t check-types" }}The references that break are the ones below the root, so search the workspace before you commit:
grep -rn turbo --include=package.json --include="*.js" --include="*.mjs" . | grep -v node_modulesGet-ChildItem -Recurse -Include package.json,*.js,*.mjs | Where-Object { $_.FullName -notmatch 'node_modules' } | Select-String turboFor example, a create-turbo workspace ships two of these. packages/ui runs turbo gen react-component, a script in a package rather than at the root, and it fails the moment the dependency goes. Port it to a generator. The shared ESLint config depends on eslint-plugin-turbo for a rule that reads the turbo.json you deleted, so drop both.
Additional considerations
Section titled “Additional considerations”Let plugins infer your tasks
Section titled “Let plugins infer your tasks”The migration above keeps every task defined by a package.json script. That is how Turborepo works, and it runs an Nx workspace fine.
Nx can also derive tasks from the tools you already configured. Add @nx/vite and a package with a vite.config.ts gets a build target whose inputs and outputs are read from that config, with no script and no targetDefaults entry to maintain. The same applies to Jest, Playwright, ESLint, Cypress, Gradle, and others. That removes the class of cache bug where a tool's output directory changes and the config that lists it does not.
Adopt this per tool, after the migration is done. See inferred tasks.
Configuration mapping
Section titled “Configuration mapping”Global configuration:
| Turborepo Property | Nx Equivalent |
|---|---|
cacheDir | Set in cacheDirectory |
daemon | Use NX_DAEMON=false or set useDaemonProcess: false in nx.json |
envMode | Nx core does not block any environment variables. See Loading Environment Variables |
globalDependencies | Add to the sharedGlobals namedInput |
globalEnv | Add to the sharedGlobals namedInput as an env input |
globalPassThroughEnv | N/A. See Loading Environment Variables |
remoteCache | See remote caching |
ui | Nx will intelligently pick the most appropriate terminal output style, but it can be overridden with --output-style |
Task configuration:
| Turborepo Property | Nx Equivalent |
|---|---|
extends | N/A. Projects always extend targetDefaults from nx.json |
dependsOn | Same syntax |
env | Define env inputs |
passThroughEnv | N/A. See Loading Environment Variables |
outputs | Similar syntax |
cache | Similar syntax |
inputs | Similar syntax |
outputLogs | Use --output-style |
persistent | Use continuous |
interactive | N/A. Tasks marked continuous can accept stdin automatically. |
<package>#<task> | An array entry under that task's targetDefaults key with a filter |
Command equivalents
Section titled “Command equivalents”| Turborepo Command | Nx Equivalent |
|---|---|
turbo run test lint build | nx run-many -t test lint build |
turbo run build --affected | nx affected -t build |
turbo devtools | nx graph for full interactive experience, also available in Nx Console |
--cache-dir | Set in nx.json under cacheDirectory |
--concurrency | --parallel |
--continue | Use --nx-bail with the inverse value |
--cpuprofile | Use NX_PROFILE=profile.json |
--cwd | Available in run-commands executor |
--daemon | Use NX_DAEMON=false or set useDaemonProcess: false |
--dry-run | Use nx show target <project>:<target> --inputs --outputs to preview inputs and outputs (available since 22.6.0+) |
--env-mode | See Loading Environment Variables |
--filter | Use lots of advanced project matcher syntax like -p admin-* or -p tag:api-* |
--force | nx reset and then run the command again |
--framework-inference | N/A. Nx plugins infer tasks automatically as a first class feature |
--global-deps | Use the sharedGlobals namedInput. Nx is far more flexible with composable namedInputs |
--graph | Similar syntax or nx graph for full interactive experience |
--heap | N/A. Use --verbose |
--ignore | Use .nxignore or .gitignore |
--log-order | Use --output-style |
--no-cache | Use --skip-nx-cache |
--output-logs | Use --output-style |
--only | N/A |
--parallel | N/A |
--preflight | N/A |
--summarize | N/A |
--token | Set Nx Cloud CI Access Token |
--team | See --token for Nx Cloud workspace selection |
--trace | N/A. Use --verbose |
--verbosity | Use --verbose |
turbo gen | Use nx generate |
turbo login | nx login - Create an Nx Cloud account |
turbo link | nx connect - Connect a workspace to an Nx Cloud account |
For a complete list of Nx commands and options, see the Nx CLI documentation.