n8ncustom nodesdevelopmenttypescriptautomation

n8n Custom Nodes: Build Your Own (2026 Guide)

Build custom nodes for n8n - when to do it, how to scaffold the project, write the code, test it, package it, and ship it to your instance.

VV
Valerian Valkin Founder & CEO, 2V Automation
·
Jump to a section

Custom n8n nodes are the right answer when you’re hitting the same internal API from the HTTP Request node across multiple workflows - every workflow re-implementing auth, error handling, pagination, and shape transformation. Bundle that logic once into a typed node, and every future workflow gets a clean, debugged, single-line invocation.

This is the practical guide to building custom nodes for n8n in 2026: when to do it, how to scaffold, how to write the code, how to test, and how to ship to your instance.

When to build a custom node

Don’t build one because you can. Build one when:

  • You’re using the HTTP Request node to hit the same internal API from 3+ workflows. Every workflow re-implements auth headers, base URLs, error handling, retries, and pagination. A custom node consolidates that.
  • You need authentication that doesn’t fit the built-in credential types. Custom OAuth flows, internal service-mesh tokens, mTLS - easier to wrap once than to handle ad-hoc.
  • You have an internal library you’d like to expose as a node. A pricing engine, a tax calculator, a custom ML inference call - wrap the library in a node and any workflow can use it.
  • You want to expose a domain operation cleanly. “Charge a customer” or “Create a renewal contract” reads better in a workflow than 8 nested HTTP calls.

Don’t build a custom node when:

  • You only need this in one workflow (a sub-workflow is cheaper to build and maintain)
  • The HTTP Request node + a couple of Set / Edit Fields nodes already cover it
  • The integration is to a public API that probably has (or will get) a community node soon

For more on the build-vs-don’t decision, see why your business needs its own n8n node.

What a custom node actually is

An n8n node is a TypeScript class that implements the INodeType interface. It declares:

  • A description (display name, icon, properties shown in the UI)
  • A list of credentials it requires
  • An execute method that runs when the node fires in a workflow

The class compiles to JavaScript, gets packaged as an npm module, and gets dropped into n8n’s nodes_modules directory (or installed via the community nodes feature). On next n8n restart, it appears in the node picker alongside built-ins.

There are two flavors:

  • Programmatic nodes. Full TypeScript, full execute logic, can do anything Node.js can do. Most flexibility, more code.
  • Declarative nodes. JSON-style routing definition for REST API wrappers. Less code, less flexibility. Good for straightforward CRUD-against-an-API nodes.

We’ll cover both, with most depth on the programmatic flavor since that’s what real internal-API wrappers usually need.

Prerequisites

Before you start:

  • Node.js 20 or newer
  • TypeScript familiarity (the code is TypeScript)
  • A running n8n instance you control (Cloud doesn’t support custom nodes - you need self-hosted)
  • Git, npm, and a code editor

For self-hosting setup, see n8n setup & installation.

Scaffolding a new node project

n8n maintains a starter repo. Clone it:

git clone https://github.com/n8n-io/n8n-nodes-starter.git my-custom-node
cd my-custom-node
npm install

The starter ships with:

  • An example node (nodes/ExampleNode/)
  • An example credential (credentials/ExampleCredentialsApi.credentials.ts)
  • TypeScript config, ESLint config, and a build script

Rename the example to your node’s name. Update package.json - most importantly the n8n block that declares which nodes and credentials this package ships:

{
  "name": "n8n-nodes-yourcompany-internal",
  "version": "0.1.0",
  "n8n": {
    "n8nNodesApiVersion": 1,
    "credentials": [
      "dist/credentials/YourCompanyApi.credentials.js"
    ],
    "nodes": [
      "dist/nodes/YourCompany/YourCompany.node.js"
    ]
  }
}

Writing the credential

If your node needs auth, start with the credential definition. It’s a TypeScript file declaring the fields the user fills in (and how to use them).

import {
  ICredentialType,
  INodeProperties,
  ICredentialTestRequest,
} from 'n8n-workflow';

export class YourCompanyApi implements ICredentialType {
  name = 'yourCompanyApi';
  displayName = 'YourCompany API';
  documentationUrl = 'https://docs.yourcompany.com/api';

