# N8n Impl Custom Nodes

> 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.

- Skill: `majiayu000/n8n-impl-custom-nodes` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/n8n-impl-custom-nodes`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/n8n-impl-custom-nodes/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/n8n-impl-custom-nodes

---


# 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.

```json
{
    "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:

```typescript
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 `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.

```typescript
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 `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.

```typescript
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.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

```typescript
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 `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
```bash
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
```bash
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
```bash
npm run build  # Compiles TypeScript to dist/
npm run lint   # Checks code quality (ALWAYS run before publishing)
```

### Step 4: Test Locally with npm link
```bash
# 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
```bash
npm publish    # Publishes to npm registry
```

## Testing with WorkflowTestData

See [references/examples.md](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

- [ ] Package name starts with `n8n-nodes-`
- [ ] `keywords` includes `"n8n-community-node-package"`
- [ ] `n8n.nodes` and `n8n.credentials` point to `dist/*.js` files
- [ ] `n8n-workflow` is in `peerDependencies`
- [ ] `files` field includes `["dist"]`
- [ ] `npm run build` succeeds without errors
- [ ] `npm run lint` passes
- [ ] Icon is included (SVG preferred)
- [ ] README.md describes installation and available nodes

## Reference Links

- [Project structure, package.json, development commands](references/methods.md)
- [Complete examples: programmatic, declarative, credential, tests](references/examples.md)
- [Common mistakes and anti-patterns](references/anti-patterns.md)
- [n8n-nodes-starter template](https://github.com/n8n-io/n8n-nodes-starter)
- [n8n creating nodes docs](https://docs.n8n.io/integrations/creating-nodes/)
- [n8n community nodes docs](https://docs.n8n.io/integrations/community-nodes/)

