Skillshub 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.
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/geckse/n8n-skills/building-n8n-nodes" ~/.claude/skills/comeonoliver-skillshub-building-n8n-nodes && rm -rf "$T"
skills/geckse/n8n-skills/building-n8n-nodes/SKILL.mdn8n 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:
— Full declarative node template with routing, credentials, and codex filereferences/declarative-node.md
— Full programmatic node template with execute method, error handling, item linking, and trigger patternsreferences/programmatic-node.md
— All credential/auth patterns (API key, Bearer, OAuth2, Basic, Custom, testedBy)references/credentials.md
— Linting, testing, releasing, and verification checklistreferences/publishing.md
— Error catalog with 36 numbered mistake patterns and fixesreferences/common-mistakes.md
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
CLIn8n-node - 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
functions)postReceive - Even if you need file upload/download (use
with form-data,preSend
with binary data)postReceive - 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:
— Demo with multiple operations and credentials (good for learning)declarative/github-issues
— Blank declarative starting point (prompts for base URL, auth type)declarative/custom
— Programmatic with full flexibilityprogrammatic/example
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:
(e.g.,n8n-nodes-<NAME>
)n8n-nodes-acme
(e.g.,@<ORG>/n8n-nodes-<NAME>
)@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
)
<Name>.node.tsThis 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
→ fileAcme
)Acme.node.ts - Use
for inputs/outputs (imported fromNodeConnectionType.Main
). If yourn8n-workflow
version exports it as type-only, use the stringn8n-workflow
as fallback'main' - The
field in the description must be a camelCase unique identifiername - Use Title Case for
and all UI-facing stringsdisplayName - Always set
on Resource and Operation selectorsnoDataExpression: true - Always include
on every operation option (e.g.,action
)action: 'Create a contact' - Use
for symbols only used in type annotations (rule of thumb: if a symbol only appears inimport type
annotations, function signatures, or: Type
casts, useas Type
; if it's used as a value likeimport type
, use regular import)throw new NodeApiError(...) - Dynamic expressions in routing must start with
prefix:='=/contacts/{{$parameter["id"]}}' - Declarative nodes cannot have an
method — ifexecute()
is present, n8n uses the routing engine and ignoresrequestDefaults
. Use one or the otherexecute() - The
method must returnexecute()
— an array of arrays (one per output connector). Forgetting the outer array is a common error[returnData]
Standard description parameters (same for both styles):
| Parameter | Type | Purpose |
|---|---|---|
| string | Name shown in the UI |
| string | Internal camelCase identifier |
| string | — reference the icon file |
| string[] | for action nodes, for triggers |
| number or number[] | Start at ; use array for light versioning |
| string | Template shown below node name, e.g. |
| string | Short description for the node panel |
| object | |
| array | |
| array | |
| boolean | — enables use as an AI agent tool (recommended) |
| array | |
| array | Resource, operation, and field definitions |
For declarative nodes, also add:
— supports dynamic expressions from credentials (e.g.,requestDefaults: { baseURL: '...', headers: { Accept: 'application/json' } }
)'={{ !$credentials.customBaseUrl ? "https://api.example.com/v1" : $credentials.baseUrl }}'- Operations use a
key to define HTTP method, URL, query strings, and body — userouting
for user values in URLsencodeURIComponent()
can be placed on any parameter (not just operations) — fields, fixedCollections, etc.routing- Use
for declarative dynamic dropdowns withtypeOptions.loadOptions.routing
/setKeyValue
postReceive transformssort - Use dynamic property paths with
,$parent
expressions (e.g.,$index
,'=attributes.{{$parent.fieldName}}'
)'=items[{{$index}}].value' - Operations can use
array for custom request transformation functionsrouting.send.preSend - Operations can use
array for custom response transformation (including binary file handling)routing.output.postReceive - Operations can use
for custom pagination functionsrouting.operations.pagination - Use
for multi-mode entity selection (list/URL/ID) withtype: 'resourceLocator'methods.listSearch - Use
for dynamic field mapping UIs withtype: 'resourceMapper'methods.resourceMapping - Use
for structured parameter groups (filters, sort rules) withtype: 'fixedCollection'
for array mapping$index - Combine
anddisplayOptions.show
for fine-grained field visibilitydisplayOptions.hide - Split operations/fields into separate
files per resource, spread into the main node*Description.ts - Define a
object on the class formethods
,listSearch
, andloadOptionsresourceMapping
For programmatic nodes, also add:
- An
methodasync execute() - Proper item looping with
andthis.getInputData()
linkingpairedItem
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
)
<Name>.node.jsonMetadata 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
must match the node'snamecredentials[].name- Use
for header/body/query authauthenticate: IAuthenticateGeneric - Use
to validate credentials (ortest: ICredentialTestRequest
in the node for complex validation)testedBy - Always use
(plural) in expressions —$credentials
(singular) is wrong$credential - The linter requires an
property usingicon
type from n8n-workflowIcon
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
for type-only imports (if a symbol only appears inimport type
annotations or: Type
casts, useas Type
)import type - Use
(not the deprecatedhttpRequestWithAuthentication
); userequestWithAuthentication
noturl
inuriIHttpRequestOptions - Never mutate incoming data — clone with spread or
structuredClone() - No external runtime dependencies for verified nodes — use built-in helpers only
should be a peer dependency, not bundledn8n-workflow- Follow Resource → Operation pattern with
on selectorsnoDataExpression: true - Always include
on every operation optionaction - Use
withconstructExecutionMetaData
for proper item linkingitemData - Implement
in every execute loopcontinueOnFail() - The
method returnsexecute()
— don't forget the outer array wrapper[returnData] - Create
for shared API request helpers (includeGenericFunctions.ts
andIHookFunctions
in theIWebhookFunctions
type for trigger node compatibility)this - Add
to node descriptions for AI agent compatibilityusableAsTool: true - Name list operations "Get Many" (not "Get All") — the linter enforces this
- Use
/returnAll
pair for list operationslimit - Use
for progressive field disclosuredisplayOptions - Optional params go in "Additional Fields" collections
- Title Case for UI text; Sentence case for descriptions/hints
- Trigger nodes:
,inputs: []
, "Trigger" suffix ingroup: ['trigger']
and class namedisplayName - Reuse internal parameter
names across operationsvalue - Set
in the"strict": true
config ofn8npackage.json - Use
(plural) in credential expressions —$credentials
(singular) won't resolve$credential - Dynamic expressions in routing need the
prefix:='=/path/{{$parameter.id}}' - Declarative nodes cannot have
— use routing OR execute, not bothexecute() - Use
for declarative dynamic dropdowns — chaintypeOptions.loadOptions.routing
→rootProperty
→setKeyValue
postReceive transformssort - Use dynamic property paths:
for nested objects,$parent.fieldName
for array indexing in fixedCollections$index - Use
/encodeURIComponent()
for user-provided values in routing URLsencodeURI() - Place
on any parameter that needs it (fields, fixedCollections), not just on operationsrouting - Split multi-resource nodes into
files per resource, spread into properties array*Description.ts - Use
functions for custom request body transformation in declarative nodes — they receive and returnpreSendIHttpRequestOptions - Use custom
functions for response transformation beyondpostReceive
/rootProperty
/filter
/limit
/set
/setKeyValue
/sort
— they receivebinaryData
and return(items, response)INodeExecutionData[] - All postReceive transforms support optional
(boolean/expression) andenabled
propertieserrorMessage - For file downloads in declarative nodes, set
andreturnFullResponse: true
on the request, then handle binary conversion inencoding: 'arraybuffer'postReceive - For file uploads in declarative nodes, use
to buildpreSend
fromFormDatathis.helpers.getBinaryDataBuffer() - Use
on request when you need custom error handling in postReceiveignoreHttpStatusErrors: true - Use
onpropertyInDotNotation: false
when property names contain literal dots (default isrouting.send
, which creates nested objects)true - For generic/token-based pagination, use
withtype: 'generic'
/$response.body
expressions$request - For cursor-based pagination, create a reusable factory function using
andIExecutePaginationFunctionsmakeRoutingRequest() - Use
operators in displayOptions for advanced conditions:_cnd
,{ _cnd: { gte: 2 } }
, etc.{ _cnd: { startsWith: 'https' } } - Use
,@version
,@tool
special keys in displayOptions for version/context-specific fields@feature - Use
for entity selection (provides list, URL, and ID modes) — requirestype: 'resourceLocator'
on the node classmethods.listSearch - Use
for dynamic field mapping (Create/Update) — requirestype: 'resourceMapper'
on the node classmethods.resourceMapping - Use
withtype: 'fixedCollection'
for repeatable structured parameter groups (filters, sort rules)multipleValues: true - Combine
anddisplayOptions.show
for excluding specific parameter valuesdisplayOptions.hide - Pass the linter before publishing — see
for the full error catalogreferences/common-mistakes.md
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) | + | Service supports API-based webhook registration | Stripe Trigger |
| Webhook (manual) | only | User pastes webhook URL into external service | Generic Webhook |
| Polling | | No webhook support; check for new data on a schedule | Gmail Trigger |
| Event/Stream | | Long-running connection (WebSocket, SSE, message queue) | AMQP Trigger |
Key differences from action nodes:
- Set
and suffix thegroup: ['trigger']
with "Trigger"displayName - Trigger nodes have
— they have NO inputsinputs: [] - Class names and filenames get the
suffix (e.g.,Trigger
)MyServiceTrigger - Use
to persist state (webhook IDs, last-checked timestamps) between callsgetWorkflowStaticData('node')
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).