Skip to main content
โšก Calmops

n8n Custom Nodes Development: Building Your Own Integrations

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.

Resources

Comments