Introduction
While n8n offers 400+ built-in integrations, you’ll often need to connect to internal systems, proprietary APIs, or unique services. Creating custom nodes allows you to encapsulate complex logic, share reusable components across workflows, and contribute to the n8n community.
Understanding n8n Nodes
Node Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ n8n Node Structure โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Node Class โ โ
โ โ โ โ
โ โ Properties โ Credentials โ โ
โ โ (name, version, (API keys, tokens, โ โ
โ โ description) auth data) โ โ
โ โ โ โ
โ โ Methods: โ โ
โ โ - this.execute() โ Main execution logic โ โ
โ โ - this.executeSingle() โ Single item processing โ โ
โ โ - this.prepare() โ Setup/initialization โ โ
โ โ - this.nodeVersion() โ Version management โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Node Types
| Type | Description | Example |
|---|---|---|
| Trigger | Starts a workflow | Webhook, Schedule |
| Action | Performs an operation | HTTP Request, Send Email |
| Nested | Contains sub-workflows | Execute Workflow |
Setting Up Development Environment
Prerequisites
# Node.js (LTS version)
node --version # Should be v18+
# npm
npm --version
# TypeScript (recommended)
npm install -g typescript
Project Setup
# Create new node project
npx n8n-node-dev create my-custom-node
# Or manually
mkdir my-n8n-node && cd my-n8n-node
npm init -y
npm install n8n-workflow typescript @types/node
# Install dev dependencies
npm install --save-dev ts-node jest @types/jest
Project Structure
my-n8n-node/
โโโ package.json
โโโ tsconfig.json
โโโ jest.config.js
โโโ README.md
โ
โโโ src/
โ โโโ MyCustomNode.ts # Main node file
โ โโโ MyCustomNode.other.ts # Additional files
โ โโโ index.ts # Entry point
โ
โโโ credentials/
โ โโโ MyCustomApi.credentials.ts # Credential definition
โ
โโโ test/
โ โโโ MyCustomNode.test.ts
โ โโโ workflow.json
โ
โโโ nodes/
โโโ MyCustomNode/
โโโ MyCustomNode.node.ts # Node definition
Creating Your First Node
Basic Node Template
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodePropertyTypes,
} from 'n8n-workflow';
export class MyCustomNode implements INodeType {
description: INodeTypeDescription = {
displayName: 'My Custom Node',
name: 'myCustomNode',
group: ['transform'],
version: 1,
description: 'Performs custom operations',
defaults: {
name: 'My Custom Node',
color: '#00FF00',
},
inputs: ['main'],
outputs: ['main'],
inputNames: ['Input'],
outputNames: ['Output'],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{ name: 'Get', value: 'get', description: 'Get a record' },
{ name: 'Create', value: 'create', description: 'Create a record' },
{ name: 'Update', value: 'update', description: 'Update a record' },
{ name: 'Delete', value: 'delete', description: 'Delete a record' },
],
default: 'get',
},
{
displayName: 'Resource ID',
name: 'resourceId',
type: 'string',
displayOptions: {
show: {
operation: ['get', 'update', 'delete'],
},
},
default: '',
placeholder: 'e.g., 12345',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const operation = this.getNodeParameter('operation', i) as string;
const resourceId = this.getNodeParameter('resourceId', i) as string;
let result;
switch (operation) {
case 'get':
result = await this.getResource(resourceId);
break;
case 'create':
result = await this.createResource(items[i].json);
break;
case 'update':
result = await this.updateResource(resourceId, items[i].json);
break;
case 'delete':
result = await this.deleteResource(resourceId);
break;
}
returnData.push({ json: result });
}
return [returnData];
}
private async getResource(id: string): Promise<object> {
// Implement your API call here
return { id, data: 'example' };
}
private async createResource(data: object): Promise<object> {
return { id: 'new-id', ...data };
}
private async updateResource(id: string, data: object): Promise<object> {
return { id, ...data };
}
private async deleteResource(id: string): Promise<object> {
return { id, deleted: true };
}
}
Working with Credentials
Credential Definition
import {
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class MyCustomApiCredential implements ICredentialType {
name = 'myCustomApi';
displayName = 'My Custom API';
documentationUrl = 'myCustomApi';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.example.com',
placeholder: 'https://api.example.com',
},
];
}
Using Credentials in Your Node
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
// Get credential
const credentials = await this.getCredentials('myCustomApi') as {
apiKey: string;
baseUrl: string;
};
const apiKey = credentials.apiKey;
const baseUrl = credentials.baseUrl;
// Use in API call
const response = await axios.get(`${baseUrl}/resource`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
returnData.push({ json: response.data });
}
return [returnData];
}
Advanced Node Features
Pagination
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('myCustomApi');
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await axios.get(`${credentials.baseUrl}/items`, {
params: { page, limit: 100 },
headers: { Authorization: `Bearer ${credentials.apiKey}` },
});
for (const item of response.data.items) {
returnData.push({ json: item });
}
hasMore = response.data.hasMore;
page++;
}
return [returnData];
}
Binary Data
// Working with binary data
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (const item of items) {
// Get binary data
const binaryData = item.binary;
if (binaryData && binaryData.data) {
// Process image, file, etc.
const buffer = Buffer.from(binaryData.data, 'base64');
// Upload to service
const uploadResult = await this.uploadFile(buffer);
returnData.push({
json: { url: uploadResult.url },
});
}
}
return [returnData];
}
Error Handling
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
try {
const items = this.getInputData();
// ... process items
return [returnData];
} catch (error) {
// Throw error for n8n to handle
if (error.response?.status === 429) {
// Rate limited - retry later
throw new NodeApiError(this.getNode(), error, {
message: 'Rate limited',
retry: true,
});
}
throw new NodeOperationError(
this.getNode(),
`Error: ${error.message}`,
{ itemIndex: 0 }
);
}
}
Testing Your Node
Unit Tests
import { MyCustomNode } from './MyCustomNode';
describe('MyCustomNode', () => {
let node: MyCustomNode;
let mockExecute: any;
beforeEach(() => {
node = new MyCustomNode();
mockExecute = jest.fn();
});
test('should execute get operation', async () => {
// Mock getNodeParameter
const context = {
getNodeParameter: jest.fn((param: string) => {
if (param === 'operation') return 'get';
if (param === 'resourceId') return '123';
return '';
}),
getCredentials: jest.fn().mockResolvedValue({
apiKey: 'test-key',
baseUrl: 'https://api.test.com',
}),
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
getNode: jest.fn().mockReturnValue({ name: 'Test Node' }),
} as any;
// Execute
const result = await node.execute.call(context);
expect(result).toBeDefined();
});
});
Testing with Workflows
{
"name": "Test Workflow",
"nodes": [
{
"parameters": {
"operation": "get",
"resourceId": "123"
},
"id": "node-1",
"name": "My Custom Node",
"type": "myCustomNode",
"typeVersion": 1,
"position": [250, 300]
}
],
"connections": {},
"active": false,
"settings": {}
}
Publishing Your Node
NPM Package
// package.json
{
"name": "n8n-nodes-my-custom-node",
"version": "1.0.0",
"description": "Custom n8n node for My Service",
"keywords": ["n8n", "n8n-nodes", "my-service"],
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"test": "jest"
},
"n8n": {
"n8nNodes": {
"path": "dist"
}
}
}
Local Installation
# Build and install locally
cd my-n8n-node
npm run build
npm link
# In your n8n folder
npm link n8n-nodes-my-custom-node
n8n start
Real-World Examples
Internal API Integration
// Integration with internal CRM
export class InternalCrmNode implements INodeType {
description: INodeTypeDescription = {
displayName: 'Internal CRM',
name: 'internalCrm',
group: ['output'],
version: 1,
description: 'Connect to internal CRM system',
defaults: { name: 'Internal CRM' },
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'internalCrmApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{ name: 'Contact', value: 'contact' },
{ name: 'Company', value: 'company' },
{ name: 'Deal', value: 'deal' },
],
default: 'contact',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{ name: 'Create', value: 'create' },
{ name: 'Update', value: 'update' },
{ name: 'Get', value: 'get' },
{ name: 'List', value: 'list' },
{ name: 'Delete', value: 'delete' },
],
default: 'list',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('internalCrmApi');
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter('resource', i) as string;
const operation = this.getNodeParameter('operation', i) as string;
let endpoint = `/${resource}`;
let method = 'GET';
switch (operation) {
case 'create':
method = 'POST';
break;
case 'update':
method = 'PUT';
endpoint += `/${items[i].json.id}`;
break;
case 'delete':
method = 'DELETE';
endpoint += `/${items[i].json.id}`;
break;
}
const response = await axios({
method,
url: `${credentials.baseUrl}${endpoint}`,
data: operation === 'create' ? items[i].json : undefined,
headers: {
Authorization: `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
});
returnData.push({ json: response.data });
}
return [returnData];
}
}
Webhook Handler Node
// Custom webhook processing node
export class WebhookProcessorNode implements INodeType {
description: INodeTypeDescription = {
displayName: 'Webhook Processor',
name: 'webhookProcessor',
group: ['trigger'],
version: 1,
description: 'Process incoming webhooks',
defaults: { name: 'Webhook Processor' },
inputs: [],
outputs: ['main'],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
responseData: 'allEntries',
path: 'webhook',
},
],
properties: [
{
displayName: 'Signature Validation',
name: 'validateSignature',
type: 'boolean',
default: true,
},
{
displayName: 'Event Type Field',
name: 'eventTypeField',
type: 'string',
default: 'type',
},
],
};
async webhook(this: IExecuteFunctions): Promise<void> {
const body = this.getBodyData();
const validateSignature = this.getNodeParameter('validateSignature') as boolean;
if (validateSignature) {
const signature = this.getHeader('x-signature');
// Validate signature logic
}
this.emit([
[{ json: body }],
]);
}
}
Best Practices
Good Patterns
// Good: Clear parameter descriptions
properties: [
{
displayName: 'Email Address',
name: 'email',
type: 'string',
placeholder: '[email protected]',
description: 'The email address to send notifications to',
default: '',
required: true,
},
]
// Good: Use options for known values
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{ name: 'Active', value: 'active' },
{ name: 'Inactive', value: 'inactive' },
],
default: 'active',
}
// Good: Error handling
try {
const result = await apiCall();
} catch (error) {
throw new NodeOperationError(this.getNode(), error.message);
}
Bad Patterns to Avoid
// Bad: Hardcoded values
const API_URL = 'https://api.example.com'; // Should use credentials
// Bad: No error handling
const data = await axios.get(url); // Can fail silently
// Bad: Missing parameter validation
const id = this.getNodeParameter('id'); // No null check
// Bad: Not supporting batch operations
// Always iterate over items for proper batch support
Debugging
VS Code Debug Configuration
{
"type": "node",
"request": "launch",
"name": "Debug n8n Node",
"program": "${workspaceFolder}/node_modules/n8n/bin/n8n",
"args": ["start"],
"console": "integratedTerminal",
"env": {
"N8N_PATH": "/home/user/.n8n/",
"NODE_ENV": "development"
}
}
Logging
// Debug logging
this.logger.info('Processing item', { itemIndex: i, operation });
console.log('Debug:', JSON.stringify(data, null, 2));
Conclusion
Creating custom n8n nodes allows you to integrate with any service or system. Start with simple nodes and gradually add complexity. Remember to handle errors gracefully, support batch operations, and thoroughly test before deployment.
Related Articles
- n8n Complete Guide: AI-Powered Workflow Automation
- n8n Advanced Workflow Patterns
- n8n Webhooks and API Integration
Comments