How to Automate Dependency Monitoring with n8n
The Problem
Outdated dependencies rarely cause trouble until the day they do. A deprecated package quietly stops getting security fixes, a major version gap turns a five minute upgrade into a two week migration, and nobody remembers to run npm outdated. Services like Dependabot help but generate noisy pull requests and cannot give a single prioritized view across repos. This guide builds a lightweight weekly check with n8n. It reads package.json from GitHub, asks the npm registry about every dependency, compares versions with semver logic, and sends a ranked list to Slack and email.
What You Will Need
- A self-hosted n8n instance (Community Edition is free) or an n8n Cloud trial
- A GitHub repository with a package.json file (and a read-only token if it is private)
- A Slack app with the
chat:writescope, invited to #engineering - SMTP credentials for the Send Email node
- Estimated setup time: 45 minutes
How the n8n Workflow Fits Together
This n8n workflow turns a package.json file into a ranked upgrade list. It has nine nodes: a Schedule Trigger, two HTTP Request nodes, two Code nodes, a Split Out node, an IF node, a Slack node, and a Send Email node. The first half gathers data and the second half makes decisions, which is a common pattern for n8n monitoring jobs.
The unusual part is that all version logic runs inside n8n itself. There is no external dependency scanner involved, so the n8n workflow can run on a small server, and you can change the priority rules by editing a few lines of JavaScript.
Step-by-Step Workflow Setup
1 Schedule the Monday check in n8n
Dependency drift is a slow problem, so a weekly cadence is enough. A Schedule Trigger set to Monday at 8am means the team sees the upgrade list at the start of the work week, when there is time to act on it.
Add an n8n Schedule Trigger node and configure:
Node: Schedule Trigger
Trigger Interval: Weeks
Weeks Between Triggers: 1
Trigger on Weekdays: Monday
Trigger at Hour: 8
Trigger at Minute: 0
Set the workflow timezone in the n8n workflow settings so 8am means your local 8am. To monitor several repositories, add a Set node after the trigger that lists repo names, then let the next node loop over them.
Expression: {{ $now.toFormat("yyyy-MM-dd") }}
{
"timestamp": "2026-11-09T08:00:00.000-08:00",
"Day of week": "Monday"
}
A manual execution in the n8n editor behaves exactly like a scheduled one, so you can test the whole chain before activating it.
2 Fetch package.json from GitHub in n8n
An HTTP Request node downloads the raw file from the repository. Raw URLs return plain text, which keeps the request light and avoids the base64 decoding that the GitHub contents API requires.
Add an n8n HTTP Request node and configure:
Node: HTTP Request
Method: GET
URL: https://raw.githubusercontent.com/YOUR-ORG/YOUR-REPO/main/package.json
Authentication: Generic Credential Type > Header Auth (private repos only)
Name: Authorization
Value: Bearer ghp_yourFineGrainedToken
Options > Response > Response Format: JSON
For a public repo you can leave authentication off. For a private repo, create a fine-grained personal access token with read-only Contents permission and store it as an n8n Header Auth credential, never inline in the URL.
Expression: https://raw.githubusercontent.com/{{ $json.repo }}/main/package.json
{
"name": "acme-dashboard",
"version": "2.4.1",
"dependencies": {
"express": "^4.17.1",
"@sentry/node": "^6.19.7",
"request": "^2.88.2",
"lodash": "~4.17.15"
},
"devDependencies": {
"jest": "^26.6.3"
}
}
If your default branch is not main, change the path segment or use a ref in a variable. Setting the response format to JSON means n8n parses the file for you, so the next node can read $json.dependencies directly.
3 Parse dependencies and split them out in n8n
A Code node flattens dependencies and devDependencies into a list of name and range pairs, and strips range prefixes like caret and tilde so only a plain version remains. A Split Out node then turns that array into one n8n item per package.
Add an n8n Code, then Split Out node and configure:
Node 1: Code (Run Once for All Items)
const pkg = $input.first().json;
const all = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
const list = Object.entries(all)
.map(([name, range]) => ({ name, range, current: String(range).replace(/^[\^~>=<\s]+/, '') }))
.filter(d => /^\d+\.\d+\.\d+/.test(d.current));
return [{ json: { packages: list } }];
Node 2: Split Out
Fields To Split Out: packages
Include: No Other Fields
The filter drops git URLs, file paths, and workspace references that have no semver version and would only cause 404 errors at the registry. Split Out gives each package its own item, so the next HTTP Request node runs once per dependency.
Expression: {{ $json.name }} @ {{ $json.current }}
{
"name": "@sentry/node",
"range": "^6.19.7",
"current": "6.19.7"
}
Peer dependencies are not included on purpose. Add pkg.peerDependencies to the merge if you want them tracked as well.
4 Query the npm registry for each package in n8n
The registry exposes a public JSON document for every package. Requesting the latest tag returns the newest version and, if the maintainers have deprecated it, a deprecated message string. No API key is needed.
Add an n8n HTTP Request node and configure:
Node: HTTP Request
Method: GET
URL: https://registry.npmjs.org/{{ $json.name.replace("/", "%2F") }}/latest
Authentication: None
Options > Batching:
Items per Batch: 10
Batch Interval (ms): 500
Options > Response > Never Error: On
Scoped packages such as @sentry/node must have the slash encoded as %2F, which the replace expression handles. Batching keeps n8n polite toward the registry when a project has hundreds of dependencies. Never Error lets one missing package pass through instead of stopping the whole run.
Expression: {{ $json.name.replace("/", "%2F") }}
{
"name": "request",
"version": "2.88.2",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142"
}
The registry returns deprecated only when a deprecation exists, so an absent field means the package is healthy.
5 Compare versions with semver logic in n8n
This Code node is the core of the workflow. It splits each version into major, minor and patch numbers, compares them numerically, and assigns a priority. The logic is small enough to live inside n8n with no npm package required.
Add an n8n Code node and configure:
Node: Code (Run Once for All Items)
const parse = v => String(v).split('-')[0].split('.').map(n => parseInt(n, 10) || 0);
const rows = [];
$input.all().forEach((item, i) => {
const base = $('Split Out').itemMatching(i).json;
const cur = parse(base.current), lat = parse(item.json.version);
let gap = 'current';
if (lat[0] > cur[0]) gap = 'major';
else if (lat[1] > cur[1]) gap = 'minor';
else if (lat[2] > cur[2]) gap = 'patch';
const deprecated = Boolean(item.json.deprecated);
const priority = deprecated ? 'P0' : gap === 'major' ? 'P1' : gap === 'minor' ? 'P2' : gap === 'patch' ? 'P3' : 'OK';
rows.push({ name: base.name, current: base.current, latest: item.json.version,
gap, deprecated, priority, note: item.json.deprecated || '' });
});
const order = { P0: 0, P1: 1, P2: 2, P3: 3, OK: 4 };
rows.sort((a, b) => order[a.priority] - order[b.priority]);
const critical = rows.filter(r => r.priority === 'P0' || r.priority === 'P1');
return [{ json: { rows: rows.filter(r => order[r.priority] < 4), criticalCount: critical.length, total: rows.length } }];
Comparing arrays numerically matters: as strings, "10.0.0" would sort below "9.0.0". Pre-release suffixes after a hyphen are stripped, which treats 5.0.0-beta as 5.0.0. The itemMatching(i) call pairs each registry response with the package it came from, even when upstream items are skipped.
Expression: {{ $json.criticalCount }} critical of {{ $json.total }} packages
{
"rows": [
{ "name": "request", "current": "2.88.2", "latest": "2.88.2", "gap": "current", "deprecated": true, "priority": "P0" },
{ "name": "@sentry/node", "current": "6.19.7", "latest": "8.34.0", "gap": "major", "deprecated": false, "priority": "P1" },
{ "name": "lodash", "current": "4.17.15", "latest": "4.17.21", "gap": "patch", "deprecated": false, "priority": "P3" }
],
"criticalCount": 2,
"total": 5
}
Priorities work as a rule of thumb: P0 is deprecated, P1 is a major version behind, P2 a minor behind, and P3 a patch behind. Adjust the mapping if your team treats security patches as urgent.
6 Alert Slack when something is critical in n8n
An IF node checks whether any P0 or P1 package exists. The true branch posts a prioritized list to Slack; the false branch continues straight to the email so a quiet week still produces a record.
Add an n8n IF, then Slack node and configure:
Node 1: IF
Conditions: Number
Value 1: {{ $json.criticalCount }}
Operation: is greater than
Value 2: 0
Node 2: Slack (true branch)
Resource: Message
Operation: Send
Channel: #engineering
Text:
*{{ $json.criticalCount }} critical dependency updates this week*
{{ $json.rows.filter(r => r.priority === "P0" || r.priority === "P1").map(r => r.priority + " " + r.name + " " + r.current + " to " + r.latest).join("\n") }}
The expression inside the Slack node runs plain JavaScript, so you can filter and map the rows without another Code node. Slack renders asterisks as bold and newlines as line breaks.
Expression: {{ $json.rows.filter(r => r.priority === "P0").length }} deprecated
{
"ok": true,
"channel": "C03ENGINEER",
"message": {
"text": "*2 critical dependency updates this week*\nP0 request 2.88.2 to 2.88.2\nP1 @sentry/node 6.19.7 to 8.34.0"
}
}
Mention a group handle such as the on-call engineer inside the text if you want the alert to page someone.
7 Email the weekly dependency report in n8n
Finally, a Send Email node mails the full table through SMTP. Unlike the Slack alert, the email lists every outdated package, including low priority patches, so it works as the permanent weekly record.
Add an n8n Send Email node and configure:
Node: Send Email
Credential: SMTP (host, port 587, user, app password)
From Email: deps@yourcompany.com
To Email: engineering@yourcompany.com
Subject: Weekly dependency report: {{ $json.criticalCount }} critical, {{ $json.rows.length }} outdated
Email Format: HTML
HTML:
<table><tr><th>Priority</th><th>Package</th><th>Current</th><th>Latest</th></tr>
{{ $json.rows.map(r => "<tr><td>" + r.priority + "</td><td>" + r.name + "</td><td>" + r.current + "</td><td>" + r.latest + "</td></tr>").join("") }}
</table>
Connect both outputs of the IF node to this node, or add a Merge node set to Append, so the email sends whether or not Slack fired. Keep the table plain HTML because many mail clients strip modern CSS.
Expression: {{ $json.rows.length }} outdated packages
{
"accepted": ["engineering@yourcompany.com"],
"rejected": [],
"response": "250 2.0.0 OK queued"
}
If your SMTP provider requires TLS on port 465, enable SSL in the credential instead of using 587.
Extending This n8n Workflow
After a few runs, consider these upgrades to the n8n workflow. Store each week's rows in Google Sheets or a database so you can chart how long a package stays outdated. Add a second n8n branch that opens a GitHub issue for every P0 package through the HTTP Request node and the issues API, so deprecations get an owner. Finally, read package-lock.json instead of package.json for exact installed versions, as noted below in troubleshooting.
Because the report is built from structured rows, the same data can feed a dashboard or a pull request bot later without changing the n8n nodes that collect it.
Troubleshooting
Scoped packages return 404 from the registry
The slash in names like @sentry/node must be encoded. Use {{ $json.name.replace("/", "%2F") }} in the URL, and confirm the final request in the node output panel. A plain slash makes the registry look for a nested path that does not exist.
Some dependencies are missing from the report
The filter in the parse step drops any version that is not a plain number triplet, such as latest, workspace:*, or git URLs. If you want those shown, remove the filter and push them to a separate section labeled unchecked.
Versions look wrong because the range is wider than the installed version
A package.json range like ^4.17.1 does not tell you what is actually installed. For exact results, fetch package-lock.json instead and read the resolved versions from its packages map, then feed those into the same semver Code node.
Free n8n Workflow Resources
Get started with the free workflow pack on the LightCube Labs templates page.
Production-Ready Workflow Packs
Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.
View the Starter ($29) and Pro ($99) PacksFrequently Asked Questions
Does this detect security vulnerabilities?
Not directly. It flags deprecation and version distance. For known CVEs, add one more HTTP Request node that posts the dependency list to the GitHub advisory or OSV API and merge the findings into the priority logic.
Can it monitor a monorepo with several package.json files?
Yes. Put the file paths in a Set node before the fetch step, loop over them with a Split Out node, and include the path in each row so the Slack and email output groups by workspace.
Why compare versions in a Code node rather than use the semver package?
Self-hosted n8n blocks external modules in the Code node unless you allow them with an environment variable. The number comparison here covers major, minor and patch with no installation, which keeps the workflow portable to n8n Cloud.
Will this hit npm rate limits?
The public registry tolerates normal read traffic. The batching option of ten items every 500 ms keeps even large dependency trees well within polite use.