n8n Workflow Auditor - Linter & Security Review
Pricing
from $0.20 / workflows audited
n8n Workflow Auditor - Linter & Security Review
Audit n8n workflows for hardcoded credentials, unauthenticated webhooks, expression-injection exposure, missing error handling, deprecated nodes, and unreachable branches. Paste workflow JSON or connect an n8n instance and get one ranked finding per row, plus a health score out of 100 per workflow.
Pricing
from $0.20 / workflows audited
Rating
0.0
(0)
Developer
Mediocre_Interest
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
n8n Workflow Auditor — Linter & Security Review for Workflow JSON
Paste an n8n workflow — or point the Actor at your own n8n instance — and get a ranked list of security, reliability, and correctness findings, one row per issue, plus a health score out of 100 per workflow.
It catches the things that only show up in production: an API key typed into a node parameter instead of a credential, a webhook anyone can call, a typeVersion two majors behind, a branch that no trigger can reach, and a workflow with no error handling anywhere.
- Batch. Audit one pasted workflow, a list of exports, or every workflow on an instance in a single run.
- Read-only. Against an instance it calls
GET /api/v1/workflowsand nothing else, and n8n returns credentials as name references without their secret values.
Rules, not a model
Every finding comes from a deterministic rule written in TypeScript — there is no LLM anywhere in the audit path. The same workflow produces the same findings, in the same order, on every run: nothing drifts between runs, nothing needs re-prompting, and no finding is a judgment call you have to take on faith.
That determinism is what makes the Actor safe to gate a build on — a run that passes today passes tomorrow on the same input.
How to audit an n8n workflow
Give the Actor workflows in any one of three ways — pasted JSON, a URL, or a live instance — or combine all three in one run. Leave Rule set and Minimum severity at their defaults for the first run; narrowing before you have seen the output usually hides the finding you were looking for.
Audit pasted JSON
- In n8n, open the workflow and use the Workflow menu → Download. To audit only part of it, select those nodes on the canvas and press
Ctrl+Cinstead — n8n copies them as JSON. - Paste it into Workflow JSON. For several workflows at once, wrap them as
{"workflows": [ ... ]}— ann8n export:workflowbundle already has that shape. - Click Start. A single workflow takes a few seconds.
Audit workflows by URL
- Put one or more links in Workflow JSON URLs — a raw file in a Git repo, a Gist, or an n8n template link. See Auditing workflows by URL for the exact URL shapes accepted.
- Click Start. Each URL is fetched and audited independently; one that returns several workflows is audited — and billed — as several.
Audit a live n8n instance
- Create an n8n API key under Settings → n8n API — see Auditing a live instance for the required scopes and n8n plan requirements.
- Fill in n8n instance URL and n8n API key. Optionally set Workflow IDs to audit only specific workflows, or turn on Active workflows only to skip deactivated ones.
- Click Start. The Actor only reads: it calls
GET /api/v1/workflowsand nothing else. An instance sweep takes about as long as its slowest page of workflows.
Reading the results
- Open the report from the run's Output tab for the whole audit on one page, or read the Workflow scores table first if you would rather work in the data — one row per workflow, sorted by health score.
- Work the Findings table top down. It is sorted most severe first, and every row carries the exact node, the parameter path, and a remediation.
- Suppress what you have decided to live with. Copy the
rulevalue from any row into Ignore rules and the next run will not report it again.
What it checks
Twenty rules across six categories. Every finding carries its rule ID, so you can suppress it with ignoreRules on the next run.
Secrets and credentials
| Rule | Severity | What it catches |
|---|---|---|
secret-in-parameters | critical | AWS, Slack, OpenAI, Anthropic, GitHub, Google, Stripe, SendGrid, Twilio keys, JWTs and private-key blocks typed into node parameters, plus high-entropy values under names like apiKey or password. |
manual-auth-header | high | An Authorization or X-API-Key header set literally on an HTTP Request node instead of through a credential. |
Security
| Rule | Severity | What it catches |
|---|---|---|
expression-sandbox-escape | critical | constructor.constructor, process.env, require(), globalThis inside an expression (CVE-2025-68613, CVE-2026-1470). |
form-expression-injection | high | Unauthenticated Form input interpolated into a downstream expression (CVE-2026-27493, patched in n8n 1.123.22 / 2.9.3 / 2.10.1). |
webhook-no-auth | high | A Webhook node with no authentication and no validator node wired directly to it. |
sql-expression-injection | high | An expression interpolated into an Execute Query field instead of using query parameters. |
code-node-network-egress | medium | fetch, axios, require, or a hardcoded URL inside a Code node — traffic no credential governs. |
Reliability
| Rule | Severity | What it catches |
|---|---|---|
no-error-handling | medium | No error workflow, no Error Trigger, and no node with retry or on-error set, while nodes call external services. Reported once per workflow. |
loop-without-exit-branch | medium | A Loop Over Items node on a cycle whose "done" output is not connected. |
http-no-timeout | low | An HTTP Request node with no timeout. |
deprecated-continue-on-fail | low | The continueOnFail flag n8n replaced with onError. |
missing-error-workflow | medium / info | No error workflow set. medium for a workflow on an instance; info for pasted JSON. |
http-no-pagination | info | A call to what looks like a list endpoint with no pagination configured. |
Correctness
| Rule | Severity | What it catches |
|---|---|---|
unreachable-node | medium | A node no trigger can reach, so it never runs. |
no-trigger-node | medium | A workflow with no trigger at all. |
dangling-connection | medium | A connection pointing at a node that is not in the workflow. |
disabled-node-on-critical-path | low | A disabled node sitting between two connected nodes. |
Versions and supply chain
| Rule | Severity | What it catches |
|---|---|---|
deprecated-node | high | A node type n8n has hidden from the panel — cron, function, readBinaryFile and 42 others. |
stale-type-version | low / medium | A node behind the current major type version. One major behind is low, two or more is medium. |
unknown-node-type | medium / high | A community node dependency. Escalates to high when the package is unverified or abandoned. |
Input
Ten fields, all optional. Give it workflows in any one of three ways — you can combine them:
{// 1. Paste a workflow. For several, wrap them: { "workflows": [ ... ] }"workflowJson": { "name": "My workflow", "nodes": [], "connections": {} },// 2. Fetch exports from URLs - a Gist, a file in a Git repo"workflowUrls": ["https://example.com/workflow.json"],// 3. Pull from your own n8n instance"n8nBaseUrl": "https://n8n.example.com","n8nApiKey": "<your n8n public API key>","workflowIds": [], // empty = every workflow on the instance"activeOnly": true, // only what is actually running// Tuning"ruleSet": "all", // all | security | reliability | maintainability"minSeverity": "low", // info | low | medium | high | critical"ignoreRules": ["http-no-timeout"],}
There is nothing else to configure. The node index refreshes itself from n8n on every run, and community-node trust signals are looked up for whichever packages your workflows actually use.
Auditing workflows by URL
workflowUrls takes any URL that returns workflow JSON — the response does not have to be served as application/json, so a raw file in a Git repo works as-is. These are all accepted directly:
| URL | What it returns |
|---|---|
https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>.json | A workflow exported from the n8n editor, committed to a repo. |
https://gist.githubusercontent.com/<user>/<id>/raw/<file>.json | The same, from a Gist. Use the Raw URL, not the Gist page. |
https://api.n8n.io/api/workflows/templates/<id> | One workflow from n8n's public template library — the endpoint n8n's own Import from URL uses. |
https://api.n8n.io/api/templates/workflows/<id> | The same template with its listing metadata around it. |
https://<host>/api/v1/workflows | A saved response from an n8n instance. Prefer n8nBaseUrl for a live one — it paginates and unlocks the instance-only rules. |
A URL that returns several workflows — an n8n export:workflow --all --output bundle, or a { "workflows": [...] } array — is fine too; each one is audited and billed separately. A URL that fetches but contains no workflow is logged and skipped rather than failing the run.
The three sources are additive. If the same workflow arrives from more than one, it is audited once — the instance copy wins over a URL, which wins over pasted JSON — and every skipped duplicate is named in the run log. You are never billed twice for one workflow.
Auditing a live instance
Create the API key under Settings → n8n API; it needs the workflow:list and workflow:read scopes. The public API is a paid n8n feature and is unavailable during the free trial.
n8nApiKey is marked secret: stored encrypted, redacted from the run log, and never written to a dataset.
Output
Findings — one row per issue
Default dataset, most severe first:
{"fingerprint": "8db3cb38c79e","workflowId": "customer-intake","workflowName": "Customer intake","workflowSource": "json","scope": "node","nodeId": "3","nodeName": "Enrich","nodeType": "n8n-nodes-base.httpRequest","nodeTypeVersion": 3,"nodeDisabled": false,"rule": "secret-in-parameters","ruleTitle": "Credential hardcoded in node parameters","category": "secrets","severity": "critical","severityRank": 4,"message": "\"headerParameters.parameters[0].value\" on \"Enrich\" holds what looks like an OpenAI API key.","remediation": "Move the value into an n8n credential and reference it from the node…","evidence": "Bear…<redacted, 63 chars>","parameterPath": "headerParameters.parameters[0].value","docsUrl": null,"auditedAt": "2026-08-31T10:13:50.621Z"}
fingerprintis a stable id for the finding, built from the workflow, the rule, the node and the parameter — not from the message or the evidence. Diff two runs on this field to see only what changed.severityRankisseverityas a number, 4 down to 0. Sort on it rather than onseverity, whose names sort alphabetically.scopeisnodeorworkflow. Three rules report against the workflow as a whole — no error handling, no error workflow, no trigger — and their node fields are all null.nodeDisabledmarks a finding against a node that is switched off.
Workflow scores — one row per workflow
summaries dataset, with a health score out of 100, node and credential counts, and counts by severity. Read its Workflow scores view first on a multi-workflow run to see which workflow to open in the findings table.
credentialCount and credentialNames answer "what does this workflow have access to?" — names only, never a secret and never the credential id.
Every row carries the runId that produced it, along with the ruleSet and minSeverity the run used. This table is retained across runs, so check those two before comparing scores: a run narrowed to critical scores higher than a full one on the same workflow.
Run totals
Key-value store — summary-run: workflows audited, skipped and deduplicated; average score; findings by severity and by rule; the rule set and severity floor; whether the node index came from n8n live or from the bundled snapshot; any rule that failed; any warning the input raised; reportRequested, reportGenerated and reportUrl; and instanceError if the n8n instance could not be read.
HTML report — optional
Switch on HTML audit report to get report.html in the key-value store: the whole run as one page, every workflow's score followed by the findings grouped by severity. It appears as Audit report (HTML) on the run's Output tab, and its link is also on the run summary as reportUrl and in the run log.
It is billed as its own event, once per run, on top of the per-workflow charge, so it is off unless you ask for it. A run that leaves it off writes no report at all; every finding and every score still lands in the datasets.
How the score works
A workflow starts at 100 and loses points for each finding, weighted by severity:
| Severity | Points off |
|---|---|
| critical | 30 |
| high | 15 |
| medium | 6 |
| low | 2 |
| info | 0 |
100 means nothing above info was found, and 0 is the floor. info findings never cost anything, and no single rule can deduct more than 45 points however many times it fires. Use the score to rank workflows against each other and to track one over time; for the precise picture, read the finding count and severity breakdown next to it.
Evidence is always masked
Every value that could be a credential is reduced to a short prefix and its length (sk-p…<redacted, 51 chars>) before it reaches the output — enough to find the key in your workflow, never enough to use it.
Pricing
The Actor is pay per event, and there are two events:
| Event | Charged | When |
|---|---|---|
workflow-audited | Once per workflow | After that workflow's findings are written |
report-generated | Once per run, only if you switched on HTML audit report | After the report reaches the key-value store |
The cost of a run is the number of workflows in it, plus one if you asked for the report: auditing one pasted workflow is one charge; sweeping an instance of 40 is 40. Current per-event prices are on this Actor's Store page.
Running it from n8n
Install the official Apify node, add a Run Actor step, and pass the workflow JSON as input. Paired with a Schedule Trigger, that gives you a recurring audit of an entire instance — with results landing back in n8n through the Actor Run Finished trigger.
Calling it from the API
A single audit takes a few seconds, so the Actor can run synchronously — one request in, findings out, no polling:
curl -X POST "https://api.apify.com/v2/acts/mediocre_interest~n8n-workflow-auditor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"workflowJson": {"name": "Incoming leads","nodes": [{ "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2,"position": [0, 0], "parameters": { "path": "incoming" } }],"connections": {}},"minSeverity": "medium"}'
The response body is a JSON array of the finding rows shown under Output. Add &format=csv to get a spreadsheet instead, or &fields=severity,rule,nodeName,message to trim the columns.
For a longer sweep of a whole instance, start the run asynchronously with POST /v2/acts/.../runs and read the dataset when the run finishes, or let the Apify node inside n8n do both for you.
For a CI gate built on this endpoint, see Using this as a CI gate.
Using this as a CI gate
Every finding comes from a deterministic rule, so a build that passes today passes tomorrow on the same input — there is no model behind it to drift, and no run-to-run variance to chase.
Call the sync endpoint on every pull request with the changed workflow's JSON, and fail the build if anything comes back critical. $WORKFLOW_FILE below is the workflow JSON exported for the pull request; $APIFY_TOKEN is your Apify API token:
RESULT=$(curl -s -X POST "https://api.apify.com/v2/acts/mediocre_interest~n8n-workflow-auditor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d "$(jq -n --slurpfile wf "$WORKFLOW_FILE" '{workflowJson: $wf[0]}')")if echo "$RESULT" | jq -e 'any(.[]; .severity == "critical")' > /dev/null; thenecho "$RESULT" | jq '[.[] | select(.severity == "critical") | {rule, nodeName, message}]'exit 1fi
To fail only on new findings instead of every pre-existing critical — useful once a workflow already carries findings the team has accepted — cache the previous run's fingerprint values and gate on any that are not in that cache:
NEW=$(comm -13 <(sort previous-fingerprints.txt) <(echo "$RESULT" | jq -r '.[].fingerprint' | sort))if [ -n "$NEW" ]; thenecho "New finding(s) since the last run: $NEW"exit 1fiecho "$RESULT" | jq -r '.[].fingerprint' | sort > previous-fingerprints.txt
For a scheduled sweep of a whole instance instead of a per-PR check, use Apify's own scheduler to point the Actor at your instance — see Auditing a live instance.
FAQ
Does this send my workflows to an LLM?
No. Every finding comes from a deterministic rule, which is why the same workflow produces identical findings on every run.
Can it see my credentials?
No. n8n's public API returns per-node credentials as { id, name } references with the secret values stripped, so they never reach the Actor. Your n8nApiKey is stored encrypted, redacted from the run log, and never written to a dataset. Evidence for a key found hardcoded in a parameter is masked to a prefix and a length before it reaches the output.
Does it change my workflows?
No. The Actor only ever reads. Against an instance it calls GET /api/v1/workflows and nothing else.
How is this different from n8n's built-in n8n audit?
n8n's audit is instance-scoped, needs owner credentials, and cannot read a JSON file, so it cannot run in CI or across a portfolio of client exports. It covers five categories; this Actor covers twenty rules across six, adding hardcoded secrets, error handling, typeVersion staleness, graph reachability, HTTP hygiene, and the Form-node expression-injection CVE. Where the two overlap, this Actor mirrors n8n's own definitions so the verdicts agree.
Why did the run finish with no findings?
Three possibilities. Your minimum severity is above anything the run could report — reliability has no rule more severe than medium and maintainability none above high, so pairing either with high or critical filters out everything, and the run log warns you when that happens. Or your ignore rules cover what was found. Or the workflow really is clean, in which case the summaries row records a score of 100.
Can I run it in CI or on a schedule?
Yes — see Using this as a CI gate for CI. On a schedule, use Apify's scheduler to point the Actor at your instance; the summaries table keeps a row per workflow per run, so a score moving down is a regression you can act on.
What happens if n8n's API is down?
The audit still runs and its verdicts do not change. Deprecation and version rules use n8n's node index, which the Actor fetches fresh at the start of every run and otherwise falls back to a bundled snapshot of all 572 built-in nodes. The run log says which one was used.
I switched on the report but the Output tab shows nothing
The findings and scores are already in the datasets by the time the run finishes — the report can take a little longer to reach the key-value store. Refresh the run's page in Console. If it is still missing after that, check reportGenerated and reportUrl in the run summary to confirm whether it was written at all.
What other Actors work with this one?
| Actor | What it does |
|---|---|
| n8n Node Catalog & Community Package Scraper | Catalogs every n8n node — built-in, verified, and community — with npm provenance, license, weekly downloads, and maintenance risk for each package. |
| Ad Library Scraper — Google, Meta & LinkedIn | Scrapes ads from the Google Ads Transparency Center, Meta Ad Library and LinkedIn Ad Library in one run — ad copy, creatives, impressions and run dates. |
| Multi-Platform Job Scraper | Collects job listings from LinkedIn, Indeed, Glassdoor, ZipRecruiter, Greenhouse, Lever and more into a single dataset. |
The node catalog above is a natural companion if you also want an inventory of every node in play across your instance. The other two are the kind of Actor an n8n workflow calls on a schedule through the Apify node — and this one audits the workflow doing the calling.
Support
Found a false positive, a rule that should exist, or a workflow shape the parser mishandles? Open an issue on the Actor's Issues tab in Apify Console.
When reporting a finding you disagree with, include the rule ID from the row and the smallest workflow JSON that reproduces it.