n8n-impl-custom-nodes

Installation
SKILL.md

n8n Custom Node Development

Build, test, and publish custom n8n community nodes using TypeScript.

Quick Reference

Task Command / Action
Scaffold project Clone n8n-io/n8n-nodes-starter from GitHub
Install dependencies npm install
Development mode npm run dev (hot-reload at http://localhost:5678)
Build npm run build (compiles to dist/)
Lint npm run lint
Publish npm publish (package name MUST start with n8n-nodes-)

Decision Tree: Programmatic vs Declarative

Need a custom node?
├── Is it a REST API wrapper with standard CRUD operations?
│   └── YES → Use DECLARATIVE style (routing in properties, NO execute() method)
├── Does it require complex data transformation or custom logic?
│   └── YES → Use PROGRAMMATIC style (execute() method with full control)
└── Does it need to call multiple APIs or combine data sources?
    └── YES → Use PROGRAMMATIC style

Project Structure

n8n-nodes-<service-name>/
├── package.json              # Node registration via n8n field (CRITICAL)
├── tsconfig.json             # TypeScript config
├── credentials/
│   └── MyApi.credentials.ts  # Credential definitions
├── nodes/
│   └── MyNode/
│       ├── MyNode.node.ts    # Main node implementation
│       ├── MyNode.node.json  # Codex metadata (optional, for search/AI)
│       └── resources/        # Resource descriptions (declarative style)
├── icons/
│   └── my-icon.svg           # Node icon (SVG or PNG, max 60x60px)
└── dist/                     # Compiled output (generated by build)

package.json n8n Field (CRITICAL)

The n8n field in package.json registers your nodes and credentials with n8n.

{
    "name": "n8n-nodes-myservice",
    "version": "0.1.0",
    "keywords": ["n8n-community-node-package"],
    "n8n": {
        "n8nNodesApiVersion": 1,
        "credentials": [
            "dist/credentials/MyApi.credentials.js"
        ],
        "nodes": [
            "dist/nodes/MyNode/MyNode.node.js"
        ]
    },
    "files": ["dist"],
    "peerDependencies": {
        "n8n-workflow": "*"
    }
}

Rules:

  • ALWAYS set keywords to ["n8n-community-node-package"] — n8n uses this to discover community nodes
  • ALWAYS point n8n.nodes and n8n.credentials to compiled .js files in dist/
  • NEVER point to .ts source files in the n8n field
  • ALWAYS use n8n-nodes- prefix for the package name
  • ALWAYS declare n8n-workflow as a peerDependency, NEVER as a regular dependency

Node Description (INodeTypeDescription)

Every node MUST define a description property with these required fields:

description: INodeTypeDescription = {
    displayName: 'My Node',           // Human-readable name shown in UI
    name: 'myNode',                   // Internal name (camelCase, unique)
    icon: 'file:myIcon.svg',          // Icon reference
    group: ['transform'],             // 'input' | 'output' | 'transform' | 'trigger'
    version: 1,                       // Node version (number or number[])
    description: 'Short description', // Shown in node creator panel
    defaults: { name: 'My Node' },    // Default instance name
    inputs: [NodeConnectionTypes.Main],
    outputs: [NodeConnectionTypes.Main],
    credentials: [                    // Credential requirements
        { name: 'myApi', required: true },
    ],
    properties: [ /* INodeProperties[] */ ],
};

Rules:

  • ALWAYS set group to ['trigger'] and inputs to [] for trigger nodes
  • ALWAYS use NodeConnectionTypes.Main from n8n-workflow (NEVER hardcode 'main')
  • ALWAYS provide a defaults.name that matches displayName

Programmatic Node Pattern

Use when you need full control over execution logic.

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

export class MyNode implements INodeType {
    description: INodeTypeDescription = { /* ... */ };

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

        for (let i = 0; i < items.length; i++) {
            try {
                const param = this.getNodeParameter('myParam', i) as string;
                const credentials = await this.getCredentials('myApi');
                const response = await this.helpers.httpRequest({
                    method: 'GET',
                    url: `${credentials.baseUrl}/endpoint`,
                    headers: { Authorization: `Bearer ${credentials.apiKey}` },
                });
                returnData.push({ json: response as IDataObject });
            } catch (error) {
                if (this.continueOnFail()) {
                    returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
                    continue;
                }
                throw error;
            }
        }
        return [returnData];
    }
}

