Skip to main content

n8n Custom Nodes: Build, Test and Publish Your Own (2026 Guide)

Build an n8n custom node: scaffold with npm create @n8n/node, pick declarative or programmatic style, test with npm run dev, then install or publish it.

AI Automation Architect

Published
Jan 15, 2025
Updated
Sep 30, 2026
Reading time
12 min read
Quick answer

To build an n8n custom node, run npm create @n8n/node@latest, pick the HTTP API (declarative) or Other (programmatic) template, define your operations and credential, then run npm run dev to test it in a local n8n at localhost:5678. Install it privately on a self-hosted instance, or publish it to npm as a community node. n8n Cloud can install only verified community nodes, which since May 1, 2026 must be published from GitHub Actions with npm provenance.

Checked against n8n's node-building documentation, the n8n-nodes-starter repository and the @n8n/node-cli package on October 1, 2026. This update replaces the January 2025 version, whose code used a deprecated request helper, labeled a programmatic example as declarative, and included client results we could not verify.

When a custom node is worth building

You rarely need a custom node for a one-off call. The HTTP Request node can reach any REST API, and it can be connected to an AI agent as a tool. Build a node when:

  • The same API calls repeat across many workflows and you want one tested, documented component.
  • Teammates who don't read API docs need clean fields, dropdowns and a reusable credential.
  • You connect to an internal or niche system that has no built-in node.
  • You want to publish an integration for other n8n users as a community node.

Note that the Code node can't make HTTP requests or touch the file system, per n8n's Code node docs, so it is not a substitute for either.

Declarative or programmatic?

n8n's style guide is direct: "Build your node in the declarative style. It's the default for new nodes."

DeclarativeProgrammatic
How it worksJSON-like routing on each operation and field; no execute() methodAn execute() method reads items and parameters and makes the requests
Use it forREST APIsTrigger nodes, non-REST APIs such as GraphQL, nodes with external dependencies, heavier per-item processing
Trade-offLess code, fewer bugs; the CLI calls its template "designed for faster approval for n8n Cloud"Full flexibility, more code to maintain

You can mix both in one package, for example a declarative action node with a programmatic trigger node for the same service.

What you need

  • Node.js 22.22.0 or later with npm, per n8n's development environment page, plus git.
  • VS Code with the ESLint, EditorConfig and Prettier extensions, which surface the n8n node linter's warnings as you type.
  • Docker or Podman for local testing from n8n 3.0 (scheduled for October 2026), when npm run dev starts n8n in a container. To run n8n yourself instead, use n8n-node dev --external-n8n and start that instance with N8N_DEV_RELOAD=true.
  • Working TypeScript and REST API knowledge.

Step 1: Scaffold the project

npm create @n8n/node@latest

# or install the CLI globally and run: n8n-node new
npm install --global @n8n/node-cli

The CLI asks three questions (n8n-node docs):

  1. Node name: it must follow n8n-nodes-<name> or @<org>/n8n-nodes-<name>.
  2. Kind of node: HTTP API (declarative) or Other (programmatic). Pick Other for trigger nodes.
  3. Template: GitHub Issues API (a demo with several operations and credentials) or Start from scratch, which then asks for the API's base URL and authentication: API Key, Bearer Token, OAuth2, Basic Auth, Custom or None.

To skip the prompts, pass a name and one of the three templates, declarative/github-issues, declarative/custom or programmatic/example. With the npm create form, add -- before any options:

npm create @n8n/node@latest n8n-nodes-acme-crm -- --template declarative/custom

Step 2: Know the project layout and scripts

n8n-nodes-acme-crm/
├── credentials/
│   └── AcmeCrmApi.credentials.ts
├── nodes/
│   └── AcmeCrm/
│       ├── AcmeCrm.node.ts
│       ├── AcmeCrm.node.json   # codex file: categories and docs links
│       └── acmecrm.svg
├── package.json
└── tsconfig.json

The generated package.json wires the scripts to the CLI and registers the built files in its n8n section:

