Documentation Index
Fetch the complete documentation index at: https://mintlify.com/nrwl/nx/llms.txt
Use this file to discover all available pages before exploring further.
Executors are functions that run a task — a build, a test run, a deployment step, or anything else that can be scripted. Defining a task as an executor gives you:
- Schema validation for options, with
--help output in the terminal.
- Nx caching — Nx can cache executor outputs and replay them on cache hits.
- Composability — executors can invoke other executors via
runExecutor.
- Nx Console integration — options are rendered as a form in the editor.
Creating an executor
Add the plugin package if you don't have one yet
nx add @nx/plugin
nx g @nx/plugin:plugin tools/my-plugin
Scaffold a new executor
nx generate @nx/plugin:executor tools/my-plugin/src/executors/echo
This creates the following files:tools/my-plugin/src/executors/echo/
├── executor.spec.ts # Unit tests
├── executor.ts # Implementation
├── schema.d.ts # TypeScript interface for options
└── schema.json # JSON Schema for validation
Defining the schema
schema.json describes the options your executor accepts:
// tools/my-plugin/src/executors/echo/schema.json
{
"$schema": "https://json-schema.org/schema",
"type": "object",
"properties": {
"textToEcho": {
"type": "string",
"description": "The text to echo to the console"
}
},
"required": ["textToEcho"]
}
The TypeScript interface in schema.d.ts mirrors the JSON schema:
// tools/my-plugin/src/executors/echo/schema.d.ts
export interface EchoExecutorOptions {
textToEcho: string;
}
Executor function signature
Every executor exports a default async function that receives the resolved options and an ExecutorContext. It must return Promise<{ success: boolean }>.
// tools/my-plugin/src/executors/echo/executor.ts
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 };
}
Accessing project information via context
The ExecutorContext object contains the full project graph and the name of the project and target being run:
import type { ExecutorContext } from '@nx/devkit';
export default async function myExecutor(
options: MyExecutorOptions,
context: ExecutorContext
): Promise<{ success: boolean }> {
// The name of the project being built
const projectName = context.projectName;
// Full project configuration (root, sourceRoot, targets, tags, etc.)
const projectConfig = context.projectsConfigurations.projects[projectName];
const projectRoot = projectConfig.root;
// The full Nx project graph
const graph = context.projectGraph;
// Dependencies of this project
const deps = graph.dependencies[projectName] ?? [];
console.log(`Building ${projectName} at ${projectRoot}`);
console.log(`Has ${deps.length} dependencies`);
return { success: true };
}
Registering an executor in project.json
Add the executor to the relevant project’s targets block in project.json:
// apps/my-app/project.json
{
"name": "my-app",
"targets": {
"echo": {
"executor": "@myorg/my-plugin:echo",
"options": {
"textToEcho": "Hello World"
}
}
}
}
Use the name field from tools/my-plugin/package.json when referencing your plugin — not the folder path.
Then run the executor:
Expected output:
Executing "echo"...
Options: {
"textToEcho": "Hello World"
}
Hello World
Composing executors with runExecutor
Executors can invoke other executors. This is useful when a task is a combination of multiple steps:
// tools/my-plugin/src/executors/serve-all/executor.ts
import { ExecutorContext, runExecutor } from '@nx/devkit';
export interface MultipleExecutorOptions {}
export default async function serveAllExecutor(
options: MultipleExecutorOptions,
context: ExecutorContext
): Promise<{ success: boolean }> {
// Run both servers concurrently; stop when the first one finishes
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 };
}
Custom hashers
By default, Nx hashes all files in a project to determine cache validity. If your executor only depends on a subset of files, or on external data not in the project, you can provide a custom hasher.
Generate an executor with a hasher included:
nx g @nx/plugin:executor tools/my-plugin/src/executors/my-executor --includeHasher
Or add a hasher manually by creating hasher.ts and registering it in executors.json:
// tools/my-plugin/executors.json
{
"executors": {
"my-executor": {
"implementation": "./src/executors/my-executor/executor",
"hasher": "./src/executors/my-executor/hasher",
"schema": "./src/executors/my-executor/schema.json"
}
}
}
The hasher receives the full Task and a HasherContext:
// tools/my-plugin/src/executors/my-executor/hasher.ts
import { CustomHasher, Task, HasherContext } from '@nx/devkit';
// This hasher delegates to the default Nx algorithm
export const myHasher: CustomHasher = async (
task: Task,
context: HasherContext
) => {
return context.hasher.hashTask(task);
};
export default myHasher;
A custom hasher replaces the default hashing — it does not extend it. If the hash never changes, every run of that target will be a cache hit. Make sure all inputs that affect the executor’s output are included in your hash.
Key devkit utilities for executors
| API | Description |
|---|
ExecutorContext | Context object passed to every executor; contains project graph, project name, and workspace root |
runExecutor(target, options, context) | Invoke another executor from within an executor |
parseTargetString(str) | Parse a project:target:configuration string into a Target object |
CustomHasher | Type for a custom hash function |
HasherContext | Context passed to a custom hasher |