Rules:

  • ALWAYS iterate over all input items (this.getInputData())
  • ALWAYS pass the item index i to this.getNodeParameter() — it resolves expressions per item
  • ALWAYS handle this.continueOnFail() in the catch block
  • ALWAYS include pairedItem: { item: i } when pushing error items
  • ALWAYS return a 2D array: [returnData] for single output, [trueItems, falseItems] for two outputs

Declarative Node Pattern

Use for REST API wrappers. n8n handles HTTP requests automatically.

export class MyApiNode implements INodeType {
    description: INodeTypeDescription = {
        /* ... standard fields ... */
        requestDefaults: {
            baseURL: 'https://api.example.com',
            headers: { Accept: 'application/json' },
        },
        properties: [
            {
                displayName: 'Operation',
                name: 'operation',
                type: 'options',
                options: [
                    {
                        name: 'Get',
                        value: 'get',
                        routing: {
                            request: { method: 'GET', url: '=/items/{{$parameter.itemId}}' },
                        },
                    },
                ],
                default: 'get',
            },
            {
                displayName: 'Title',
                name: 'title',
                type: 'string',
                default: '',
                routing: {
                    send: { type: 'body', property: 'title' },
                },
            },
        ],
    };
    // NO execute() method — n8n handles requests via routing config
}

Rules:

  • NEVER define an execute() method on declarative nodes
  • ALWAYS define requestDefaults.baseURL for the API base URL
  • ALWAYS use routing.request on operation options to define HTTP method and URL
  • ALWAYS use routing.send on parameter properties to map values to request body/query

Credential Definition

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

export class MyApi implements ICredentialType {
    name = 'myApi';                                 // Must match credential name in node
    displayName = 'My API';
    documentationUrl = 'https://docs.example.com';
    properties: INodeProperties[] = [
        {
            displayName: 'API Key',
            name: 'apiKey',
            type: 'string',
            typeOptions: { password: true },
            default: '',
        },
    ];
    authenticate: IAuthenticate = {
        type: 'generic',
        properties: {
            headers: { Authorization: '=Bearer {{$credentials.apiKey}}' },
        },
    };
}

Rules:

  • ALWAYS set typeOptions: { password: true } for sensitive fields (API keys, tokens, passwords)
  • ALWAYS match the credential name property with the name referenced in node credentials array
  • ALWAYS use authenticate for declarative nodes (auto-injects auth into requests)
  • For programmatic nodes, retrieve credentials via await this.getCredentials('myApi') in execute()

Icon Handling

  • ALWAYS use SVG format (preferred) or PNG (fallback)
  • ALWAYS place icons in the project root icons/ directory or next to the node file
  • Reference format: icon: 'file:myIcon.svg' (relative to node file or icons dir)
  • For themed icons: icon: { light: 'file:icon.svg', dark: 'file:icon.dark.svg' }
  • For FontAwesome: icon: 'fa:clock' (limited set available)
  • Maximum recommended size: 60x60 pixels

Development Workflow

Step 1: Scaffold

git clone https://github.com/n8n-io/n8n-nodes-starter.git n8n-nodes-myservice
cd n8n-nodes-myservice
rm -rf .git
npm install

Step 2: Develop

npm run dev    # Starts n8n with your node loaded, hot-reload enabled

Open http://localhost:5678 — your custom node appears in the node creator panel.

Step 3: Build and Lint

npm run build  # Compiles TypeScript to dist/
npm run lint   # Checks code quality (ALWAYS run before publishing)

Step 4: Test Locally with npm link

# In your node package directory:
npm link

# In your n8n installation directory:
npm link n8n-nodes-myservice

# Restart n8n to load the linked package

Step 5: Publish

npm publish    # Publishes to npm registry

Testing with WorkflowTestData

See references/examples.md for the complete test setup pattern.

Key points:

  • ALWAYS mock external API calls using nock definitions in the test data
  • ALWAYS define expected nodeData output for assertion
  • ALWAYS specify nodeExecutionOrder to verify execution flow

Publishing Checklist

  • Package name starts with n8n-nodes-
  • keywords includes "n8n-community-node-package"
  • n8n.nodes and n8n.credentials point to dist/*.js files
  • n8n-workflow is in peerDependencies
  • files field includes ["dist"]
  • npm run build succeeds without errors
  • npm run lint passes
  • Icon is included (SVG preferred)
  • README.md describes installation and available nodes

Reference Links

Related skills
Installs
1
GitHub Stars
1
First Seen
Apr 3, 2026