Skip to content
Back to Knowledge Base

Migrate to Inferred Tasks

In this recipe, you'll learn how to migrate an existing Nx workspace from using executors in project.json to using inferred tasks.

The main benefits of migrating to inferred tasks are

  • reducing the amount of configuration needed in project.json
  • inferring the correct cache settings based on the tool configuration files
  • splitting tasks (Atomizer) for plugins that support it

For the best experience, we recommend that you migrate to the latest Nx version before continuing.

Terminal window
npx nx migrate latest

Enable inferred tasks for older workspaces

Section titled “Enable inferred tasks for older workspaces”

If your workspace was upgraded from an Nx version that predates inferred tasks, a migration may have set useInferencePlugins to false in nx.json. When useInferencePlugins is false:

  1. A newly generated project will have all targets defined with executors, not with inferred tasks.
  2. Running nx add @nx/some-plugin will not register the plugin in nx.json, so that plugin will not infer tasks.

Remove the property, or set it to true, before you migrate your projects.

You can use the infer-targets generator to quickly migrate all available plugins to use inferred tasks. See the sections below for more details on the individual plugins' migration processes.

Terminal window
npx nx g infer-targets

The generator will automatically detect all available convert-to-inferred generators and run the ones you choose. If you only want to try it on a single project, pass the --project option.

Most of the official plugins come with a convert-to-inferred generator. This generator will

  • register the inference plugin in the plugins section of nx.json
  • migrate executor options into the tool's configuration files (where applicable)
  • centralize configuration that is shared across the migrated projects into targetDefaults in nx.json (whole-workspace migrations only, see below)
  • clean up project.json to keep only the configuration that deviates from what is inferred or centralized
  • remove targetDefaults entries keyed by a migrated executor once no target resolves through that executor (whole-workspace migrations only)

To get started, run nx g convert-to-inferred, and you'll be prompted to choose a plugin to migrate.

npx nx g convert-to-inferred
? Which generator would you like to use? …
@nx/eslint:convert-to-inferred
@nx/playwright:convert-to-inferred
@nx/vite:convert-to-inferred
None of the above

We recommend that you check that the configurations are correct before continuing to the next plugin. If you only want to try it on a single project, pass the --project option.

The convert-to-inferred generator removes uses of executors from the corresponding plugin. For example, if @nx/vite is migrated, then uses of @nx/vite:build, @nx/vite:dev-server, @nx/vite:preview-server, and @nx/vite:test executors will be removed.

Target and configuration names are maintained for each project in their project.json files. A target may be removed from project.json entirely when nothing is left to store there: either everything is inferred, or the remaining customized options and configurations were shared across the migrated projects and moved into targetDefaults in nx.json (whole-workspace migrations only). What stays in project.json is the per-project deviation. To get the full project details (including all inferred tasks), run:

Terminal window
npx nx show project <project-name>

For example, if we migrated the @nx/vite plugin for a single app (i.e. nx g @nx/vite:convert-to-inferred --project demo), then running nx show project demo will show a screen similar to the following.

Test

demo

Root: apps/demo

Type:application

Targets

  • build

    vite build

  • serve

    vite dev

You'll notice that the serve and build tasks are running the Vite CLI and there are no references to Nx executors. Since the targets directly invoke the Vite CLI, any options that may be passed to it can be passed via Nx commands. e.g. nx serve demo --cors --port 8888 enables CORs and uses port 8888 using Vite CLI options The same CLI setup applies to other plugins as well.

Read the recipe on passing args to commands for more information.

When inference does not produce the expected target

Section titled “When inference does not produce the expected target”

The migration reads each executor target, then asks the plugin to infer the equivalent task from the tool's configuration files. If the plugin finds your project but does not infer a task with the name the migration expects, the migration stops with an error like The nx plugin found a project inside <root> but did not infer a "<target>" target for it. See the Troubleshooting section below for how to resolve this error and the related The nx plugin did not find a project inside ... error.

There may also be changes to the configuration files used by the underlying tool. The changes come with comments to explain them, and may also provide next steps for you to take. One common change is to add support for different configuration options. For example, if we have an existing Vite app with the following build target:

project.json
"build": {
"executor": "@nx/vite:build",
"options": {
"mode": "development"
},
"defaultConfiguration": "production",
"configurations": {
"development": {},
"production": {},
"ci": {}
}
}

Where we have development, production, and ci configurations. Then running nx g @nx/vite:convert-to-inferred will result in these lines added to vite.config.ts.

vite.config.ts
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
// These options were migrated by @nx/vite:convert-to-inferred from the project.json file.
const configValues = { default: {}, development: {}, production: {}, ci: {} };
// Determine the correct configValue to use based on the configuration
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
const options = {
...configValues.default,
...(configValues[nxConfiguration] ?? {}),
};
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/demo',
// ...
});

