Skip to main content

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

1

Add the plugin package if you don't have one yet

nx add @nx/plugin
nx g @nx/plugin:plugin tools/my-plugin
2

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:
nx run my-app:echo
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

APIDescription
ExecutorContextContext 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
CustomHasherType for a custom hash function
HasherContextContext passed to a custom hasher

Build docs developers (and LLMs) love