Local Executors

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

If you don't already have a local plugin, use Nx to generate one:

# replace `latest` with the version that matches your Nx version

npm install @nx/plugin@latest

nx g @nx/plugin:plugin my-plugin

Nx 15 and lower use @nrwl/ instead of @nx/

Use the Nx CLI to generate the initial files needed for your executor.

nx generate @nx/plugin:executor echo --project=my-plugin

Nx 15 and lower use @nrwl/ instead of @nx/

After the command is finished, the executor is created in the plugin executors folder.

1happynrwl/ 2├── apps/ 3├── libs/ 4│ ├── my-plugin 5│ │ ├── src 6│ │ │ ├── executors 7│ │ │ | └── echo/ 8│ │ │ | | ├── executor.spec.ts 9│ │ │ | | ├── executor.ts 10│ │ │ | | ├── schema.d.ts 11│ │ │ | | └── schema.json 12├── nx.json 13├── package.json 14└── tsconfig.base.json 15

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.

1{ 2 "$schema": "https://json-schema.org/schema", 3 "type": "object", 4 "properties": { 5 "textToEcho": { 6 "type": "string", 7 "description": "Text To Echo" 8 } 9 } 10} 11

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

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 }>.

1import type { ExecutorContext } from '@nx/devkit'; 2import { exec } from 'child_process'; 3import { promisify } from 'util'; 4 5export interface EchoExecutorOptions { 6 textToEcho: string; 7} 8 9export default async function echoExecutor( 10 options: EchoExecutorOptions, 11 context: ExecutorContext 12): Promise<{ success: boolean }> { 13 console.info(`Executing "echo"...`); 14 console.info(`Options: ${JSON.stringify(options, null, 2)}`); 15 16 const { stdout, stderr } = await promisify(exec)( 17 `echo ${options.textToEcho}` 18 ); 19 console.log(stdout); 20 console.error(stderr); 21 22 const success = !stderr; 23 return { success }; 24} 25
Nx 15 and lower use @nrwl/ instead of @nx/

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:

project.json
1{ 2 //... 3 "targets": { 4 "build": { 5 // ... 6 }, 7 "serve": { 8 // ... 9 }, 10 "lint": { 11 // ,,, 12 }, 13 "echo": { 14 "executor": "@my-org/my-plugin:echo", 15 "options": { 16 "textToEcho": "Hello World" 17 } 18 } 19 } 20} 21

Finally, you run the executor via the CLI as follows:

nx run my-project:echo

To which we'll see the console output:

~/workspace

nx run my-project:echo

1Executing "echo"... 2Options: { 3 "textToEcho": "Hello World" 4} 5Hello World 6
string

Nx uses the paths from tsconfig.base.json when running plugins locally, but uses the recommended tsconfig for node 16 for other compiler options. See https://github.com/tsconfig/bases/blob/main/bases/node16.json

Using Node Child Process

Node’s 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:

1import { ExecutorContext, runExecutor } from '@nx/devkit'; 2 3export interface MultipleExecutorOptions {} 4 5export default async function multipleExecutor( 6 options: MultipleExecutorOptions, 7 context: ExecutorContext 8): Promise<{ success: boolean }> { 9 const result = await Promise.race([ 10 await runExecutor( 11 { project: 'api', target: 'serve' }, 12 { watch: true }, 13 context 14 ), 15 await runExecutor( 16 { project: 'web-client', target: 'serve' }, 17 { watch: true }, 18 context 19 ), 20 ]); 21 for await (const res of result) { 22 if (!res.success) return res; 23 } 24 25 return { success: true }; 26} 27
Nx 15 and lower use @nrwl/ instead of @nx/

For other ideas on how to create your own executors, you can always check out Nx's own open-source executors as well!