"scripts": {
  "build": "n8n-node build",
  "dev": "n8n-node dev",
  "lint": "n8n-node lint",
  "lint:fix": "n8n-node lint --fix",
  "release": "n8n-node release"
},
"keywords": ["n8n-community-node-package"],
"n8n": {
  "n8nNodesApiVersion": 1,
  "strict": true,
  "credentials": ["dist/credentials/AcmeCrmApi.credentials.js"],
  "nodes": ["dist/nodes/AcmeCrm/AcmeCrm.node.js"]
}

Keep the class name and file name identical: a class AcmeCrm lives in AcmeCrm.node.ts.

Step 3: Write the credential

A credential file defines the fields users fill in and how n8n attaches them to requests. authenticate adds the key to every call, and test lets n8n check it when the user saves (see credentials files):

// credentials/AcmeCrmApi.credentials.ts
import type {
  IAuthenticateGeneric,
  ICredentialTestRequest,
  ICredentialType,
  INodeProperties,
} from 'n8n-workflow';

export class AcmeCrmApi implements ICredentialType {
  name = 'acmeCrmApi';
  displayName = 'Acme CRM API';
  documentationUrl = 'https://docs.acme-crm.example/api/auth';

  properties: INodeProperties[] = [
    {
      displayName: 'API Key',
      name: 'apiKey',
      type: 'string',
      typeOptions: { password: true },
      default: '',
    },
  ];

  authenticate: IAuthenticateGeneric = {
    type: 'generic',
    properties: {
      headers: { Authorization: '=Bearer {{$credentials.apiKey}}' },
    },
  };

  test: ICredentialTestRequest = {
    request: { baseURL: 'https://api.acme-crm.example/v1', url: '/me' },
  };
}

The name here must match the name in the node's credentials array exactly, or n8n reports that the credential type isn't known.

Step 4: Write a declarative node

This node creates and fetches contacts in a fictional Acme CRM. requestDefaults sets the base URL, each operation's routing sets the method and path, and routing.send puts a field into the request body. The pattern follows n8n's declarative tutorial and the starter's GitHub Issues node.

// nodes/AcmeCrm/AcmeCrm.node.ts
import { NodeConnectionTypes } from 'n8n-workflow';
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';

export class AcmeCrm implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'Acme CRM',
    name: 'acmeCrm',
    icon: 'file:acmecrm.svg',
    group: ['transform'],
    version: 1,
    subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
    description: 'Create and read contacts in Acme CRM',
    defaults: { name: 'Acme CRM' },
    usableAsTool: true,
    inputs: [NodeConnectionTypes.Main],
    outputs: [NodeConnectionTypes.Main],
    credentials: [{ name: 'acmeCrmApi', required: true }],
    requestDefaults: {
      baseURL: 'https://api.acme-crm.example/v1',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    },
    properties: [
      {
        displayName: 'Resource',
        name: 'resource',
        type: 'options',
        noDataExpression: true,
        options: [{ name: 'Contact', value: 'contact' }],
        default: 'contact',
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        noDataExpression: true,
        displayOptions: { show: { resource: ['contact'] } },
        options: [
          {
            name: 'Create',
            value: 'create',
            action: 'Create a contact',
            routing: { request: { method: 'POST', url: '/contacts' } },
          },
          {
            name: 'Get',
            value: 'get',
            action: 'Get a contact',
            routing: { request: { method: 'GET', url: '=/contacts/{{$parameter.contactId}}' } },
          },
        ],
        default: 'create',
      },
      {
        displayName: 'Email',
        name: 'email',
        type: 'string',
        placeholder: 'name@example.com',
        required: true,
        default: '',
        displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
        routing: { send: { type: 'body', property: 'email' } },
      },
      {
        displayName: 'Contact ID',
        name: 'contactId',
        type: 'string',
        required: true,
        default: '',
        displayOptions: { show: { resource: ['contact'], operation: ['get'] } },
      },
    ],
  };
}

usableAsTool: true, which n8n's tutorials include, lets the AI Agent node call your node as a tool. Optional parameters usually go in an Additional Fields collection so the default view stays short.

When you need the programmatic style