  properties: INodeProperties[] = [
    {
      displayName: 'API Key',
      name: 'apiKey',
      type: 'string',
      typeOptions: { password: true },
      default: '',
      required: true,
    },
    {
      displayName: 'Base URL',
      name: 'baseUrl',
      type: 'string',
      default: 'https://api.yourcompany.com',
    },
  ];

  // Optional: a test request so the UI can verify the credential
  test: ICredentialTestRequest = {
    request: {
      baseURL: '={{ $credentials.baseUrl }}',
      url: '/v1/me',
      headers: {
        Authorization: '=Bearer {{ $credentials.apiKey }}',
      },
    },
  };
}

A few notes:

  • typeOptions: { password: true } hides the field value in the UI
  • The test block makes the “Test” button in the credential dialog work
  • Expressions like {{ $credentials.apiKey }} reference other fields in the same credential

Writing the node (programmatic flavor)

A simple programmatic node that calls your internal API:

import {
  IExecuteFunctions,
  INodeExecutionData,
  INodeType,
  INodeTypeDescription,
  NodeOperationError,
} from 'n8n-workflow';

export class YourCompany implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'YourCompany',
    name: 'yourCompany',
    icon: 'file:yourCompany.svg',
    group: ['transform'],
    version: 1,
    description: 'Interact with the YourCompany internal API',
    defaults: {
      name: 'YourCompany',
    },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [
      {
        name: 'yourCompanyApi',
        required: true,
      },
    ],
    properties: [
      {
        displayName: 'Resource',
        name: 'resource',
        type: 'options',
        noDataExpression: true,
        options: [
          { name: 'Customer', value: 'customer' },
          { name: 'Invoice', value: 'invoice' },
        ],
        default: 'customer',
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        noDataExpression: true,
        displayOptions: {
          show: { resource: ['customer'] },
        },
        options: [
          { name: 'Get', value: 'get', action: 'Get a customer' },
          { name: 'Create', value: 'create', action: 'Create a customer' },
          { name: 'Update', value: 'update', action: 'Update a customer' },
        ],
        default: 'get',
      },
      {
        displayName: 'Customer ID',
        name: 'customerId',
        type: 'string',
        required: true,
        displayOptions: {
          show: {
            resource: ['customer'],
            operation: ['get', 'update'],
          },
        },
        default: '',
      },
      {
        displayName: 'Customer Data',
        name: 'customerData',
        type: 'json',
        displayOptions: {
          show: {
            resource: ['customer'],
            operation: ['create', 'update'],
          },
        },
        default: '{}',
      },
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];

    const credentials = await this.getCredentials('yourCompanyApi');
    const baseUrl = credentials.baseUrl as string;
    const apiKey = credentials.apiKey as string;

    for (let i = 0; i < items.length; i++) {
      try {
        const resource = this.getNodeParameter('resource', i) as string;
        const operation = this.getNodeParameter('operation', i) as string;

        let response;

        if (resource === 'customer') {
          if (operation === 'get') {
            const customerId = this.getNodeParameter('customerId', i) as string;
            response = await this.helpers.httpRequest({
              method: 'GET',
              url: `${baseUrl}/v1/customers/${customerId}`,
              headers: { Authorization: `Bearer ${apiKey}` },
              json: true,
            });
          } else if (operation === 'create') {
            const data = JSON.parse(
              this.getNodeParameter('customerData', i) as string,
            );
            response = await this.helpers.httpRequest({
              method: 'POST',
              url: `${baseUrl}/v1/customers`,
              headers: { Authorization: `Bearer ${apiKey}` },
              body: data,
              json: true,
            });
          }
          // ... other operations
        }

        returnData.push({ json: response });
      } catch (error) {
        if (this.continueOnFail()) {
          returnData.push({ json: { error: error.message } });
          continue;
        }
        throw new NodeOperationError(this.getNode(), error as Error, {
          itemIndex: i,
        });
      }
    }

    return [returnData];
  }
}

