Azure Pipelines legacy Node runners are scheduled for removal from the agent on November 24, 2026. The affected versions are Node.js 6, 10, and 16. This is a task-execution change: a .NET application can target no JavaScript at all and still fail because a custom or marketplace pipeline task declares an old Node handler.
Azure DevOps now provides a preview restriction that runs affected tasks on a newer available runner and writes warnings to the pipeline log. Use it as an early-compatibility test, not as proof that every task is safe. This Azure Pipelines Node runner audit separates application runtime from task runtime, inventories custom task manifests, turns provider warnings into an ownership list, and upgrades custom tasks to Node20_1 before the removal date.
Table of Contents
Plan the Azure Pipelines Node runner audit
An Azure Pipelines task is an extension executed by the agent. Its task.json manifest selects a handler under execution. That handler is independent from all of these application choices:
- The Node.js version installed by a
NodeToolorUseNodestep. - The JavaScript runtime used by an ASP.NET Core front end.
- The .NET SDK selected by
UseDotNet@2. - The agent process version and operating system.
Changing the application’s Node version does not rewrite a task manifest. Conversely, upgrading a task handler should not change the Node version used by the application build. Keep those inventories separate so the remediation reaches the component that will actually be removed.
Microsoft’s Sprint 279 release notes name Node.js 6, 10, and 16 and set November 24, 2026 as the scheduled removal date. The notes also warn that a task written for an unsupported runner can fail or behave unexpectedly when forced onto a newer runner. Treat a warning-free canary as evidence for the tested task versions and inputs, not as a universal compatibility guarantee.
Audit custom task.json execution handlers
Start with task source owned by your organization. Search task manifests, not every JSON file in the application. The following script reports execution-handler keys and fails when it finds the common legacy handlers Node, Node10, or Node16:
#!/usr/bin/env bash
set -euo pipefail
legacy_found=0
while IFS= read -r -d '' manifest; do
while IFS= read -r handler; do
printf '%s\t%s\n' "$manifest" "$handler"
case "$handler" in
Node|Node10|Node16)
legacy_found=1
;;
esac
done < <(jq -r '.execution // {} | keys[]' "$manifest")
done < <(find ./extensions -type f -name task.json -print0)
if (( legacy_found )); then
echo "Legacy Azure Pipelines task handler found." >&2
exit 1
fi
Adjust ./extensions to the directory that contains task source. The script deliberately does not scan package-lock.json for Node version strings; dependency metadata cannot tell you which Azure Pipelines execution handler the agent selects.
For every match, record the task GUID and name, extension publisher, manifest version, owning team, pipelines that invoke it, and whether the repository contains the source. A YAML reference such as MyTask@2 identifies a task major version, not its Node handler. You need either the source manifest or the provider warning to establish the runner.
Enable the provider preview as a canary
In Azure DevOps, open Organization settings → Pipelines → Settings. Under Task restrictions, enable Restrict out of support Node.js versions in pipeline tasks. The setting causes tasks that target Node.js 6, 10, or 16 to run on a newer available runner and adds warnings for affected tasks.
Enable it first where you can observe representative pipelines without blocking every delivery path. Queue one canary for each task version and important input mode. Capture the complete log even when the job succeeds, because the warning is the discovery signal. A successful compile after a skipped code path is not sufficient.
| Canary result | Interpretation | Next action |
|---|---|---|
| Warning and task succeeds | The manifest targets a retired runner; the tested path happened to work on the newer runner. | Upgrade the manifest and dependencies, then retest. |
| Warning and task fails | The task has a real compatibility problem or unsupported dependency. | Keep the restriction canary isolated; repair the task before organization-wide enablement. |
| No warning, task succeeds | No affected handler was observed for that task version in this run. | Confirm the expected task version actually executed. |
| Marketplace task warns | The extension publisher owns the implementation. | Upgrade to a fixed version or replace the task; do not patch downloaded agent files. |
Preserve test and diagnostic artifacts even when a job fails. The bounded .NET test-run guide for Azure Pipelines shows how job and test timeouts affect the evidence available after a failure.
Upgrade a custom task to Node20_1
Microsoft’s current custom-task guidance recommends Node.js 20 or later and uses the Node20_1 execution handler. Change the manifest only after confirming the bundled JavaScript and dependencies run on that runtime.
{
"id": "4dcf0c2d-3b8f-4c6b-8916-5ff9d4f3382c",
"name": "ValidateDotNetArtifact",
"friendlyName": "Validate .NET artifact",
"version": {
"Major": 3,
"Minor": 0,
"Patch": 0
},
"execution": {
"Node20_1": {
"target": "dist/index.js"
}
}
}
The GUID above is illustrative; keep the stable GUID of an existing task. Follow your extension’s versioning policy and test how consumers select the changed major version. Do not copy a new GUID into an upgrade and accidentally publish a second unrelated task.
Compile the task to JavaScript that Node 20 can execute, bundle production dependencies inside the task folder, and review native modules carefully. Azure Pipelines does not run npm install for a packaged task at execution time. A manifest update that points to unbundled or incompatible code simply moves the failure.
npm ci
npx tsc --project tsconfig.json
npm prune --omit=dev
test -f dist/index.js
jq -e '.execution.Node20_1.target == "dist/index.js"' task.json
If the task uses deprecated Node APIs, old TLS defaults, native add-ons, or libraries that no longer support Node 20, update those dependencies and add tests for the affected path. Keep the change scoped: runner compatibility, task behavior, and permissions should be reviewed separately.
Test the packaged task before rollout
Unit tests should cover success, invalid input, service failure, and emitted logging commands. Then package the exact extension artifact that will be installed and inspect it before upload:
npm test
tfx extension create --manifest-globs vss-extension.json
vsix="$(find . -maxdepth 1 -name '*.vsix' -print -quit)"
test -n "$vsix"
unzip -l "$vsix" | grep -E 'task\.json|dist/index\.js'
Install the package into a nonproduction Azure DevOps organization or private sharing scope. Queue the same input matrix used by production, including authentication failures and empty outputs. Compare task result, warnings, variables, artifacts, service calls, and cleanup behavior. A task that exits zero but stops emitting an output variable is still incompatible.
Handle marketplace and self-hosted tasks
Marketplace tasks require a different path because your pipeline repository does not own their task.json. Use the warning to capture publisher, task name, and major version. Check for an updated extension, read the publisher’s compatibility notes, and test the replacement in a canary. If no supported version exists, replace the task with a maintained alternative or an explicit script whose runtime you control.
Do not rely on a self-hosted agent retaining obsolete runners indefinitely. That postpones the compatibility work and leaves the task tied to unsupported software. Also avoid treating NodeTaskRunnerInstaller as the final remediation: it can help with temporary compatibility scenarios, but it does not convert an old task to a supported handler.
Inventory agent pools separately. Microsoft-hosted, Managed DevOps Pool, and self-hosted agents can move on different timelines, but the task package should be supportable across every pool on which it is allowed to run. Record the tested agent version and image with the canary evidence.
Roll out with evidence and rollback
Roll out one task major version at a time. Update a small set of pipelines, enable the restriction, and compare output with the previous version. Monitor task failures, duration, warning count, generated artifacts, output variables, and downstream deployment gates.
The rollback is the previous extension version or task-major reference, provided that version remains available and the agent still contains its runner. That rollback window closes when Azure Pipelines removes Node.js 6, 10, and 16, so finish validation before November 24. Keep a script-based emergency path only for critical operations and subject it to the same secret, permission, and output checks.
Adoption checklist
- Separate application Node versions from Azure Pipelines task handlers.
- Scan owned
task.jsonfiles forNode,Node10, andNode16. - Enable the organization task restriction first as a bounded canary.
- Capture task name, version, publisher, owner, pool, and warning evidence.
- Move owned tasks to
Node20_1and test bundled dependencies. - Package and inspect the exact VSIX artifact before installation.
- Upgrade or replace marketplace tasks through their publisher-supported path.
- Canary output variables, artifacts, service calls, and failure behavior—not only exit codes.
- Record a rollback version before the old runners are removed.
- Complete the migration before November 24, 2026.
References
- Microsoft Learn — Azure DevOps Sprint 279 update
- Microsoft Learn — Restrict out-of-support Node.js versions in pipeline tasks
- Microsoft Learn — Add a custom pipelines task extension
- Microsoft DevOps Blog — Node runner update guidance for Azure Pipelines task authors
Found this useful? Support more practical developer content.