Every major version of Nx removes something, but most of the time the removal is small enough that a migration takes care of it while you are getting a coffee. Nx v24, which ships in a few weeks, is different. It removes the built-in executors that were deprecated in Nx v23, and for a lot of workspaces that means a real change in how tasks are defined. If your project.json files reference @nx/jest:jest, @nx/webpack:webpack, or any of their siblings, this affects you.
The change was announced in advance, and every affected executor has been printing a deprecation warning since Nx v23.0.0. Still, "we told you so" is not a migration strategy. For most workspaces the biggest difference after the switch is less configuration to maintain, along with a set of features that were never possible with executors. This post explains where executors came from, why inferred tasks replace them, and how to convert an entire workspace.
Brief history of executors
Executors are almost as old as Nx itself. They started as builders, a concept borrowed from Angular, and were renamed to executors with the introduction of Nx Devkit. Their purpose has not changed since: bake the custom behavior a specific tool needs into the workspace, behind a uniform contract. An executor receives a set of options from project configuration, does whatever the tool needs, and reports success or failure. Because every task goes through the same contract, Nx can collect its logs, cache it, orchestrate it, distribute it across machines, and retry it, without knowing anything about the tool used within.
That contract is also where the problems started. An executor is essentially a wrapper, and a wrapper has to know the shape of the thing it wraps. Every option of the underlying tool needs a matching entry in the executor schema, every new flag upstream needs a release on our side, and every breaking change in the tool needs a matching change in the executor. It also leaves you with two sources of truth. The tool has its own config file and the executor has its options in project.json, and the two have to agree. When they do not, the executor usually wins and the config file silently lies.
Birth of inferred targets
The alternative was introduced with Nx v18 and Project Crystal, and our original post is still the best place for the technical details. With inferred tasks, instead of wrapping a tool, a plugin reads the configuration the tool already has and infers the targets from it. A vite.config.ts in a project means the project gets build, serve, and preview targets. A jest.config.ts means a test target. An eslint.config.ts will get you a lint target. The plugin knows enough about the tool to derive the correct inputs, outputs, and cache settings from that config, and the target itself runs the native CLI through nx:run-commands.
That last part is what makes the difference. There is no executor wrapper between you and the tool anymore. Passing --coverage to nx test my-app passes --coverage to Vitest. Upgrading to a new major version of React is a package.json change, not a wait for the next Nx release. The plugin is a shallow layer, which means it has less to break and less to maintain.
It also makes plugins far less invasive. Drop one on top of an existing workspace and it infers the targets from the configuration that is already there. Remove it, and you can still invoke the tool yourself, because nothing about the tool setup was ever owned by Nx. The only configuration you have to maintain is the part that deviates from what the plugin infers.
Inferred tasks also unlocked features that were awkward or impossible with executors:
- Atomizer. Because the plugin sees every test file, it can generate one target per file and let Nx distribute them across machines. We covered this in detail in 3 Test Splitting Techniques that Cut E2E Times up to 90%.
- Correct caching by default. Inputs and outputs are derived from the actual configuration, not guessed from a convention.
- No configuration boilerplate. A project with tool related configs needs no
project.jsonat all. - Loose coupling. The plugin and the tool can evolve independently, as long as the config file format stays recognizable.
Now that every first-party tool integration has an inferred plugin, keeping the executors around means maintaining two implementations of the same thing. Nx v23 deprecated them, and Nx v24 removes them.
What is being removed
The removal only concerns the executors shipped with the core Nx plugins. The executor API itself is not going anywhere. nx:run-commands is an executor, and it is the executor every inferred target runs through. The @nx/js build executors (tsc, swc, node), @nx/esbuild:esbuild, all @nx/angular executors, and the batch executors for Gradle and Maven stay as well. Executors you wrote yourself, or installed from a community plugin, will continue to work in Nx v24 exactly as they do today. The executor API is the extension point; the built-in executors were just the first thing we built on it.
The following executors were deprecated and will be removed in Nx v24. Each has an inferred plugin counterpart and a convert-to-inferred generator.
@nx/cypress:cypress@nx/detox:build,@nx/detox:test@nx/eslint:lint@nx/expo:build,export,install,prebuild,run,serve,start,submit@nx/jest:jest@nx/next:build,@nx/next:server@nx/playwright:playwright@nx/react-native:build-android,build-ios,bundle,pod-install,run-android,run-ios,start,upgrade@nx/remix:build,@nx/remix:serve@nx/rollup:rollup@nx/rspack:rspack,@nx/rspack:dev-server@nx/storybook:storybook,@nx/storybook:build@nx/vite:build,@nx/vite:dev-server,@nx/vite:preview-server@nx/vitest:test@nx/webpack:webpack,@nx/webpack:dev-server
The Module Federation dev-server executors in @nx/angular, @nx/react, and @nx/rspack are removed as well, but they follow a different migration path described in the Nx v23 release post.
Converting your workspace
The converter is a generator, and it has existed since Nx v19, so there is a good chance you have already run it on part of your workspace. If not, the process has three steps.
First, get on the latest Nx v23 release. The conversion generators in v23.2 and later are significantly faster and handle more edge cases than earlier versions:
npx nx migrate latest
npx nx migrate --run-migrationsSecond, run the conversion. The infer-targets generator finds every installed plugin that has a convert-to-inferred generator and runs all of them:
npx nx g infer-targetsIf you prefer to go one plugin or one project at a time, each plugin exposes its own generator:
npx nx g @nx/eslint:convert-to-inferred --project my-appThird, check the result. The generator registers the plugin in nx.json, compares the target it would infer with the target you had in project.json, and keeps only the difference. Options that the plugin already derives from the config file are removed. Options that were specific to your target are either moved into the tool's config file, added as command-line arguments, or left in project.json as an override. When every project in the workspace shares the same override, the generator hoists it into targetDefaults so it is defined once. A final inference pass verifies that the resulting targets are equivalent to what you had before.
npx nx show project my-app --webThe project details view shows every target, where it came from, and which options it ended up with. A target with the plugin name next to it is inferred. A target that still lists an executor was skipped, and the generator output will tell you why.
If your nx.json has "useInferencePlugins": false, inferred plugins are disabled workspace-wide and the conversion will not do anything useful. Remove the flag, or set it to true, before running the generator.
Two situations need manual attention. Targets defined through package.json scripts are left alone, because the generator cannot know whether the script is meant to be the source of truth. Webpack projects that still use composePlugins and withNx need their config converted to NxAppWebpackPlugin first, which the Nx v23 release post also covers. The conversion guide and the troubleshooting page list the remaining cases we know about.
A note on graph computation
With executors, Nx read targets straight from project.json. With inferred tasks, plugins derive them from your tool configuration, which means a little more work when the project graph is computed. That work happens once, after a configuration change. The daemon caches the result, so the commands you run day to day are served from the cached graph.
If you tried the conversion in its early days, you may remember that work being more noticeable. A large part of the Nx v22 and v23 release cycles went into the plugins, the daemon, and the conversion generator, and what remains is small.
If your workspace turns out to be an exception, we want to hear about it. Run NX_PERF_LOGGING=true NX_DAEMON=false nx graph to see where the time goes, and reach out with the output. The performant plugins guide explains how you can improve your own inferred plugins.
Conclusion
Executors carried Nx for the better part of a decade, and they are the reason caching and distribution worked for tools that had no idea what a monorepo was. Inferred tasks keep that contract and drop the part that got in the way, which is why Nx v24 removes the built-in executors deprecated in Nx v23.
The conversion is a single generator, it runs in seconds, and it leaves your workspace with less configuration than it had before. Run it on Nx v23, verify the result with nx show project, and you are ready for Nx v24 before it lands. If the conversion does not go as described here, open an issue or find us on Discord and we will look at it together.










