AgentSkillsCN

n8n-create-nodes

创建n8n社区节点,构建n8n集成,搭建n8n节点包。在创建n8n节点、构建声明式或程序式节点、定义INodeType类、编写凭证类型、创建触发节点(Webhook/轮询)、配置节点属性与UI元素、搭建n8n-nodes-starter项目、将社区节点发布至npm,或与n8n-workflow SDK接口协作时使用此功能。

SKILL.md
--- frontmatter
name: n8n-create-nodes
description: Create n8n community nodes, build n8n integrations, scaffold n8n node packages. Use when creating n8n nodes, building declarative or programmatic nodes, defining INodeType classes, writing credential types, creating trigger nodes (webhook/poll), configuring node properties and UI elements, setting up n8n-nodes-starter projects, publishing community nodes to npm, or working with the n8n-workflow SDK interfaces.

n8n Community Node Creation

Build production-ready n8n community nodes as npm packages. This skill covers both declarative (REST API wrapping) and programmatic (custom logic) node styles.

References: CREDENTIAL_PATTERNS.md | TRIGGER_PATTERNS.md | EXAMPLES.md | COMMON_MISTAKES.md


Quick Reference: Declarative vs Programmatic

AspectDeclarativeProgrammatic
Best forSimple REST API wrappersCustom logic, transforms, multi-call flows
HTTP handlingAutomatic via routing keysManual in execute() method
Key propertyrequestDefaults in descriptionasync execute() method
Imports neededINodeType, INodeTypeDescription+ IExecuteFunctions, INodeExecutionData
ComplexityLower — declare routes, n8n handles requestsHigher — full control over execution
PaginationVia operations.pagination configManual loop in apiRequestAllItems helper
When to chooseAPI maps 1:1 to operations, no transformsNeed data manipulation, conditional logic, or chained calls

Decision rule: If every operation is a single HTTP request with simple input→body mapping and the response can be used as-is (or with minor postReceive transforms), use declarative. Otherwise, use programmatic.


Getting Started

1. Scaffold from Starter

bash
# Clone the starter repo
git clone https://github.com/n8n-io/n8n-nodes-starter.git n8n-nodes-<yourservice>
cd n8n-nodes-<yourservice>
npm install

2. Required Files

FilePurpose
nodes/<Name>/<Name>.node.tsMain node class (logic + description)
nodes/<Name>/<Name>.node.jsonCodex file (categories, doc links)
nodes/<Name>/<name>.svgNode icon (SVG, 60×60px recommended)
credentials/<Name>Api.credentials.tsCredential/auth definition
package.jsonnpm config with n8n object linking nodes & credentials

Critical rules:

  • npm package name must start with n8n-nodes-
  • Class name must match filename (class MyServiceMyService.node.ts)
  • Icon filename is lowercase (myservice.svg)

3. Package.json n8n Config

json
{
  "name": "n8n-nodes-myservice",
  "version": "0.1.0",
  "n8n": {
    "n8nNodesApiVersion": 1,
    "nodes": [
      "dist/nodes/MyService/MyService.node.js"
    ],
    "credentials": [
      "dist/credentials/MyServiceApi.credentials.js"
    ]
  }
}

Node Class Structure

Every node implements INodeType with a description object:

typescript
import { INodeType, INodeTypeDescription, NodeConnectionType } from 'n8n-workflow';

export class MyService implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'My Service',
    name: 'myService',
    icon: 'file:myservice.svg',
    group: ['transform'],
    version: 1,
    subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
    description: 'Interact with My Service API',
    defaults: { name: 'My Service' },
    inputs: [NodeConnectionType.Main],
    outputs: [NodeConnectionType.Main],
    credentials: [
      { name: 'myServiceApi', required: true },
    ],
    properties: [
      // Resource and operation selectors, then parameter fields
    ],
  };
}

Required Description Properties