Programmatic nodes implement execute(). Make requests with this.helpers.httpRequestWithAuthentication. The older this.helpers.request is deprecated (its original implementation was removed in n8n 1.0), and the HTTP request helpers reference says new nodes should all use the new helper. Loop over items, keep item links, and wrap API failures in NodeApiError:

import type { IDataObject, IExecuteFunctions, INodeExecutionData, JsonObject } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

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

  for (let i = 0; i < items.length; i++) {
    try {
      const email = this.getNodeParameter('email', i) as string;
      const response = await this.helpers.httpRequestWithAuthentication.call(this, 'acmeCrmApi', {
        method: 'POST',
        url: 'https://api.acme-crm.example/v1/contacts',
        body: { email },
        json: true,
      });
      returnData.push(
        ...this.helpers.constructExecutionMetaData(
          this.helpers.returnJsonArray(response as IDataObject),
          { itemData: { item: i } },
        ),
      );
    } catch (error) {
      if (this.continueOnFail()) {
        returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
        continue;
      }
      throw new NodeApiError(this.getNode(), error as JsonObject);
    }
  }

  return [returnData];
}

Use NodeOperationError for validation and configuration problems, and NodeApiError for failures from the external service; n8n's error handling reference shows how to add custom messages for codes such as 401 and 404.

Step 5: Test it locally

npm run dev
# then open http://localhost:5678 and search for "Acme CRM" in the nodes panel

n8n-node dev builds the node, links it into its own n8n user folder (~/.n8n-node-cli, so it never touches another n8n install), starts n8n with the node loaded and rebuilds when you save. Search by the node's display name, not the package name. Stop and restart the command after changing description properties, since those don't refresh live. Run npm run lint (or npm run lint:fix) before every release.

Step 6: Install it on your own n8n

For a private node you won't publish, build it and give a self-hosted n8n the files in dist. n8n loads every *.node.js and *.credentials.js it finds in ~/.n8n/custom or in a folder named by N8N_CUSTOM_EXTENSIONS:

npm run build

docker run -it --rm --name n8n -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  -v "$(pwd)/dist":/home/node/custom-nodes \
  -e N8N_CUSTOM_EXTENSIONS=/home/node/custom-nodes \
  n8nio/n8n

For a package already on npm, owners and admins of a self-hosted instance have three options (installation docs):

  • GUI: Settings > Community Nodes > Install, then enter the package name, optionally with a version.
  • Command line: inside the container, run npm i n8n-nodes-yourname in ~/.n8n/nodes and restart. Use this for queue mode or private npm packages.
  • Environment variables (n8n 2.21.0 and later): set N8N_COMMUNITY_PACKAGES_MANAGED_BY_ENV=true and list packages in N8N_COMMUNITY_PACKAGES. On first start, n8n uninstalls any community package not in that list.

n8n Cloud is different: owners and admins can install verified community nodes from the nodes panel, but unverified npm packages and private custom nodes need self-hosted n8n.

Step 7: Publish and get verified

To publish, log in with npm login and run npm run release, which builds, lints, updates the changelog, tags, creates a GitHub release and publishes. For verification, so that Cloud users can install your node from the nodes panel, it must also:

  • Be published from GitHub Actions with an npm provenance statement, required since May 1, 2026. New projects from npm create @n8n/node include a publish.yml workflow; existing projects need @n8n/node-cli 0.23.0 or later and the starter's workflow file.
  • Use the MIT license, have no runtime dependencies, and never read environment variables or files.
  • Integrate one third-party service per package, not duplicate an existing n8n node, and not be a logic or flow-control node.
  • Pass the linter and npx @n8n/scan-community-package n8n-nodes-yourname, with the interface and README in English.

Then submit it in the n8n Creator Portal. The full list is in n8n's verification guidelines, and n8n says it may reject nodes that compete with its paid features.

Troubleshooting

The node doesn't appear in the nodes panel

Search by display name, check the n8n.nodes paths in package.json point to files that exist in dist, rebuild, and restart the dev command.

"Credentials of type ... aren't known"

The name in your credential class and the node's credentials array don't match.

