Nx Executors Guide
What Are Executors?
Executors define reusable build, test, and development tasks. They encapsulate task logic that can be shared across multiple projects in a workspace with cache-aware execution.
Executor Implementation
Basic structure:
import { ExecutorContext } from '@nx/devkit';
import { ExecutorSchema } from './schema';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
console.log('Executing task with options:', options);
console.log('Project:', context.projectName);
try {
// Implementation logic here
return { success: true };
} catch (error) {
console.error('Execution failed:', error);
return { success: false };
}
}
ExecutorContext API
Available properties:
interface ExecutorContext {
root: string; // Workspace root
cwd: string; // Current working directory
projectName: string; // Current project name
targetName: string; // Current target name
configurationName?: string; // Active configuration
workspace: WorkspaceJsonConfiguration;
projectsConfigurations: ProjectsConfigurations;
nxJsonConfiguration: NxJsonConfiguration;
isVerbose: boolean;
}
Common usage:
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const projectRoot = project.root;
const workspaceRoot = context.root;
console.log(`Running ${context.targetName} for ${context.projectName}`);
console.log(`Project root: ${projectRoot}`);
console.log(`Workspace root: ${workspaceRoot}`);
// Implementation
return { success: true };
}
Schema Definition
schema.json:
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"title": "My Executor",
"description": "Execute something useful",
"type": "object",
"properties": {
"outputPath": {
"type": "string",
"description": "Output directory"
},
"watch": {
"type": "boolean",
"description": "Watch for changes",
"default": false
},
"port": {
"type": "number",
"description": "Port number",
"default": 3000
}
},
"required": ["outputPath"]
}
schema.d.ts:
export interface ExecutorSchema {
outputPath: string;
watch?: boolean;
port?: number;
}
Executor Registration
executors.json:
{
"executors": {
"build": {
"implementation": "./src/executors/build/executor",
"schema": "./src/executors/build/schema.json",
"description": "Build the project"
},
"serve": {
"implementation": "./src/executors/serve/executor",
"schema": "./src/executors/serve/schema.json",
"description": "Serve the project"
}
}
}
Target Configuration
project.json:
{
"name": "my-app",
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/my-app"
},
"configurations": {
"production": {
"optimization": true,
"sourceMap": false
}
}
}
}
}
Cache Configuration
Declare outputs for caching:
{
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"outputs": [
"{options.outputPath}",
"{workspaceRoot}/dist/{projectRoot}"
],
"options": {
"outputPath": "dist/apps/my-app"
}
}
}
}
Declare inputs for cache invalidation:
{
"targets": {
"build": {
"executor": "@my-org/my-plugin:build",
"inputs": [
"default",
"{workspaceRoot}/tsconfig.base.json"
],
"outputs": ["{options.outputPath}"]
}
}
}
Async File Operations
Use async I/O:
import { promises as fs } from 'fs';
import { join } from 'path';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const filePath = join(context.root, project.root, 'package.json');
try {
const content = await fs.readFile(filePath, 'utf-8');
const pkg = JSON.parse(content);
// Process package.json
await fs.writeFile(
join(options.outputPath, 'manifest.json'),
JSON.stringify(pkg, null, 2)
);
return { success: true };
} catch (error) {
console.error('File operation failed:', error);
return { success: false };
}
}
Watch Mode Support
Implement watch mode:
import { watch } from 'fs';
export default async function runExecutor(
options: ExecutorSchema,
context: ExecutorContext
): Promise<{ success: boolean }> {
const project = context.projectsConfigurations.projects[context.projectName];
const watchPath = join(context.root, project.root, 'src');
if (options.watch) {
console.log(`Watching ${watchPath} for changes...`);
watch(watchPath, { recursive: true }, async (eventType, filename) => {
console.log(`File ${filename} changed, rebuilding...`);
await build(options, context);
});
return { success: true };
}
return await build(options, context);
}
async function build(options: ExecutorSchema, context: ExecutorContext) {
// Build logic
return { success: true };
}
Workspace Setup
Enable package references:
- Add to root
package.json:
{
"workspaces": [
"packages/*",
"tools/*"
]
}
- Add to
tsconfig.base.json:
{
"compilerOptions": {
"paths": {
"@my-org/tools": ["tools/src/index.ts"]
}
}
}
- Create entry point
tools/src/index.ts:
export * from './executors/build/executor';
export * from './executors/serve/executor';
- Set main field in
tools/package.json:
{
"name": "@my-org/tools",
"main": "./src/index.ts",
"executors": "./executors.json"
}
Testing
Unit test example:
import { ExecutorContext } from '@nx/devkit';
import executor from './executor';
import { ExecutorSchema } from './schema';
const options: ExecutorSchema = {
outputPath: 'dist/test'
};
const context: ExecutorContext = {
root: '/workspace',
cwd: '/workspace',
projectName: 'test-project',
targetName: 'build',
workspace: { version: 2, projects: {} },
projectsConfigurations: {
version: 2,
projects: {
'test-project': {
root: 'apps/test-project'
}
}
},
isVerbose: false
};
describe('Build Executor', () => {
it('should execute successfully', async () => {
const result = await executor(options, context);
expect(result.success).toBe(true);
});
});
Best Practices
- Return success boolean - always return
{ success: boolean } - Use package notation - use
@scope/package:executornot relative paths - Declare outputs - enable cache for deterministic tasks
- Use async I/O - avoid blocking operations
- Delegate logic - keep executors thin, delegate to library functions
- Validate options - check required fields early
- Log clearly - provide actionable output for debugging