PropertyTypeNotes
displayNamestringShown in node panel
namestringcamelCase internal ID, unique across all nodes
iconstring'file:icon.svg' — ref to SVG in node folder
groupstring[]['transform'], ['output'], ['input'], or ['trigger']
versionnumberStart at 1, increment for breaking changes
subtitlestringExpression template shown below node name
descriptionstringShort one-liner for node panel
defaultsobject{ name: 'Display Name' }
inputsarray[NodeConnectionType.Main] — empty [] for triggers
outputsarray[NodeConnectionType.Main]
credentialsarray[{ name: 'credName', required: true }]
propertiesINodeProperties[]UI fields — resources, operations, parameters

Resource & Operation Pattern

The standard way to organize a node with multiple API resources:

typescript
properties: [
  // 1. Resource selector
  {
    displayName: 'Resource',
    name: 'resource',
    type: 'options',
    noDataExpression: true,       // REQUIRED on resource/operation selectors
    options: [
      { name: 'Contact', value: 'contact' },
      { name: 'Deal', value: 'deal' },
    ],
    default: 'contact',
  },
  // 2. Operation selector (per resource)
  {
    displayName: 'Operation',
    name: 'operation',
    type: 'options',
    noDataExpression: true,
    displayOptions: { show: { resource: ['contact'] } },
    options: [
      { name: 'Create', value: 'create', action: 'Create a contact' },
      { name: 'Delete', value: 'delete', action: 'Delete a contact' },
      { name: 'Get', value: 'get', action: 'Get a contact' },
      { name: 'Get Many', value: 'getAll', action: 'Get many contacts' },
      { name: 'Update', value: 'update', action: 'Update a contact' },
    ],
    default: 'create',
  },
  // 3. Operation-specific fields
  {
    displayName: 'Contact ID',
    name: 'contactId',
    type: 'string',
    required: true,
    default: '',
    displayOptions: {
      show: { resource: ['contact'], operation: ['get', 'update', 'delete'] },
    },
    description: 'The ID of the contact',
  },
  // 4. returnAll / limit pair for getAll
  {
    displayName: 'Return All',
    name: 'returnAll',
    type: 'boolean',
    default: false,
    displayOptions: { show: { resource: ['contact'], operation: ['getAll'] } },
    description: 'Whether to return all results or only up to a given limit',
  },
  {
    displayName: 'Limit',
    name: 'limit',
    type: 'number',
    default: 50,
    typeOptions: { minValue: 1 },
    displayOptions: {
      show: { resource: ['contact'], operation: ['getAll'], returnAll: [false] },
    },
    description: 'Max number of results to return',
  },
  // 5. Additional fields (optional params)
  {
    displayName: 'Additional Fields',
    name: 'additionalFields',
    type: 'collection',
    placeholder: 'Add Field',
    default: {},
    displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
    options: [
      { displayName: 'Email', name: 'email', type: 'string', default: '' },
      { displayName: 'Phone', name: 'phone', type: 'string', default: '' },
    ],
  },
],

Key rules:

  • Always set noDataExpression: true on resource and operation selectors
  • Always include action on each operation option (used for the node action list)
  • Standard operation values: create, get, getAll, update, delete, upsert
  • Use displayOptions.show to conditionally display fields per resource/operation

Declarative Style

Add requestDefaults to description and routing to each operation. No execute() method.

typescript
description: INodeTypeDescription = {
  // ...standard properties...
  requestDefaults: {
    baseURL: 'https://api.myservice.com/v1',
    headers: { Accept: 'application/json' },
  },
  properties: [
    {
      displayName: 'Operation', name: 'operation', type: 'options',
      noDataExpression: true,
      options: [
        {
          name: 'Create', value: 'create', action: 'Create an item',
          routing: {
            request: { method: 'POST', url: '/items' },
          },
        },
        {
          name: 'Get', value: 'get', action: 'Get an item',
          routing: {
            request: { method: 'GET', url: '=/items/{{$parameter.itemId}}' },
          },
        },
      ],
      default: 'create',
    },
    // Map field values to request body/query
    {
      displayName: 'Name', name: 'name', type: 'string', default: '',
      routing: { send: { type: 'body', property: 'name' } },
    },
    {
      displayName: 'Tag', name: 'tag', type: 'string', default: '',
      routing: { send: { type: 'query', property: 'tag' } },
    },
  ],
};

