> building-n8n-nodes
Builds custom community nodes for n8n, the workflow automation platform. Activates when the user wants to create, scaffold, develop, test, lint, or publish an n8n node — including both declarative (REST API) and programmatic styles. Also triggers when the user mentions n8n nodes, n8n-cli, n8n-node, community nodes, node credentials, or anything related to extending n8n with custom integrations. Encodes all official best practices from n8n's documentation.
curl "https://skillshub.wtf/geckse/n8n-skills/building-n8n-nodes?format=md"n8n Node Builder
Build production-ready custom nodes for n8n using the official n8n-node CLI tool and n8n's best practices.
When You Need More Detail
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 fixes
Read the appropriate reference file before writing any code.
Workflow Overview
Building an n8n node follows this sequence:
- Decide on the node style (declarative vs programmatic)
- Scaffold the project with the
n8n-nodeCLI - Implement the node base file, credentials file, and codex file
- Test locally with
npm run dev - Lint with
npm run lint - Publish to npm and optionally submit for verification
Step 1: Choose Your Node Style
n8n has two node-building styles. Picking the right one up front saves significant rework.
Declarative Style (preferred for REST APIs)
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:
- The API is REST-based
- You want a simpler, lower-risk codebase
- Even if you need custom request/response transformation (use
preSend/postReceivefunctions) - Even if you need file upload/download (use
preSendwith form-data,postReceivewith binary data) - Even if you need dynamic field mapping (use
resourceMapper) - Even if you need custom pagination logic (use custom pagination functions)
Programmatic Style (required for advanced use cases)
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:
- Trigger nodes (webhook, polling, or other event-driven)
- GraphQL APIs
- Non-REST protocols
- Complex multi-step logic that chains multiple sequential API calls where later calls depend on earlier results
Quick Decision
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.
Step 2: Scaffold with the n8n-node CLI
The CLI sets up the correct project structure, dependencies, linter config, and build scripts automatically.
Option A: Without installing (recommended)
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 flexibility
Option B: Install globally
npm install --global @n8n/node-cli
n8n-node new n8n-nodes-<YOUR_NODE_NAME> --template <template>
Option C: Clone the n8n-nodes-starter repo
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.
Naming Rules
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)
Step 3: Implement the Node
Every node needs three files at minimum: the base file, the codex file, and the credentials file (unless no auth is needed).
3A: The Node Base File (<Name>.node.ts)
This is the heart of the node. It exports a class implementing INodeType with a description object.
Critical rules:
- The class name must match the filename (e.g., class
Acme→ fileAcme.node.ts) - Use
NodeConnectionType.Mainfor inputs/outputs (imported fromn8n-workflow). If yourn8n-workflowversion exports it as type-only, use the string'main'as fallback - The
namefield in the description must be a camelCase unique identifier - Use Title Case for
displayNameand all UI-facing strings - Always set
noDataExpression: trueon Resource and Operation selectors - Always include
actionon every operation option (e.g.,action: 'Create a contact') - Use
import typefor symbols only used in type annotations (rule of thumb: if a symbol only appears in: Typeannotations, function signatures, oras Typecasts, useimport type; if it's used as a value likethrow new NodeApiError(...), use regular import) - Dynamic expressions in routing must start with
=prefix:'=/contacts/{{$parameter["id"]}}' - Declarative nodes cannot have an
execute()method — ifrequestDefaultsis present, n8n uses the routing engine and ignoresexecute(). Use one or the other - The
execute()method must return[returnData]— an array of arrays (one per output connector). Forgetting the outer array is a common error
Standard 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 }}')- Operations use a
routingkey to define HTTP method, URL, query strings, and body — useencodeURIComponent()for user values in URLs routingcan be placed on any parameter (not just operations) — fields, fixedCollections, etc.- Use
typeOptions.loadOptions.routingfor declarative dynamic dropdowns withsetKeyValue/sortpostReceive transforms - Use dynamic property paths with
$parent,$indexexpressions (e.g.,'=attributes.{{$parent.fieldName}}','=items[{{$index}}].value') - Operations can use
routing.send.preSendarray for custom request transformation functions - Operations can use
routing.output.postReceivearray for custom response transformation (including binary file handling) - Operations can use
routing.operations.paginationfor custom pagination functions - Use
type: 'resourceLocator'for multi-mode entity selection (list/URL/ID) withmethods.listSearch - Use
type: 'resourceMapper'for dynamic field mapping UIs withmethods.resourceMapping - Use
type: 'fixedCollection'for structured parameter groups (filters, sort rules) with$indexfor array mapping - Combine
displayOptions.showanddisplayOptions.hidefor fine-grained field visibility - Split operations/fields into separate
*Description.tsfiles per resource, spread into the main node - Define a
methodsobject on the class forlistSearch,loadOptions, andresourceMapping
For programmatic nodes, also add:
- An
async execute()method - Proper item looping with
this.getInputData()andpairedItemlinking
For complete templates, read the appropriate reference file before coding:
- Declarative → Read
references/declarative-node.md - Programmatic → Read
references/programmatic-node.md
3B: The Resource → Operation Pattern
n8n 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.
Return All / Limit Pattern
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.
3C: displayOptions and Conditional Fields
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] } }.
3D: Additional Fields (Optional Parameters)
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
],
}
3E: The Codex File (<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.
3F: Credentials
Read references/credentials.md for complete patterns. Key points:
- File:
credentials/<Name>Api.credentials.ts - Class implements
ICredentialType namemust match the node'scredentials[].name- Use
authenticate: IAuthenticateGenericfor header/body/query auth - Use
test: ICredentialTestRequestto validate credentials (ortestedByin the node for complex validation) - Always use
$credentials(plural) in expressions —$credential(singular) is wrong - The linter requires an
iconproperty usingIcontype from n8n-workflow
3G: The Icon
SVG 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.
Step 4: Error Handling (Programmatic Nodes)
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.
Step 5: Item Linking (pairedItem / constructExecutionMetaData)
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.
Step 6: HTTP Helpers
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.
GenericFunctions.ts Pattern
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.
Dynamic Options (loadOptionsMethod)
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.
Step 7: Node Versioning
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.
Step 8: Test, Lint, Publish
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.
Code Standards Summary
- Write in TypeScript; use
import typefor type-only imports (if a symbol only appears in: Typeannotations oras Typecasts, useimport type) - Use
httpRequestWithAuthentication(not the deprecatedrequestWithAuthentication); useurlnoturiinIHttpRequestOptions - Never mutate incoming data — clone with spread or
structuredClone() - No external runtime dependencies for verified nodes — use built-in helpers only
n8n-workflowshould be a peer dependency, not bundled- Follow Resource → Operation pattern with
noDataExpression: trueon selectors - Always include
actionon every operation option - Use
constructExecutionMetaDatawithitemDatafor proper item linking - Implement
continueOnFail()in every execute loop - The
execute()method returns[returnData]— don't forget the outer array wrapper - Create
GenericFunctions.tsfor shared API request helpers (includeIHookFunctionsandIWebhookFunctionsin thethistype for trigger node compatibility) - Add
usableAsTool: trueto node descriptions for AI agent compatibility - Name list operations "Get Many" (not "Get All") — the linter enforces this
- Use
returnAll/limitpair for list operations - Use
displayOptionsfor progressive field disclosure - Optional params go in "Additional Fields" collections
- Title Case for UI text; Sentence case for descriptions/hints
- Trigger nodes:
inputs: [],group: ['trigger'], "Trigger" suffix indisplayNameand class name - Reuse internal parameter
valuenames across operations - Set
"strict": truein then8nconfig ofpackage.json - Use
$credentials(plural) in credential expressions —$credential(singular) won't resolve - Dynamic expressions in routing need the
=prefix:'=/path/{{$parameter.id}}' - Declarative nodes cannot have
execute()— use routing OR execute, not both - Use
typeOptions.loadOptions.routingfor declarative dynamic dropdowns — chainrootProperty→setKeyValue→sortpostReceive transforms - Use dynamic property paths:
$parent.fieldNamefor nested objects,$indexfor array indexing in fixedCollections - Use
encodeURIComponent()/encodeURI()for user-provided values in routing URLs - Place
routingon any parameter that needs it (fields, fixedCollections), not just on operations - Split multi-resource nodes into
*Description.tsfiles per resource, spread into properties array - Use
preSendfunctions for custom request body transformation in declarative nodes — they receive and returnIHttpRequestOptions - Use custom
postReceivefunctions for response transformation beyondrootProperty/filter/limit/set/setKeyValue/sort/binaryData— they receive(items, response)and returnINodeExecutionData[] - All postReceive transforms support optional
enabled(boolean/expression) anderrorMessageproperties - For file downloads in declarative nodes, set
returnFullResponse: trueandencoding: 'arraybuffer'on the request, then handle binary conversion inpostReceive - For file uploads in declarative nodes, use
preSendto buildFormDatafromthis.helpers.getBinaryDataBuffer() - Use
ignoreHttpStatusErrors: trueon request when you need custom error handling in postReceive - Use
propertyInDotNotation: falseonrouting.sendwhen property names contain literal dots (default istrue, which creates nested objects) - For generic/token-based pagination, use
type: 'generic'with$response.body/$requestexpressions - For cursor-based pagination, create a reusable factory function using
IExecutePaginationFunctionsandmakeRoutingRequest() - Use
_cndoperators in displayOptions for advanced conditions:{ _cnd: { gte: 2 } },{ _cnd: { startsWith: 'https' } }, etc. - Use
@version,@tool,@featurespecial keys in displayOptions for version/context-specific fields - Use
type: 'resourceLocator'for entity selection (provides list, URL, and ID modes) — requiresmethods.listSearchon the node class - Use
type: 'resourceMapper'for dynamic field mapping (Create/Update) — requiresmethods.resourceMappingon the node class - Use
type: 'fixedCollection'withmultipleValues: truefor repeatable structured parameter groups (filters, sort rules) - Combine
displayOptions.showanddisplayOptions.hidefor excluding specific parameter values - Pass the linter before publishing — see
references/common-mistakes.mdfor the full error catalog
UX Patterns (Verification Requirements)
These 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.
Trigger Nodes
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:
- Set
group: ['trigger']and suffix thedisplayNamewith "Trigger" - Trigger nodes have
inputs: []— they have NO inputs - Class names and filenames get the
Triggersuffix (e.g.,MyServiceTrigger) - Use
getWorkflowStaticData('node')to persist state (webhook IDs, last-checked timestamps) between calls
For complete trigger templates with full code examples, read references/programmatic-node.md → "Trigger Node Patterns".
Modular Structure (Complex Nodes)
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
n8n Data Structure
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).