A security release wearing a version number
n8n 2.0 landed in December 2025 and it was unusual: almost no new nodes, almost no new UI, a long list of defaults flipped from convenient to safe. The line as of late September 2026 is in the 2.40.x range, so by now "upgrading to 2" mostly means "you are already late".
Hardening releases break differently from feature releases. Nothing errors at import. The workflow opens, the canvas looks right, and then a Code node returns undefined where it used to return your API key.
Here are the five that actually catch people.
1. Task runners are on by default
Code node executions now run in an isolated task runner instead of inside the main process. On n8n Cloud this is invisible. Self-hosted, it is an infrastructure change: the main n8nio/n8n image no longer ships the task runner for external mode, so if you run external mode you run a second container from n8nio/runners.
If you are on a one-container Docker setup that someone pasted into a compose file two years ago, this is the item that decides your afternoon.
2. The Code node can no longer read environment variables
Blocked by default (N8N_BLOCK_ENV_ACCESS_IN_NODE). Anything in a Code node that reached for process.env to pick up a key, a base URL or a tenant id now gets nothing.
The fix is the one you would have picked anyway: move those values into n8n credentials, or into workflow static data if they are not secrets. Flipping the flag back is possible and is a bad trade — the whole point of the runner isolation is that a Code node cannot read the host's secrets.
3. $evaluateExpression() is gone from the Code node
Same cause: secure execution in the runner. The replacement pattern is mechanical — evaluate the expression in an Edit Fields (Set) node placed before the Code node, then read the field off the incoming item.
Annoying for exactly one class of workflow: the clever ones that built expressions as strings at runtime. If you have one of those, it is a rewrite, not a rename.
4. ExecuteCommand and LocalFileTrigger are disabled
Both are off by default now, and file access is confined to ~/.n8n-files. If a workflow shells out to convert a PDF or watches a folder for dropped invoices, it will not run until you explicitly take the node out of the exclusion list via NODES_EXCLUDE.
Worth pausing before you do. Re-enabling ExecuteCommand on an instance that also accepts webhooks from the public internet is a meaningful decision, not a config detail.
5. Save and Publish are two different things now
The Active toggle is gone. A workflow has a saved state and a published state: saving keeps editing, publishing deploys. This is strictly better — it ends the classic accident where a half-finished edit to a live workflow starts firing at 3 a.m. — but it does mean the habit of "save and it's live" no longer holds, and the first few times, nothing happens when you expect it to.
One more, quieter: Python in the Code node is native now, not browser Pyodide, and it needs the external runner.
Before you upgrade
n8n ships a Migration Report — Settings → Migration Report, available since 1.121.0 — that lists workflow-level and instance-level issues for your specific instance. Run it first; it is faster than reading changelogs and it knows what you actually built.
Then, in order: snapshot the volume or export every workflow, grep your Code nodes for process.env and $evaluateExpression, list the workflows that use ExecuteCommand or LocalFileTrigger, and upgrade a copy before the real one. The copy step is the one people skip and regret.