Routing Keys

KeyPurposeExample
routing.requestHTTP method, URL, headers per operation{ method: 'POST', url: '/items' }
routing.sendMap parameter to body/query{ type: 'body', property: 'name' }
routing.output.postReceiveTransform response[{ type: 'rootProperty', properties: { property: 'data' } }]
routing.operations.paginationAuto-pagination config{ type: 'offset', properties: { ... } }

postReceive Transforms

typescript
routing: {
  output: {
    postReceive: [
      { type: 'rootProperty', properties: { property: 'data' } },     // Extract nested data
      { type: 'filter', properties: { pass: '={{$responseItem.active}}' } },
      { type: 'limit', properties: { maxResults: '={{$parameter.limit}}' } },
      { type: 'set', properties: { value: '={{ { "id": $response.body.id } }}' } },
    ],
  },
},

Programmatic Style

Add an async execute() method for full control:

typescript
import {
  IExecuteFunctions, INodeExecutionData, INodeType,
  INodeTypeDescription, NodeConnectionType,
} from 'n8n-workflow';

export class MyService implements INodeType {
  description: INodeTypeDescription = { /* ...same as above, minus requestDefaults/routing... */ };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];
    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;

        if (resource === 'contact') {
          if (operation === 'create') {
            const email = this.getNodeParameter('email', i) as string;
            const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
            const body: IDataObject = { email, ...additionalFields };
            responseData = await myServiceApiRequest.call(this, 'POST', '/contacts', body);
          }
          if (operation === 'getAll') {
            const returnAll = this.getNodeParameter('returnAll', i) as boolean;
            if (returnAll) {
              responseData = await myServiceApiRequestAllItems.call(
                this, 'contacts', 'GET', '/contacts',
              );
            } else {
              const limit = this.getNodeParameter('limit', i) as number;
              responseData = await myServiceApiRequest.call(
                this, 'GET', '/contacts', {}, { limit },
              );
              responseData = responseData.contacts;
            }
          }
        }

        const executionData = this.helpers.constructExecutionMetaData(
          this.helpers.returnJsonArray(responseData),
          { itemData: { item: i } },
        );
        returnData.push(...executionData);

      } catch (error) {
        if (this.continueOnFail()) {
          const executionErrorData = this.helpers.constructExecutionMetaData(
            this.helpers.returnJsonArray({ error: error.message }),
            { itemData: { item: i } },
          );
          returnData.push(...executionErrorData);
          continue;
        }
        throw error;
      }
    }

    return [returnData];
  }
}

Key Programmatic Helpers

MethodPurpose
this.getInputData()Get input items array
this.getNodeParameter(name, index)Read a user-configured parameter
this.getCredentials('credName')Retrieve stored credentials
this.helpers.returnJsonArray(data)Wrap response as INodeExecutionData[]
this.helpers.constructExecutionMetaData(data, { itemData })Link output to input items
this.continueOnFail()Check if user enabled "Continue On Fail"
this.helpers.request(options)Make HTTP request
this.helpers.requestWithAuthentication('credName', options)Authenticated HTTP request

UI Property Types

TypeDescriptionCommon typeOptions
stringText inputpassword: true, rows: N (multiline)
numberNumeric inputminValue, maxValue, numberPrecision
booleanToggle
optionsSingle-select dropdown
multiOptionsMulti-select dropdown
collection"Add Field" group of optional params
fixedCollectionStructured key-value groupsmultipleValues: true
jsonJSON code editor
dateTimeDate/time picker
colorColor picker
resourceLocatorFind by ID/URL/searchmode: ['list', 'url', 'id']
resourceMapperMap fields to external schema
noticeInfo box (not a parameter)

Dynamic Options (Load from API)

typescript
{
  displayName: 'Channel',
  name: 'channelId',
  type: 'options',
  typeOptions: { loadOptionsMethod: 'getChannels' },
  default: '',
}