Key things going on:

  • Properties declare what the user sees in the UI (resource, operation, fields). displayOptions controls which fields show based on other field values.
  • The execute method iterates over input items, calls the API, and pushes results onto returnData.
  • this.helpers.httpRequest is n8n’s wrapped HTTP client. It handles retries, error formatting, and integrates with the credential system.
  • this.continueOnFail() lets the workflow keep running with the error captured if the user has enabled “Continue on Fail” on the node.
  • NodeOperationError is the right error type - it surfaces with the item index in the execution UI.

Declarative nodes for REST APIs

If your node is mostly a REST API wrapper without much custom logic, declarative nodes are dramatically less code. Instead of an execute method, you declare routing:

import { INodeType, INodeTypeDescription } from 'n8n-workflow';

export class YourCompanySimple implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'YourCompany (Simple)',
    name: 'yourCompanySimple',
    group: ['transform'],
    version: 1,
    description: 'Simple REST wrapper',
    defaults: { name: 'YourCompany' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'yourCompanyApi', required: true }],
    requestDefaults: {
      baseURL: '={{ $credentials.baseUrl }}',
      headers: {
        Authorization: '=Bearer {{ $credentials.apiKey }}',
      },
    },
    properties: [
      {
        displayName: 'Resource',
        name: 'resource',
        type: 'options',
        noDataExpression: true,
        options: [{ name: 'Customer', value: 'customer' }],
        default: 'customer',
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        noDataExpression: true,
        displayOptions: { show: { resource: ['customer'] } },
        options: [
          {
            name: 'Get',
            value: 'get',
            action: 'Get a customer',
            routing: {
              request: { method: 'GET', url: '=/v1/customers/{{ $parameter.customerId }}' },
            },
          },
        ],
        default: 'get',
      },
      {
        displayName: 'Customer ID',
        name: 'customerId',
        type: 'string',
        required: true,
        default: '',
      },
    ],
  };
}

No execute method needed - the routing block handles it. Good for simple wrappers; falls short for anything with custom transforms or multi-step logic.

Build and test locally

The build step compiles TypeScript to JavaScript:

npm run build

This produces a dist/ directory with the JS files referenced in package.json.

To test against a local n8n instance:

# In your custom node project:
npm link

# In your n8n custom nodes directory (~/.n8n/custom by default):
npm link n8n-nodes-yourcompany-internal

Restart n8n. Your node appears in the node picker. Modify code, rebuild, restart n8n to see changes.

For a tighter dev loop, run npm run dev (the starter ships with this script) for incremental TypeScript builds, and use nodemon or similar to restart n8n on rebuild.

Testing the node

For unit-testing the node logic, mock the n8n helpers and test the execute method directly. The starter includes a test setup; expand it.

For integration testing, run the node in a real n8n instance against a test API or mock server. We typically use a local Postman Mock Server or a small Express app to fake the API for tests.

Things to test:

  • Each operation against each resource (happy path)
  • Auth failure handling
  • API error responses (4xx, 5xx)
  • Empty inputs
  • Multiple-item input (the per-item loop)
  • The “Continue on Fail” behavior

Packaging and publishing

Two distribution paths.

Path 1: Internal use only

Build, package as a tarball, install on your n8n instance.

npm run build
npm pack
# Produces n8n-nodes-yourcompany-internal-0.1.0.tgz

On the n8n host:

cd ~/.n8n/custom
npm install /path/to/n8n-nodes-yourcompany-internal-0.1.0.tgz

Restart n8n. Done.

For Docker-based setups, mount the tarball into the container at startup, or build a custom n8n image with your package pre-installed. Example Dockerfile:

FROM docker.n8n.io/n8nio/n8n:latest
USER root
COPY n8n-nodes-yourcompany-internal-*.tgz /tmp/
RUN cd /home/node/.n8n/custom && \
    npm install /tmp/n8n-nodes-yourcompany-internal-*.tgz
USER node

Path 2: Publish as a community node

If the node would be useful to the broader n8n community, publish it to npm with the n8n-community-node-package keyword. Users can then install it via n8n’s community nodes UI (Settings → Community Nodes → Install) on Cloud (paid tiers) or self-hosted.

npm publish

Make sure your package.json has the right keywords and metadata, and read the n8n community nodes guidelines before publishing.

Versioning and updates

Your node has a version field in its description. n8n’s workflow versioning uses this - workflows pin to a specific node version, and old workflows continue to work after you ship a new version.