The icon is missing or distorted

Keep the SVG or 60x60 PNG in the node's folder, reference it as file:acmecrm.svg, and use a square canvas.

A 401 from the API

Check the header format your API expects (some want Bearer, some a custom header or query parameter), and test the same key with curl first.

The linter keeps flagging a renamed file on Windows

n8n's troubleshooting page notes a known Windows issue with case-only renames. Rename to a different name first, then to the correct case.

A note on security

n8n's risk page warns that community nodes "have full access to the machine that n8n runs on" and to the data in your workflows. Install only packages you trust, pin versions, and read the changelog before upgrading. Self-hosted admins can switch community nodes off with N8N_COMMUNITY_PACKAGES_ENABLED=false. In your own nodes, keep secrets in credential fields with password: true, never in node parameters or code.

Where to go next

If you are building nodes for client projects, pair this with our guides to connecting any API in n8n, error handling and self-hosting. If you would rather build your own AI product than nodes for clients, AI SaaS Builder covers tool use with the Claude API and building your own MCP server, along with Supabase, Next.js and Stripe billing.

Operator program · recommended for this article

Want the full AI SaaS Builder playbook?

A 10-module, 52-lesson curriculum: validate an idea, build on Supabase and Next.js, add AI features with the Claude API, speed up with Claude Code and MCP, deploy on Vercel, launch, and charge with Stripe.

10 modules · one-time purchase · 30-day money-back guaranteeiimagined.ai by Anyro

n8n custom nodes FAQ

How do I create a custom node in n8n?

Run npm create @n8n/node@latest, choose the HTTP API (declarative) or Other (programmatic) template, and define your operations and credential. Then run npm run dev, open http://localhost:5678 and search for your node by its display name in the nodes panel.

Can I use custom nodes on n8n Cloud?

Only verified community nodes, which instance owners and admins can install from the nodes panel. Private custom nodes and unverified community nodes from npm need a self-hosted n8n instance.

Should I build a declarative or programmatic n8n node?

Declarative, unless you have a reason not to. n8n makes it the default for REST APIs because it needs no execute() method. Use the programmatic style for trigger nodes, APIs that are not REST such as GraphQL, nodes with external dependencies, and heavier per-item data processing.

What do I need installed to build an n8n node?

Node.js 22.22.0 or later with npm, and git; n8n recommends VS Code with the ESLint, EditorConfig and Prettier extensions. From n8n 3.0, npm run dev starts n8n in a container, so you also need Docker or Podman, or you can point the CLI at an n8n instance you run yourself.

How do I get my n8n node verified?

Build it with the n8n-node tool, use the MIT license, avoid runtime dependencies, environment variables and file system access, keep the interface and docs in English, and make sure the linter and n8n's package scan pass. Since May 1, 2026, the package must be published to npm from GitHub Actions with a provenance statement. Then submit it in the n8n Creator Portal.

Why doesn't my custom node appear in n8n?

Search by the node's display name, not the package name. Check that the n8n section of package.json points to the built files in dist, rebuild, and restart npm run dev after changing description properties. In Docker, confirm the built node and credential files are in ~/.n8n/custom or a folder set with N8N_CUSTOM_EXTENSIONS.

Are community nodes safe to install?

Treat them like any third-party code. n8n warns that community nodes have full access to the machine n8n runs on and to the data in your workflows. Prefer verified nodes, install unverified ones only from sources you trust, and self-hosted admins can disable them with N8N_COMMUNITY_PACKAGES_ENABLED=false.

All-Access subscription

Every program. Member benefits.
One subscription.

Use all four premium programs with weekly live coaching, a private community, and the resource vault.

Confirm current lessons, downloadable resources and member-benefit arrangements before purchasing.

  • All 4 premium programs plus free Futures Trading
  • Weekly live coaching calls
  • Private community access
  • Resource vault and templates
  • 30-day money-back guarantee, cancel anytime
$99/ month
$99 for the first month · $702 to buy all four standalone
Start All-AccessOr browse standalone programs
30-day money-back guarantee · $99/month · cancel anytime