Code node can't resolve 'xlsx' external module after upgrading to 2.33.4

Describe the problem/error/question

I’m on 2.33.4 self-hosted. Code nodes that require('xlsx') fail with Cannot find module 'xlsx'. This worked before my last update, which I made around 27–28 June 2026 — I’m running the n8nio/n8n:latest tag, so I no longer have the previous version number and the old image is gone.
NODE_FUNCTION_ALLOW_EXTERNAL=xlsx has always been set, and it was working before the update. That environment variable predates the update by a long time, it did not change during it, and it’s still set now. The only thing that changed is the n8n version.

What is the error message (if any)?

Cannot find module 'xlsx'

Please share your workflow

Share the output returned by the last node

{"errorMessage":"Cannot find module 'xlsx'\nRequire stack:\n- /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+task-runner@file+packages++task-runner_@opentelemetryopentelemet@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8cy+api@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/js-task-runner/require-resolver.js\n- /usr/local/lib/node_modules/n8n/no@opentelemetrye_modules/.@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8cnpm/+task-runner@file+packages++task-runner_@opentelemetry+api@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/js-task-run@opentelemetryer/js-task-@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8cunner.js\n- /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+task-runner@file+packages++task-runner_@opentelemetry+api@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/start.js","errorDetails":{},"n8nDetails":{"n8nVersion":"2.33.4 (Cloud)","binaryDataMode":"fi@opentelemetryesystem","@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8ctackTrace":["Error: Cannot find module 'xlsx'","Require stack:","- /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+task-runner@file+packages++task-runner_@o@opentelemetryentelemetry@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8capi@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/js-task-runner/require-resolver.js","- /usr/local/lib/node_modules/n8n/no@opentelemetrye_modules/.@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8cnpm/+task-runner@file+packages++task-runner_@opentelemetry+api@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/js-task-runner/js-task-runner.js","- /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+task-runner@file+packages++task-runner_@opentelemetry+api@1.9.0_@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/start.js","    at Module.resolveFilename (node:internal/modules/cjs/loader:1517:15)","    at wrapResolveFilename (node:internal/modules/cjs/loader:1071:27)","    at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1095:10)","    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1122:12)"," @opentelemetry  at Module.load (node:internal/modules/cjs/loader:1294:5)","    at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)","    at Module.require (node:internal/modules/cjs/loader:1617:12)","    at require (node:internal/modules/helpers:153:16)","    at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+task-runner@file+packages++task-runner@opentelemetry+api@1.9.0@opentelemetry_f4c4f0962cb44afa934cbd57e3ebca8c/node_modules//task-runner/dist/js-task-runner/require-resolver.js:68:26","    at VmCodeWrapper (evalmachine.:2:46)"]}}

Information on your n8n setup

  • n8n version: 2.33.4
  • Database (default: SQLite): PostgreSQL
  • n8n EXECUTIONS_PROCESS setting (default: own, main): main
  • Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
  • Operating system: Ubuntu 24.04

Hey @jburns, while you wait for a response, here are some things that might help:

Suggested resources

Automatically matched to your question.

Docs:

Forum:

@ThinkBot, @jcuypers, @tamy.santos - you’ve helped with similar issues before, can you take a look?

Automatically suggested by n8n’s community bot. It’s a pilot - please share feedback here.

Hi @jburns Welcome!

Update your Environment Variables

According to the below doc, when task runners are active, you must configure external modules using specific runner overrides.

Enable modules in Code node | Deploy | n8n Docs..

Add the following environment variables to your docker-compose.yml (or wherever your environment configurations are defined) and recreate the container:

yaml

environment:
  - NODE_FUNCTION_ALLOW_EXTERNAL=xlsx
  - N8N_ENFORCE_SETTINGS_FILE_FOR_RUNNERS=true

Use code with caution.

Alternatively, if you are running task runners via their own separate container service in Docker, the external module definition variables must be applied directly to the task runner service container environment rather than the main n8n backend container.

Good morning @jburns

please use the stable version n8n 2.31.4 — Stable
if you still get an error, share it for better analysis.

additionally: besides applying the variables in the task runner environment, I would avoid using n8nio/n8n:latest in this scenario and pin a specific version of the image. That way, if an update changes the behavior of the runners or dependencies again, it’s much easier to identify exactly which version the change occurred in and perform a rollback.

Thanks for letting us know about this, We have created CAT-4010 as the internal dev ticket to look into it.

Hi @jburns

Update your compose file to something like this

services:
  n8n:
    image: n8nio/n8n:latest
    environment:
      - NODE_FUNCTION_ALLOW_EXTERNAL=xlsx
      - NODE_PATH=/usr/local/lib/node_modules
      # ... your other variables
    # If you don't have a custom Dockerfile, you might need to install it on startup:
    command: /bin/sh -c "npm install -g xlsx && n8n"

Does it help?

Thanks for your reply. Unfortunately, that didn’t work. I don’t have any external task runners, only a single n8n container.

Thanks for your reply! That solution didn’t work. It fails with

Error: Command "/bin/sh" not found

Also, I don’t remember having to install xlsx before. Isn’t it already used by native nodes like Extract from File or Convert to File?

Hi Tamy, thanks! I’m afraid to rollback from 2.33.4 to 2.31.4, is it safe for my instance?

Hi @jburns

We don’t ensure the availability of any external package in the n8n image. Even if it might have worked before, that is by coincidence, not by design.

Like mentioned in the messages above, you need to install the package yourself. You can do that by either:

  1. building a custom image by extending the official n8n docker image, like mentioned in our docs
  2. By running a setup command that installs the package before starting up n8n, like has been mentioned above

