| name | n8n-impl-custom-nodes |
| description | Use when creating custom n8n nodes or building community node packages for npm. Prevents packaging errors from incorrect package.json n8n field configuration or missing credential registration. Covers n8n-nodes-starter template, community node packaging, project structure, programmatic vs declarative node styles, testing with WorkflowTestData, and publishing to npm. Keywords: n8n, custom node, community node, npm, INodeType, starter template, build custom node, create node, npm package, community node, extend n8n..
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires n8n v1.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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',
name: 'myNode',
icon: 'file:myIcon.svg',
group: ['transform'],
version: 1,
description: 'Short description',
defaults: { name: 'My Node' },
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{ name: 'myApi', required: true },
],
properties: [ ],
};
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 = ..({
: ,
: ,
: { : },
});
returnData.({ : response });
} (error) {
(.()) {
returnData.({ : { : (error ). }, : { : i } });
;
}
error;
}
}
[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 = {
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: },
},
},
],
};
}
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';
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
Open http://localhost:5678 — your custom node appears in the node creator panel.
Step 3: Build and Lint
npm run build
npm run lint
Step 4: Test Locally with npm link
npm link
npm link n8n-nodes-myservice
Step 5: Publish
npm publish
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
Reference Links