Developing Custom Nodes for n8n
When n8n's built-in nodes are insufficient, custom nodes are developed—TypeScript packages added to n8n installation and work as native nodes in the editor.
Custom Node Structure
my-n8n-nodes/
├── nodes/
│ └── MyService/
│ ├── MyService.node.ts # Main node
│ ├── MyService.node.json # Metadata
│ └── myservice.svg # Icon
├── credentials/
│ └── MyServiceApi.credentials.ts
├── package.json
└── tsconfig.json
package.json
{
"name": "n8n-nodes-myservice",
"version": "1.0.0",
"description": "n8n nodes for MyService API",
"main": "index.js",
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": ["dist/credentials/MyServiceApi.credentials.js"],
"nodes": ["dist/nodes/MyService/MyService.node.js"]
},
"devDependencies": {
"n8n-workflow": "*",
"typescript": "^5.0.0"
}
}
Credentials
// credentials/MyServiceApi.credentials.ts
import { ICredentialType, INodeProperties } from 'n8n-workflow';
export class MyServiceApi implements ICredentialType {
name = 'myServiceApi';
displayName = 'MyService API';
documentationUrl = 'https://docs.myservice.com/api';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.myservice.com/v1',
},
];
}
Main Node
// nodes/MyService/MyService.node.ts
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
} from 'n8n-workflow';
export class MyService implements INodeType {
description: INodeTypeDescription = {
displayName: 'MyService',
name: 'myService',
icon: 'file:myservice.svg',
group: ['transform'],
version: 1,
description: 'Interact with MyService API',
defaults: { name: 'MyService' },
inputs: ['main'],
outputs: ['main'],
credentials: [
{ name: 'myServiceApi', required: true }
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{ name: 'Contact', value: 'contact' },
{ name: 'Deal', value: 'deal' },
],
default: 'contact',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: { show: { resource: ['contact'] } },
options: [
{ name: 'Create', value: 'create', action: 'Create a contact' },
{ name: 'Get', value: 'get', action: 'Get a contact' },
{ name: 'Update', value: 'update', action: 'Update a contact' },
],
default: 'create',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: {
show: { resource: ['contact'], operation: ['create'] }
},
default: '',
required: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('myServiceApi');
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
try {
let responseData: unknown;
if (resource === 'contact' && operation === 'create') {
const email = this.getNodeParameter('email', i) as string;
responseData = await this.helpers.request({
method: 'POST',
url: `${credentials.baseUrl}/contacts`,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
body: { email },
json: true,
});
}
returnData.push({ json: responseData as object });
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: i });
continue;
}
throw new NodeApiError(this.getNode(), error);
}
}
return [returnData];
}
}
Installing Custom Node
# In n8n directory
npm install /path/to/my-n8n-nodes
# or from npm registry
npm install n8n-nodes-myservice
# docker-compose: mounting custom nodes
volumes:
- ./custom-nodes:/home/node/.n8n/custom
environment:
N8N_CUSTOM_EXTENSIONS: "/home/node/.n8n/custom"
Timeline
Custom node with 2–3 operations + credentials — 2–4 days. Complex node with polling trigger, pagination, binary data — 1–2 weeks.