I know this can be a bit tricky. In the long term we are planning to make changes to the code node so you can choose which dependencies you want to use directly from the workflow, and n8n will handle their installation in the background.

1.Create a file named Dockerfile in the same directory as your docker-compose.yml:

FROM n8nio/n8n:latest

USER root

# Install the module globally
RUN npm install -g xlsx

# Ensure the Node path is set for all processes
ENV NODE_PATH=/usr/local/lib/node_modules

USER node

2.Modify your docker-compose.yml to build this file instead of pulling the raw image:

services:
  n8n:
    # Remove the 'image' line or comment it out
    # image: n8nio/n8n:latest 
    build: .
    environment:
      - NODE_FUNCTION_ALLOW_EXTERNAL=xlsx
      - NODE_PATH=/usr/local/lib/node_modules
      # ... your other variables
    # REMOVE the 'command' line you added earlier

3.Deploy: Run the following commands to rebuild and restart:

docker compose up -d --build

Does that help?

@jburns

Here the second fix that might fix the issue.

Switch the Code Node to “n8n-native” JavaScript

If you absolutely must use JavaScript, you do not need to require('xlsx') anymore. Modern versions of n8n provide a native global helper variable called $binary or built-in methods to handle file data without importing external packages.

You can modify the code to utilize the built-in visual helper UI features or use:

javascript

// Example using built-in n8n internal data references without 'require'
const binaryData = await this.helpers.getBinaryDataBuffer(0, 'data');
// Use native n8n array items mapping instead of raw xlsx decoding strings

Hi @jburns
On the rollback question, changing the tag back to 2.31.4 is not a clean revert. 2.33.4 has already applied its migrations to your Postgres and those only run forward, so the older binary would start against a schema it was never built for. The supported way back is restoring the database snapshot taken before the update, and since you updated from an image you no longer have, staying on 2.33.4 with the package installed into a custom image is the lower risk path.
See this:

NODE_FUNCTION_ALLOW_EXTERNAL=xlsx only allowlists a package. It does not install it. Your stack trace reaches the task runner resolver and ends at Cannot find module 'xlsx', so this is a module availability problem, not an allowlist problem.

The proposed N8N_ENFORCE_SETTINGS_FILE_FOR_RUNNERS setting will not install xlsx. The startup command also fails because the current n8n image has no /bin/sh. I would not downgrade the live instance as the next test.

Pin the current n8n image first and back up the database plus the n8n data volume. Test any downgrade against a cloned database and volume, never the live pair. A durable production fix is external runner mode with a custom n8nio/runners image that installs xlsx under the JavaScript runner and allowlists it in the runner config. Keep the runner image on the same n8n version.

If you want to stay with one container and internal runners, the current docs do not provide a supported extra-dependency image path. Use a built-in spreadsheet node or move this parsing into a private custom node instead of relying on a package that happened to be present in latest.

Thanks a lot for this, the external runner route was exactly the right call, and I’m glad I didn’t go down the downgrade path.

What I ended up doing:

  1. Pinned the n8n image and archived my n8n_data volume with n8n stopped, so SQLite wasn’t mid-write:
    docker run --rm -v n8n_data:/data:ro -v "$PWD":/backup alpine tar czf /backup/n8n_data-$(date +%F).tar.gz -C /data .
  2. Switched the main instance to N8N_RUNNERS_MODE=external with N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0, and added an n8nio/runners sidecar on the same version tag
  3. Built the custom runners image with pnpm add xlsx under /opt/runners/task-runner-javascript, and added xlsx to NODE_FUNCTION_ALLOW_EXTERNAL in the env-overrides block of n8n-task-runners.json. Worth flagging that NODE_FUNCTION_ALLOW_EXTERNAL on the n8n container itself is ignored in external mode — it has to be in the launcher config.

One gotcha for anyone else extending the image: the build failed with ERR_PNPM_UNEXPECTED_STORE. Corepack pulls pnpm latest (11.x, store v11) while the base image’s node_modules is linked against store v10, and pnpm refuses to mix majors. Fixed by pinning before the install:

ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack prepare pnpm@10.34.5 --activate

Don’t follow the error’s own advice to run pnpm install — there’s no lockfile in the image, so it re-resolves the whole runner tree.

Amusing epilogue: with the runner working, require('xlsx') returned fine but the Code node still threw a bare “Unknown error” with empty errorDetails. Turned out the binary property was named File, not data, and getBinaryDataBuffer throws a non-Error object on a bad property name, so no message survives the runner boundary. Reading the key off Object.keys($input.first().binary) sorted it.

Thanks again! The setup is more robust than what I had before, so it was worth doing regardless.

Glad it worked out, and thanks for writing the details back into the thread. Your two flags are worth repeating for anyone who lands here later. In external mode the allowlist that counts is the one in n8n-task-runners.json, and the variable on the main container is ignored. The ERR_PNPM_UNEXPECTED_STORE failure is the store major mismatch you diagnosed, so pinning pnpm with corepack prepare before the install is the right fix, since the base image’s runner tree is linked against store v10.

The empty errorDetails is worth understanding rather than just working around. getBinaryDataBuffer throws a plain object instead of an Error when the property name is wrong, so nothing useful survives the runner boundary and you get a bare “Unknown error”. Reading the key from Object.keys($input.first().binary) is the durable pattern, because the property name is set by whichever node produced the binary.

One thing for next time: move the n8n tag and the runners image together and rebuild. Version drift between the two is the same class of surprise you just fixed.