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
keywordsto["n8n-community-node-package"]— n8n uses this to discover community nodes - ALWAYS point
n8n.nodesandn8n.credentialsto compiled.jsfiles indist/ - NEVER point to
.tssource files in the n8n field - ALWAYS use
n8n-nodes-prefix for the package name - ALWAYS declare
n8n-workflowas apeerDependency, 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
groupto['trigger']andinputsto[]for trigger nodes - ALWAYS use
NodeConnectionTypes.Mainfromn8n-workflow(NEVER hardcode'main') - ALWAYS provide a
defaults.namethat matchesdisplayName
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
itothis.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.baseURLfor the API base URL - ALWAYS use
routing.requeston operation options to define HTTP method and URL - ALWAYS use
routing.sendon 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
nameproperty with the name referenced in nodecredentialsarray - ALWAYS use
authenticatefor declarative nodes (auto-injects auth into requests) - For programmatic nodes, retrieve credentials via
await this.getCredentials('myApi')inexecute()
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
nockdefinitions in the test data - ALWAYS define expected
nodeDataoutput for assertion - ALWAYS specify
nodeExecutionOrderto verify execution flow
Publishing Checklist
- Package name starts with
n8n-nodes- -
keywordsincludes"n8n-community-node-package" -
n8n.nodesandn8n.credentialspoint todist/*.jsfiles -
n8n-workflowis inpeerDependencies -
filesfield includes["dist"] -
npm run buildsucceeds without errors -
npm run lintpasses - Icon is included (SVG preferred)
- README.md describes installation and available nodes
Reference Links
Related skills