The pattern:

  • Bump the node version number when you ship breaking changes (new required parameter, removed operation)
  • For non-breaking changes (new operation, new optional field), keep the same version
  • Document the change in your release notes

Common gotchas

A few things that bite first-time node developers.

Forgetting to rebuild. Code change → no npm run build → no change visible in n8n. Standard pattern: rebuild then restart n8n.

Mismatched paths in package.json. The n8n.nodes and n8n.credentials arrays in package.json must point to the compiled JS files in dist/, not the TS sources.

Per-item iteration done wrong. Common newbie mistake: doing one API call for all items in a single for loop iteration, then pushing one result for many items. The per-item loop should call the API per item and push per item.

Not handling “Continue on Fail”. Every catch block should check this.continueOnFail() and push the error as data if true. Forgetting this makes your node uncooperative in production workflows.

Forgetting displayOptions.show. Without it, every property shows in the UI regardless of which resource/operation is selected. The result is a confusing form. Use displayOptions.show to gate fields by other field values.

SVG icon path issues. icon: 'file:yourCompany.svg' requires the SVG file to be in the same directory as the node TS file. Webpack copies it during build; make sure your build config is doing this.

Credentials test request that requires data the user hasn’t filled in. The test block runs the moment the user clicks “Test”. Make sure it works with only the credential fields available, not parameter values from a workflow execution.

When to revisit your node

A custom node isn’t a fire-and-forget artifact. Plan to:

  • Update when the underlying API changes (new endpoints, deprecated fields, new auth)
  • Bump version on breaking changes
  • Add new operations as workflows need them - keeps the surface area in one place
  • Re-test against n8n major version upgrades (the n8n-workflow types occasionally change)

A well-maintained internal node we’ve built for clients typically gets touched every couple of months for additions and small updates. Treat it as production code.


If you’re thinking about custom nodes for an internal API or library, our Efficiency Scorecard is a fast way to figure out whether the build cost will pay back, alongside where automation is most valuable in your business. 15 minutes, free, you keep the output.

Frequently asked questions

When should I build a custom n8n node?

When you're using the HTTP Request node to hit the same internal API from three or more workflows, when you need auth that doesn't fit built-in credential types, when you have an internal library to expose as a workflow primitive, or when you want a domain operation (like "charge customer") to read cleanly in workflow code. Don't build for one-off use cases; a sub-workflow is cheaper.

What language are n8n custom nodes written in?

TypeScript. The compiled JavaScript runs in n8n. n8n's official starter repo (n8n-io/n8n-nodes-starter) ships with the TypeScript config and build pipeline.

Can I use custom nodes on n8n Cloud?

Custom nodes generally need self-hosted n8n. Cloud paid tiers do support installing community nodes (published to npm with the right metadata), but you can't run unpublished local nodes on Cloud. For internal-only custom nodes, you need a self-hosted instance.

Programmatic vs declarative nodes - which should I use?

Use declarative nodes for simple REST API wrappers with no custom logic. Use programmatic nodes for anything with custom transforms, multi-step API flows, complex pagination, internal library calls, or non-standard auth flows. Most production internal-API nodes end up programmatic.

How do I share a custom node across multiple n8n instances?

Two ways. For internal use, package the node as an npm tarball (`npm pack`) and install on each instance, or bake into a custom Docker image. For broader use, publish to npm with the `n8n-community-node-package` keyword - users can then install via n8n's Community Nodes UI.

How do I test a custom n8n node?

Unit test the execute logic by mocking n8n's helpers. Integration test by running the node in a real n8n instance against a mock API server (Postman Mock Server or a small Express app). Test happy paths, auth failures, API error responses, multi-item input, and "Continue on Fail" behavior.

Can custom nodes access the file system?

Yes - programmatic nodes are full Node.js code with the standard library available. They can read files, call internal services, run subprocess, anything Node.js can do. The node runs inside the n8n process, so security and permissions are whatever the n8n process has.

Does building a custom node require n8n Enterprise?

No. The Community Edition fully supports custom nodes. n8n Enterprise adds features like external secrets, log streaming, SSO, and audit logs - none required for building or running custom nodes.