Skip to content

Hook into the Task Running Lifecycle

Nx plugins can hook into the task running lifecycle to execute custom logic before and after tasks are run. This is useful for implementing custom analytics, environment validation, or any other pre/post processing that should happen when running tasks.

Nx provides two hooks that plugins can register:

  1. preTasksExecution: Runs before any tasks are executed
  2. postTasksExecution: Runs after all tasks are executed

These hooks allow you to extend Nx's functionality without affecting task execution or violating any invariants.

To implement task execution hooks, create a plugin and export the preTasksExecution and/or postTasksExecution functions:

some-example-hooks.ts
// Example plugin with both pre and post execution hooks
// context contains workspaceRoot and nx.json configuration
export async function preTasksExecution(options: any, context) {
// Run custom logic before tasks are executed
console.log('About to run tasks!');
// You can modify environment variables
if (process.env.QA_ENV) {
process.env.NX_SKIP_NX_CACHE = 'true';
}
// You can validate the environment
if (!isEnvironmentValid()) {
throw new Error('Environment is not set up correctly');
}
}
// context contains workspaceRoot, nx.json configuration, and task results
export async function postTasksExecution(options: any, context) {
// Run custom logic after tasks are executed
console.log('All tasks have completed!');
// You can access task results for analytics
if (options.reportAnalytics) {
await fetch(process.env.ANALYTICS_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(context.taskResults),
});
}
}
function isEnvironmentValid() {
// Implement your validation logic
return true;
}

Configure your plugin in nx.json by adding it to the plugins array:

nx.json
{
"plugins": [
{
"plugin": "my-nx-plugin",
"options": {
"reportAnalytics": true
}
}
]
}

The options you specify in the configuration will be passed to your hook functions.

Maintaining State Across Command Invocations

Section titled “Maintaining State Across Command Invocations”

By default, every plugin initiates a long-running process, allowing you to maintain state across command invocations. This is particularly useful for gathering advanced analytics or providing cumulative feedback.

You can implement conditional logic in your hooks to control when they run:

some-example-hooks.ts
export async function preTasksExecution(options, context) {
// Only run for specific environments
if (process.env.RUNNER !== 'production') return;
// Your pre-execution logic
}
export async function postTasksExecution(options, context) {
// Only run for specific task types
const hasAngularTasks = Object.keys(context.taskResults).some((taskId) =>
taskId.includes('angular')
);
if (!hasAngularTasks) return;
// Your post-execution logic
}

Using context.argv to Determine the Command

Section titled “Using context.argv to Determine the Command”

The context object includes an argv property that contains the original CLI arguments used to invoke Nx. This allows you to distinguish between different execution modes and apply conditional logic.

Both preTasksExecution and postTasksExecution hooks receive a context object with the following properties:

type PreTaskExecutionContext = {
id: string;
workspaceRoot: string;
nxJsonConfiguration: NxJsonConfiguration;
argv: string[]; // Original CLI arguments
};
type PostTasksExecutionContext = {
id: string;
workspaceRoot: string;
nxJsonConfiguration: NxJsonConfiguration;
taskResults: TaskResults;
argv: string[]; // Original CLI arguments
startTime: number;
endTime: number;
};
export async function postTasksExecution(options, context) {
// Check if running affected command
if (context.argv.includes('affected')) {
console.log('✅ Ran affected tasks');
} else if (context.argv.includes('run-many')) {
console.log('✅ Ran tasks for multiple projects');
} else {
console.log('✅ Ran task for specific project');
}
}

Example: Conditional Analytics Based on Command

Section titled “Example: Conditional Analytics Based on Command”
function isAffectedCommand(argv) {
return argv.includes('affected');
}
function getTargetName(argv) {
const targetIndex = argv.findIndex(
(arg) => arg === '-t' || arg === '--target'
);
return targetIndex !== -1 ? argv[targetIndex + 1] : undefined;
}
export async function postTasksExecution(options, context) {
const isAffected = isAffectedCommand(context.argv);
const target = getTargetName(context.argv);
// Send analytics with command context
await sendAnalytics({
executionId: context.id,
commandType: isAffected ? 'affected' : 'direct',
target: target,
taskCount: Object.keys(context.taskResults).length,
duration: context.endTime - context.startTime,
});
}
  • Direct execution: nx build my-appargv: ['node', '/path/to/nx', 'build', 'my-app']
  • Affected: nx affected -t build testargv: ['node', '/path/to/nx', 'affected', '-t', 'build', 'test']
  • Run many: nx run-many -t build -p app1 app2argv: ['node', '/path/to/nx', 'run-many', '-t', 'build', '-p', 'app1', 'app2']

When working with context.argv, parse it defensively:

// Bad - assumes structure
const target = context.argv[2];
// Good - searches for the flag
const targetIndex = context.argv.findIndex(
(arg) => arg === '-t' || arg === '--target'
);
const target = targetIndex !== -1 ? context.argv[targetIndex + 1] : undefined;
  1. Keep hooks fast: Hooks should execute quickly to avoid slowing down the task execution process
  2. Handle errors gracefully: Ensure your hooks don't crash the entire execution pipeline
  3. Use environment variables for configuration that needs to persist across tasks
  4. Leverage context data: Use the context object to access relevant information about the workspace and task results
  5. Provide clear errors: If throwing errors, make sure they are descriptive and actionable
  6. Parse argv defensively: Don't assume the position of arguments; search for specific flags instead