// In the node class:
methods = {
  loadOptions: {
    async getChannels(this: ILoadOptionsFunctions) {
      const credentials = await this.getCredentials('myServiceApi');
      const channels = await myServiceApiRequest.call(this, 'GET', '/channels');
      return channels.map((c: any) => ({ name: c.name, value: c.id }));
    },
  },
};

Codex File (.node.json)

json
{
  "node": "n8n-nodes-myservice.myService",
  "nodeVersion": "1.0",
  "codexVersion": "1.0",
  "categories": ["Miscellaneous"],
  "resources": {
    "credentialDocumentation": [{ "url": "" }],
    "primaryDocumentation": [{ "url": "" }]
  }
}

The node field format is <package-name>.<node-internal-name>.


Error Handling

Two Error Classes

ClassUse ForImport From
NodeApiErrorExternal API failuresn8n-workflow
NodeOperationErrorInternal logic errorsn8n-workflow
typescript
// In GenericFunctions.ts — wrap API errors:
throw new NodeApiError(this.getNode(), error as JsonObject);

// In execute() — wrap logic errors:
throw new NodeOperationError(this.getNode(), 'Unsupported operation', { itemIndex: i });

continueOnFail Pattern (ALWAYS use in execute loops)

See the programmatic style example above for the full pattern. The key structure:

  1. Wrap each item's processing in try/catch
  2. On catch: if this.continueOnFail(), push error data and continue
  3. Otherwise, re-throw the error

Naming Conventions

ElementConventionExample
Node classPascalCaseMyService
Trigger classPascalCase + TriggerMyServiceTrigger
Node file{Class}.node.tsMyService.node.ts
Credential classPascalCase + ApiMyServiceApi
Credential file{Class}.credentials.tsMyServiceApi.credentials.ts
Internal namecamelCasemyService
npm packagen8n-nodes-{name}n8n-nodes-myservice
Operation valuescamelCase verbscreate, get, getAll, update, delete

Versioning Nodes

When making breaking changes, version the node instead of modifying in place:

typescript
import { INodeTypeBaseDescription, INodeTypeDescription, VersionedNodeType } from 'n8n-workflow';

export class MyService extends VersionedNodeType {
  constructor() {
    const baseDescription: INodeTypeBaseDescription = {
      displayName: 'My Service',
      name: 'myService',
      icon: 'file:myservice.svg',
      group: ['transform'],
      defaultVersion: 2,
      description: 'Interact with My Service',
    };
    const nodeVersions: IVersionedNodeType['nodeVersions'] = {
      1: new MyServiceV1(baseDescription),
      2: new MyServiceV2(baseDescription),
    };
    super(nodeVersions, baseDescription);
  }
}

Each version is a separate class with its own full INodeTypeDescription. Place in V1/ and V2/ subdirectories.


Testing & Publishing

Local Testing

bash
# Build the node package
npm run build

# Link it into your n8n installation
npm link
cd ~/.n8n
npm link n8n-nodes-myservice

# Restart n8n — your node appears in the editor
n8n start

Publishing to npm

bash
npm login
npm publish

After publishing, users install via: Settings → Community Nodes → Install → n8n-nodes-myservice

Ensure your package.json has:

  • "n8n" object with nodes and credentials arrays pointing to dist/ files
  • Correct main and files fields
  • "keywords": ["n8n-community-node-package"]

Best Practices

Do:

  • Use noDataExpression: true on resource/operation selectors
  • Include action on every operation option
  • Use constructExecutionMetaData with itemData for proper item linking
  • Implement continueOnFail() in every execute loop
  • Create GenericFunctions.ts for shared API request helpers
  • Use NodeConnectionType.Main instead of string 'main'
  • Use typeOptions: { password: true } for secret fields in credentials
  • Use returnAll/limit pair for list operations

Don't:

  • Modify v1 when adding features — create v2 instead
  • Forget the codex .node.json file
  • Use inputs other than [] for trigger nodes
  • Hard-code API base URLs in execute — use credentials or requestDefaults
  • Skip error wrapping — always use NodeApiError/NodeOperationError
  • Create large monolithic node files — split descriptions into separate files per resource

Related Files