Creating Executors for your workspace standardizes scripts that are run during your development/building/deploying tasks in order to provide guidance in the terminal with --help and when invoking with Nx Console
This guide shows you how to create, run, and customize executors within your Nx workspace. The examples use the trivial use-case of an echo command.
Creating an executor
Section titled “Creating an executor”If you don't already have a plugin, use Nx to generate one:
nx add @nx/pluginnx g @nx/plugin:plugin tools/my-pluginUse the Nx CLI to generate the initial files needed for your executor.
nx generate @nx/plugin:executor tools/my-plugin/src/executors/echoAfter the command is finished, the executor is created in the plugin executors folder.
Directoryhappynrwl/
Directoryapps/
- …
Directorytools/
Directorymy-plugin/
Directorysrc/
Directoryexecutors/
Directoryecho/
- executor.spec.ts
- executor.ts
- schema.d.ts
- schema.json
- nx.json
- package.json
- tsconfig.base.json
schema.json
Section titled “schema.json”This file describes the options being sent to the executor (very similar to the schema.json file of generators). Setting the cli property to nx indicates that you're using the Nx Devkit to make this executor.
{ "$schema": "https://json-schema.org/schema", "type": "object", "properties": { "textToEcho": { "type": "string", "description": "Text To Echo" } }}This example describes a single option for the executor that is a string called textToEcho. When using this executor, specify a textToEcho property inside the options.
In our executor.ts file, we're creating an Options interface that matches the json object being described here.
executor.ts
Section titled “executor.ts”The executor.ts contains the actual code for your executor. Your executor's implementation must export a function that takes an options object and returns a Promise<{ success: boolean }>.
import type { ExecutorContext } from '@nx/devkit';import { exec } from 'child_process';import { promisify } from 'util';
export interface EchoExecutorOptions { textToEcho: string;}
export default async function echoExecutor( options: EchoExecutorOptions, context: ExecutorContext): Promise<{ success: boolean }> { console.info(`Executing "echo"...`); console.info(`Options: ${JSON.stringify(options, null, 2)}`);
const { stdout, stderr } = await promisify(exec)( `echo ${options.textToEcho}` ); console.log(stdout); console.error(stderr);
const success = !stderr; return { success };}Running your executor
Section titled “Running your executor”Our last step is to add this executor to a given project's targets object in your project's project.json file:
{ //... "targets": { // ... "echo": { "executor": "@my-org/my-plugin:echo", "options": { "textToEcho": "Hello World", }, }, },}Finally, you run the executor via the CLI as follows:
nx run my-project:echoTo which we'll see the console output:
Executing "echo"...Options: { "textToEcho": "Hello World"}Hello WorldUsing node child process
Section titled “Using node child process”Node.js childProcess is often useful in executors.
Part of the power of the executor API is the ability to compose executors via existing targets. This way you can combine other executors from your workspace into one which could be helpful when the process you're scripting is a combination of other existing executors provided by the CLI or other custom executors in your workspace.
Here's an example of this (from a hypothetical project), that serves an api (project name: "api") in watch mode, then serves a frontend app (project name: "web-client") in watch mode:
import { ExecutorContext, runExecutor } from '@nx/devkit';
export interface MultipleExecutorOptions {}
export default async function multipleExecutor( options: MultipleExecutorOptions, context: ExecutorContext): Promise<{ success: boolean }> { const result = await Promise.race([ await runExecutor( { project: 'api', target: 'serve' }, { watch: true }, context ), await runExecutor( { project: 'web-client', target: 'serve' }, { watch: true }, context ), ]); for await (const res of result) { if (!res.success) return res; }
return { success: true };}For other ideas on how to create your own executors, you can always check out Nx own open-source executors as well!
Example repository/nrwl/nx/blob/master/packages/js/src/executors/tsc/tsc.impl.ts
Using custom hashers
Section titled “Using custom hashers”Custom hashers are deprecated and will be removed in Nx 25. Declare what a task depends on as target inputs instead. Nx prints a warning once for each executor that declares a hasher, when it first hashes one of its tasks.
A custom hasher replaces the hash Nx computes from a task's inputs. Executors use them when a target depends on a small subset of the files in a project, or on data stored outside the project. Target inputs cover those cases without code.
Replace a custom hasher with inputs
Section titled “Replace a custom hasher with inputs”Read what your hasher hashes and declare each value as an input on the targets that use the executor. Inputs cover files and filesets, other projects, environment variables, and the output of a command. For every input type and its syntax, see the inputs reference.
To apply the inputs to every target that uses the executor, set them in targetDefaults, keyed by the executor name:
{ "targetDefaults": { "@my-org/my-plugin:echo": { "inputs": [ "default", { "fileset": "{workspaceRoot}/schema.json" }, { "runtime": "node --version" } ] } }}A target that declares its own inputs replaces the inputs from targetDefaults instead of merging with them. Add the new inputs to each of those targets as well, in project.json or the nx.targets block of package.json. Run nx show target <project>:<target> for every target that uses the executor and check that each one lists the new inputs. Once every target covers everything the hasher hashed, delete the hasher file and remove hasher from executors.json.
How custom hashers work
Section titled “How custom hashers work”An executor declares a custom hasher with the hasher property in executors.json:
{ "executors": { "echo": { "implementation": "./src/executors/my-executor/executor", "hasher": "./src/executors/my-executor/hasher", "schema": "./src/executors/my-executor/schema.json" } }}The hasher module exports a function that returns the hash for a task. The hasher below mimics Nx default hashing:
import { CustomHasher, Task, HasherContext } from '@nx/devkit';
export const mimicNxHasher: CustomHasher = async ( task: Task, context: HasherContext) => { return context.hasher.hashTask(task);};
export default mimicNxHasher;Nx uses the returned hash as is, so anything that changes the executor's result has to feed into it. A hasher that returns a constant value, such as { value: 'my-static-hash' }, makes every run of the task a cache hit.