From n8n-node-builder
Builds production-ready custom n8n community nodes using n8n-node CLI for declarative REST API and programmatic styles, covering scaffolding, implementation, testing, linting, and publishing.
npx claudepluginhub geckse/n8n-skills --plugin n8n-node-builderThis skill uses the workspace's default tool permissions.
Build production-ready custom nodes for n8n using the official `n8n-node` CLI tool and n8n's best practices.
Provides operation-aware guidance for configuring n8n nodes, detailing required fields per operation, property dependencies, displayOptions visibility, and optimal get_node detail levels.
Creates, edits n8n workflows as TypeScript files with node docs access and n8nac CLI for workspace init, preventing param errors.
Guides n8n node configuration for specific resources and operations, covering required fields, property dependencies, get_node detail levels, and common patterns.
Share bugs, ideas, or general feedback.
Build production-ready custom nodes for n8n using the official n8n-node CLI tool and n8n's best practices.
This skill uses progressive disclosure. The SKILL.md covers the full workflow and decision-making. For complete code templates, read these reference files:
references/declarative-node.md — Full declarative node template with routing, credentials, and codex filereferences/programmatic-node.md — Full programmatic node template with execute method, error handling, item linking, and trigger patternsreferences/credentials.md — All credential/auth patterns (API key, Bearer, OAuth2, Basic, Custom, testedBy)references/publishing.md — Linting, testing, releasing, and verification checklistreferences/common-mistakes.md — Error catalog with 36 numbered mistake patterns and fixesRead the appropriate reference file before writing any code.
Building an n8n node follows this sequence:
n8n-node CLInpm run devnpm run lintn8n has two node-building styles. Picking the right one up front saves significant rework.
Use declarative when the integration is a REST API wrapper. It's JSON-based, simpler, more future-proof, and faster to get approved for n8n Cloud.
The declarative style handles data flow through a routing key inside the operations object. There's no execute() method — n8n constructs HTTP requests from the JSON description automatically.
Declarative nodes support advanced patterns beyond simple routing: declarative dynamic dropdowns via typeOptions.loadOptions.routing with setKeyValue/sort postReceive transforms, dynamic property paths using $parent/$index expressions for nested body structures, routing on any parameter field (not just operations), preSend functions (including factory patterns) to transform request bodies before sending, custom postReceive functions to transform responses (including binary file handling with binaryData type), custom pagination functions with duplicate detection, three pagination modes (offset, generic token-based, and cursor-based via custom functions), resourceLocator parameters for multi-mode entity selection (list/URL/ID), resourceMapper for dynamic field mapping UIs, fixedCollection for structured filters/sort rules, advanced displayOptions with _cnd operators (eq, not, gte, lte, startsWith, includes, regex, exists, etc.) and @version/@tool special keys, ignoreHttpStatusErrors for custom error handling in postReceive, conditional transforms with enabled/errorMessage on all postReceive types, propertyInDotNotation control for literal dot keys, dynamic base URLs from credentials, and a methods object for listSearch, loadOptions, and resourceMapping. See references/declarative-node.md → "Advanced Declarative Patterns" for complete templates and a TypeScript type reference.
Choose declarative when:
preSend/postReceive functions)preSend with form-data, postReceive with binary data)resourceMapper)Use programmatic when you need full control over execution. It requires an execute() method that reads inputs, builds requests, and returns results manually.
You must use programmatic for:
Ask: "Is this a REST API with no triggers and no multi-call chaining?" If yes → declarative (even for complex request/response transformation, pagination, file handling, and field mapping — use preSend/postReceive functions). Otherwise → programmatic.
The CLI sets up the correct project structure, dependencies, linter config, and build scripts automatically.
npm create @n8n/node@latest n8n-nodes-<YOUR_NODE_NAME> -- --template <template>
Templates:
declarative/github-issues — Demo with multiple operations and credentials (good for learning)declarative/custom — Blank declarative starting point (prompts for base URL, auth type)programmatic/example — Programmatic with full flexibilitynpm install --global @n8n/node-cli
n8n-node new n8n-nodes-<YOUR_NODE_NAME> --template <template>
git clone https://github.com/n8n-io/n8n-nodes-starter.git n8n-nodes-<YOUR_NODE_NAME>
cd n8n-nodes-<YOUR_NODE_NAME>
rm -rf .git && git init
npm install
The starter provides pre-configured TypeScript, ESLint, build scripts, and example files. After cloning, rename/replace the example node and credential files with your own and update package.json.
Package names must follow one of these formats:
n8n-nodes-<NAME> (e.g., n8n-nodes-acme)@<ORG>/n8n-nodes-<NAME> (e.g., @myorg/n8n-nodes-acme)After scaffolding, the project looks like:
n8n-nodes-<name>/
├── package.json # Must contain "n8n" attribute listing nodes and credentials
├── tsconfig.json
├── .eslintrc.js # Don't edit — contains n8n linter config
├── nodes/
│ └── <NodeName>/
│ ├── <NodeName>.node.ts # Base file — the node's core logic
│ ├── <NodeName>.node.json # Codex file — metadata for n8n's node panel
│ └── <NodeName>.svg # Icon — square SVG recommended
├── credentials/
│ └── <NodeName>Api.credentials.ts # Credential file
└── dist/ # Built output (generated by build command)
Every node needs three files at minimum: the base file, the codex file, and the credentials file (unless no auth is needed).
<Name>.node.ts)This is the heart of the node. It exports a class implementing INodeType with a description object.
Critical rules:
Acme → file Acme.node.ts)NodeConnectionType.Main for inputs/outputs (imported from n8n-workflow). If your n8n-workflow version exports it as type-only, use the string 'main' as fallbackname field in the description must be a camelCase unique identifierdisplayName and all UI-facing stringsnoDataExpression: true on Resource and Operation selectorsaction on every operation option (e.g., action: 'Create a contact')import type for symbols only used in type annotations (rule of thumb: if a symbol only appears in : Type annotations, function signatures, or as Type casts, use import type; if it's used as a value like throw new NodeApiError(...), use regular import)= prefix: '=/contacts/{{$parameter["id"]}}'execute() method — if requestDefaults is present, n8n uses the routing engine and ignores execute(). Use one or the otherexecute() method must return [returnData] — an array of arrays (one per output connector). Forgetting the outer array is a common errorStandard description parameters (same for both styles):
| Parameter | Type | Purpose |
|---|---|---|
displayName | string | Name shown in the UI |
name | string | Internal camelCase identifier |
icon | string | 'file:<name>.svg' — reference the icon file |
group | string[] | ['transform'] for action nodes, ['trigger'] for triggers |
version | number or number[] | Start at 1; use array for light versioning |
subtitle | string | Template shown below node name, e.g. '={{$parameter["operation"]}}' |
description | string | Short description for the node panel |
defaults | object | { name: 'Display Name' } |
inputs | array | [NodeConnectionType.Main] |
outputs | array | [NodeConnectionType.Main] |
usableAsTool | boolean | true — enables use as an AI agent tool (recommended) |
credentials | array | [{ name: 'credName', required: true }] |
properties | array | Resource, operation, and field definitions |
For declarative nodes, also add:
requestDefaults: { baseURL: '...', headers: { Accept: 'application/json' } } — supports dynamic expressions from credentials (e.g., '={{ !$credentials.customBaseUrl ? "https://api.example.com/v1" : $credentials.baseUrl }}')routing key to define HTTP method, URL, query strings, and body — use encodeURIComponent() for user values in URLsrouting can be placed on any parameter (not just operations) — fields, fixedCollections, etc.typeOptions.loadOptions.routing for declarative dynamic dropdowns with setKeyValue/sort postReceive transforms$parent, $index expressions (e.g., '=attributes.{{$parent.fieldName}}', '=items[{{$index}}].value')routing.send.preSend array for custom request transformation functionsrouting.output.postReceive array for custom response transformation (including binary file handling)routing.operations.pagination for custom pagination functionstype: 'resourceLocator' for multi-mode entity selection (list/URL/ID) with methods.listSearchtype: 'resourceMapper' for dynamic field mapping UIs with methods.resourceMappingtype: 'fixedCollection' for structured parameter groups (filters, sort rules) with $index for array mappingdisplayOptions.show and displayOptions.hide for fine-grained field visibility*Description.ts files per resource, spread into the main nodemethods object on the class for listSearch, loadOptions, and resourceMappingFor programmatic nodes, also add:
async execute() methodthis.getInputData() and pairedItem linkingFor complete templates, read the appropriate reference file before coding:
references/declarative-node.mdreferences/programmatic-node.mdn8n nodes follow a consistent UI pattern: Resource (what entity) → Operation (what action).
Each resource gets a dropdown, each operation gets a dropdown filtered by the selected resource using displayOptions.show. Operations should map to CRUD verbs where applicable: Create, Create or Update (Upsert), Delete, Get, Get Many, Update. Use the action field on each operation option to provide a human-readable description (e.g., action: 'Create a contact'). For Upsert, use displayName "Create or Update" with description "Create a new record or update an existing one (upsert)".
Important naming rule: The linter enforces naming list operations "Get Many" (not "Get All"). The operation value should be getAll but the display name must be Get Many.
For list ("Get Many") operations, always include a returnAll boolean toggle (default false, description 'Whether to return all results or only up to a given limit') paired with a conditional limit number field that only shows when returnAll is false (displayOptions: { show: { returnAll: [false] } }). This is the standard pattern used across all n8n built-in nodes. See both reference templates for complete examples.
Use displayOptions.show to conditionally display fields based on the selected resource, operation, or other parameter values (e.g., show: { resource: ['contact'], operation: ['create'] }). For version-specific fields, use '@version': displayOptions: { show: { '@version': [2] } }.
Group optional parameters under a collection named "Additional Fields":
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: { resource: ['contact'], operation: ['create'] },
},
options: [
// Individual optional fields here
],
}
<Name>.node.json)Metadata controlling how the node appears in n8n's node discovery panel:
{
"node": "n8n-nodes-<package>.<nodeName>",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Miscellaneous"],
"resources": {
"credentialDocumentation": [{ "url": "" }],
"primaryDocumentation": [{ "url": "" }]
}
}
The node field format is <npm-package-name>.<node-internal-name> (e.g., n8n-nodes-acme.acmeService).
Categories: Analytics, Communication, Data & Storage, Development, Finance & Accounting, Marketing & Content, Miscellaneous, Productivity, Sales, Utility.
Read references/credentials.md for complete patterns. Key points:
credentials/<Name>Api.credentials.tsICredentialTypename must match the node's credentials[].nameauthenticate: IAuthenticateGeneric for header/body/query authtest: ICredentialTestRequest to validate credentials (or testedBy in the node for complex validation)$credentials (plural) in expressions — $credential (singular) is wrongicon property using Icon type from n8n-workflowSVG is recommended (square aspect ratio). PNG alternative: 60×60px. Place alongside the .node.ts file. Reference with icon: 'file:<name>.svg'. For light/dark variants: icon: { light: 'file:icon.svg', dark: 'file:icon.dark.svg' }. Don't reference Font Awesome — download and embed.
Use NodeApiError for API errors and NodeOperationError for validation errors (both from n8n-workflow). Wrap each item's processing in try/catch and support continueOnFail() so users can choose to keep going on errors — push { json: { error: message }, pairedItem: { item: i } } on failure. See references/programmatic-node.md → "Error Handling Patterns" for full examples including HTTP status-specific handling.
Every output item in a programmatic node must link back to its source input. There are two approaches:
Modern approach (recommended): Use constructExecutionMetaData:
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData: { item: i } },
);
returnData.push(...executionData);
Manual approach: Set pairedItem directly:
returnData.push({
json: responseData,
pairedItem: { item: i },
});
Without item linking, n8n can't trace data flow between nodes.
Use n8n's built-in helpers — no external HTTP libraries:
// Without auth:
const response = await this.helpers.httpRequest(options);
// With auth (handles credential injection automatically):
const response = await this.helpers.httpRequestWithAuthentication.call(
this, 'credentialTypeName', options
);
Deprecation warning: this.helpers.requestWithAuthentication and IRequestOptions are deprecated. Always use httpRequestWithAuthentication with IHttpRequestOptions. The new interface uses url (not uri) and defaults to JSON parsing.
For programmatic nodes, create a GenericFunctions.ts helper to centralize HTTP logic. Include IHookFunctions, IWebhookFunctions, and IPollFunctions in the this type union for trigger node compatibility. See references/programmatic-node.md → "GenericFunctions.ts Pattern" for the full template with pagination variants.
Use loadOptionsMethod for dropdowns that fetch values from an API at runtime. Define a methods.loadOptions object in the node class, with each method returning Array<{ name: string, value: string }>. See references/programmatic-node.md for the complete pattern.
Light versioning (all node types): Change version to an array [1, 2] and use displayOptions: { show: { '@version': [2] } }.
Full versioning (programmatic only): Extend NodeVersionedType with separate v1/, v2/ directories. See the Mattermost node on GitHub for a real example.
npm run dev # Live-reload local n8n with your node
npm run lint # Check against n8n standards
npm run lint -- --fix # Auto-fix what's possible
n8n-node release # Publish to npm (uses release-it)
Read references/publishing.md for the full publishing and verification checklist.
import type for type-only imports (if a symbol only appears in : Type annotations or as Type casts, use import type)httpRequestWithAuthentication (not the deprecated requestWithAuthentication); use url not uri in IHttpRequestOptionsstructuredClone()n8n-workflow should be a peer dependency, not bundlednoDataExpression: true on selectorsaction on every operation optionconstructExecutionMetaData with itemData for proper item linkingcontinueOnFail() in every execute loopexecute() method returns [returnData] — don't forget the outer array wrapperGenericFunctions.ts for shared API request helpers (include IHookFunctions and IWebhookFunctions in the this type for trigger node compatibility)usableAsTool: true to node descriptions for AI agent compatibilityreturnAll / limit pair for list operationsdisplayOptions for progressive field disclosureinputs: [], group: ['trigger'], "Trigger" suffix in displayName and class namevalue names across operations"strict": true in the n8n config of package.json$credentials (plural) in credential expressions — $credential (singular) won't resolve= prefix: '=/path/{{$parameter.id}}'execute() — use routing OR execute, not bothtypeOptions.loadOptions.routing for declarative dynamic dropdowns — chain rootProperty → setKeyValue → sort postReceive transforms$parent.fieldName for nested objects, $index for array indexing in fixedCollectionsencodeURIComponent() / encodeURI() for user-provided values in routing URLsrouting on any parameter that needs it (fields, fixedCollections), not just on operations*Description.ts files per resource, spread into properties arraypreSend functions for custom request body transformation in declarative nodes — they receive and return IHttpRequestOptionspostReceive functions for response transformation beyond rootProperty/filter/limit/set/setKeyValue/sort/binaryData — they receive (items, response) and return INodeExecutionData[]enabled (boolean/expression) and errorMessage propertiesreturnFullResponse: true and encoding: 'arraybuffer' on the request, then handle binary conversion in postReceivepreSend to build FormData from this.helpers.getBinaryDataBuffer()ignoreHttpStatusErrors: true on request when you need custom error handling in postReceivepropertyInDotNotation: false on routing.send when property names contain literal dots (default is true, which creates nested objects)type: 'generic' with $response.body/$request expressionsIExecutePaginationFunctions and makeRoutingRequest()_cnd operators in displayOptions for advanced conditions: { _cnd: { gte: 2 } }, { _cnd: { startsWith: 'https' } }, etc.@version, @tool, @feature special keys in displayOptions for version/context-specific fieldstype: 'resourceLocator' for entity selection (provides list, URL, and ID modes) — requires methods.listSearch on the node classtype: 'resourceMapper' for dynamic field mapping (Create/Update) — requires methods.resourceMapping on the node classtype: 'fixedCollection' with multipleValues: true for repeatable structured parameter groups (filters, sort rules)displayOptions.show and displayOptions.hide for excluding specific parameter valuesreferences/common-mistakes.md for the full error catalogThese patterns are required for verified community nodes and recommended for all nodes:
Delete operation output: Always return { deleted: true } (not { success: true }) from Delete operations. This confirms the deletion and triggers the following node.
Simplify toggle: When an endpoint returns data with more than 10 fields, add a "Simplify" boolean parameter that returns a curated subset of max 10 fields. Use displayName Simplify and description Whether to return a simplified version of the response instead of the raw data. Flatten nested fields in simplified mode.
AI Tool Output parameter: For nodes used as AI agent tools, add an "Output" options parameter with three modes: Simplified (same as Simplify above), Raw (all fields), and Selected Fields (user picks which fields to send to the AI agent). This prevents context window overflow.
Resource Locator: Use type: 'resourceLocator' instead of a plain string input whenever a user needs to select a single item (e.g., a specific document, board, or channel). It offers ID, URL, and "From list" modes. Default to "From list" when available. See the Trello and Google Drive nodes for examples.
Sorting options for Get Many: Enhance list operations by providing sorting options in a dedicated collection below the main "Options" collection.
Binary data naming: Don't use "binary data" or "binary property" in field names. Instead use "Input Data Field Name" / "Output Data Field Name".
Upsert: When the API supports it, include "Create or Update" as a separate operation alongside Create and Update.
Triggers are always programmatic. Four patterns:
| Type | Method | Use When | Example |
|---|---|---|---|
| Webhook (auto) | webhook() + webhookMethods | Service supports API-based webhook registration | Stripe Trigger |
| Webhook (manual) | webhook() only | User pastes webhook URL into external service | Generic Webhook |
| Polling | poll() | No webhook support; check for new data on a schedule | Gmail Trigger |
| Event/Stream | trigger() | Long-running connection (WebSocket, SSE, message queue) | AMQP Trigger |
Key differences from action nodes:
group: ['trigger'] and suffix the displayName with "Trigger"inputs: [] — they have NO inputsTrigger suffix (e.g., MyServiceTrigger)getWorkflowStaticData('node') to persist state (webhook IDs, last-checked timestamps) between callsFor complete trigger templates with full code examples, read references/programmatic-node.md → "Trigger Node Patterns".
For many resources/operations, split into modules:
nodes/MyNode/
├── MyNode.node.ts # Main entry
├── GenericFunctions.ts # Shared API request helpers
├── actions/ # One dir per resource
│ ├── contact/
│ │ ├── create.ts
│ │ ├── get.ts
│ │ └── index.ts
│ └── deal/
│ └── index.ts
├── methods/ # loadOptions, etc.
└── transport/ # Shared HTTP helpers
Data flows between nodes as arrays of items. Each item has json (required) and optionally binary. The execute() method returns Promise<INodeExecutionData[][]> — an array of arrays (one per output). Use this.helpers.returnJsonArray(responseData) to wrap raw data, and remember to return [returnData] (nested array).