The configuration changes ensure that passing --configuration still work for the target. Differences in options can be added to the configValues object, and the right value is determined using the NX_TASK_TARGET_CONFIGURATION environment variable. Again, there may be other types of changes so read the comments to understand them.

Lastly, you can inspect the nx.json file to see a new plugins entry. For @nx/vite, there should be an entry like this:

nx.json
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"serveTargetName": "serve",
"previewTargetName": "preview",
"testTargetName": "test",
"serveStaticTargetName": "serve-static"
}
}

You may change the target name options to change how Nx adds them to the project. For example, if you use "serveTargetName": "dev" then you would run nx dev demo rather than nx serve demo for your Vite project.

Centralize shared configuration in targetDefaults

Section titled “Centralize shared configuration in targetDefaults”

When you migrate the whole workspace (that is, without --project), the generator also looks for configuration that is identical across every migrated project for a target and lifts it into targetDefaults in nx.json, so it lives in one place instead of being repeated in each project.json. The entry is scoped to the plugin's targets with a filter, so it only applies to the targets this plugin infers:

nx.json
{
"targetDefaults": {
"build": [
{
"filter": { "plugin": "@nx/vite/plugin" },
"options": {
"config": "./vite.config.ts"
}
}
]
}
}

Each project's project.json then keeps only the options that differ from this shared default. Configuration a project overrides in its own project.json still wins over the centralized default.

Some projects are excluded from centralization: when a target's identity is authored outside the plugin (a project.json executor or command, or a package.json script or nx.targets entry), the centralized default does not resolve for that target. Those projects keep their full per-project configuration, and the generator prints a warning naming them. When a package.json script with the target's name, or an nx.targets entry next to a project.json, authors the identity, the generator also leaves the target unconverted, because removing its executor would let that entry take the target over. Rename or exclude the script, or remove that nx.targets entry, then rerun the generator to convert it.

When multiple targets in one project map different target names to the same plugin option, for example two @nx/jest:jest targets in one project, a whole-workspace migration converts one of them, leaves the conflicting targets unchanged, and prints a warning naming them. Once the converted target is inferred, rerun the migration to convert the next one. A single-project migration (--project) stops with an error instead, because one plugin registration can hold only one value for that option.

Centralization is also skipped when other plugins are registered after the migrated plugin in nx.json. A plugin registered later can take over a target's identity, and the shared default would then stop applying. The generator prints a warning and keeps the full per-project configuration in that case.

Centralization is skipped for a target when adding the shared entry would change which existing targetDefaults apply to it. For example, an exact target-name entry takes precedence over a glob entry such as build-*, so adding one could stop the glob entry's configuration from applying to the migrated targets. The generator prints a warning and keeps the full per-project configuration for that target.

When you convert several plugins in one run of the infer-targets generator, the generator defers centralization until every conversion has run and registered its plugin, then centralizes the shared configuration for the whole batch in a single pass. Because that pass sees the finished plugin list, registrations added by later conversions in the same run do not block earlier ones, and every conversion in the batch is eligible to centralize. Each conversion is still subject to the skip conditions above and to the verification step below, evaluated against the final state of nx.json.

A plugin registered earlier than the migrated plugin can remain the source of a target when it infers the same target for the same project with the same executor. The generator cannot detect this case, and the centralized default does not apply to that target. Review the configuration when two inference plugins produce the same target name and executor for the same project. For nx:run-commands and nx:run-script targets, also compare the command or script.

After centralizing, the generator verifies that every migrated target still resolves to the same configuration. If the centralized default would also reach a project the generator did not migrate, or the verification cannot confirm the result is equivalent, the generator restores full per-project configuration for the affected targets and prints a warning.

This step is skipped for single-project (--project) migrations, since centralizing one project's configuration would leak it onto its siblings.

The migrations maintain the same targets and configurations for each project, thus to verify it you should run the affected targets.

For example

  • for @nx/vite you should check the build, serve, and test targets
  • for @nx/playwright you should check the e2e targets
  • for @nx/eslint you should check the lint target
  • etc.

Remember that the target names are defined in the plugin configuration in nx.json.

Make sure that the tasks are all passing before migrating another plugin.

These plugins come with the Atomizer feature.

  • @nx/cypress
  • @nx/jest
  • @nx/gradle
  • @nx/playwright

The Atomizer splits potentially slow tasks into separate tasks per file. This feature along with task distribution can speed up CI by distributing the split tasks among many agents.

To enable Atomizer, make sure that you are connected to Nx Cloud, and that you have distribution enabled in CI. Some plugins require extra configuration to enable Atomizer, so check the individual plugin documentation page for more details.

If you run into any issues during the migration, refer to the troubleshooting guide.

Last updated: