n8n Node Builder
Build production-ready custom nodes for n8n using the official n8n-node CLI tool and n8n's best practices — and prove they work with the validation gate protocol.
When You Need More Detail
This skill uses progressive disclosure. The SKILL.md covers the full workflow and decision-making. For complete code templates and protocols, read these reference files:
references/declarative-node.md— Full declarative node template with routing, credentials, codex, and advanced patterns (preSend/postReceive, pagination, resourceLocator/resourceMapper, customOperations, hints)references/programmatic-node.md— Full programmatic node template with execute method, error handling, item linking, trigger patterns, and versioningreferences/credentials.md— All credential/auth patterns (API key, Bearer, OAuth2, Basic, Custom, testedBy, preAuthentication)references/validation.md— The agent self-validation gate protocol: lint rule catalog, build, runtime testing, cloud-support, release prechecks, verification scanreferences/ai-nodes.md— AI sub-nodes: chat model and memory nodes via supplyData() and @n8n/ai-node-sdkreferences/publishing.md— Releasing, GitHub Actions publishing with provenance, and the verification checklistreferences/common-mistakes.md— Error catalog of 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 vs AI sub-node)
- Scaffold the project with the
n8n-nodeCLI - Implement the node base file, credentials file, codex file, and icon
- Validate with the gate protocol (lint → build → cloud-support → runtime)
- Release via
n8n-node releaseand GitHub Actions, then submit for verification
Step 1: Choose Your Node Style
n8n has two main node-building styles plus the AI sub-node paradigm. 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 go far beyond simple routing: preSend/postReceive transform functions, three pagination modes, declarative dynamic dropdowns, resourceLocator/resourceMapper/fixedCollection parameters, advanced displayOptions conditions, and binary file handling are all available. See references/declarative-node.md → "Advanced Declarative Patterns" for the 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 the globalFormData,postReceivewith binary handling) - Even if you need dynamic field mapping (use
resourceMapper) - Even if you need custom pagination logic (use custom pagination functions)
- Even if one or two operations need full programmatic control — use
customOperationsto implement just those resource/operation pairs as programmatic functions while routing handles the rest (a node must not define bothexecute()andcustomOperations)
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
- Full versioning (separate implementations per major version via
VersionedNodeType) — declarative nodes support only light versioning, so choose programmatic if you anticipate breaking-change versions
AI Sub-Nodes (chat models, memory)
If the goal is to plug a model or memory provider INTO the AI Agent (rather than exposing API operations the agent can call as tools), build an AI sub-node: supplyData() instead of execute(), AI connection types instead of Main, and the @n8n/ai-node-sdk. Read references/ai-nodes.md. For "let the agent use my node as a tool", a regular node with usableAsTool: true is all you need.
Quick Decision
Ask: "Is this a REST API with no triggers and no multi-call chaining?" If yes → declarative (even for complex transformation, pagination, file handling, and field mapping — and customOperations covers isolated exceptions). Building a model/memory provider for AI Agents → AI sub-node. 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>
Option B: Install globally
npm install --global @n8n/node-cli
n8n-node new 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 interactively for base URL and auth type)programmatic/example— Programmatic with full flexibilityprogrammatic/openai-chat-model— AI sub-node: chat model for OpenAI-compatible APIsprogrammatic/custom-chat-model— AI sub-node: chat model for custom protocols (skeleton)programmatic/custom-chat-model-example— AI sub-node: custom chat model with worked exampleprogrammatic/custom-chat-memory— AI sub-node: conversation memory
Useful flags for non-interactive (agent) runs: --skip-install (skip dependency installation) and --force (overwrite an existing directory). Note that declarative/custom still prompts for base URL and auth type — prefer declarative/github-issues or programmatic/example when running fully unattended, or answer the prompts.
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
├── eslint.config.mjs # Don't edit — two-line flat config importing from '@n8n/node-cli/eslint';
│ # with "n8n": { "strict": true }, lint fails if this file is modified
├── .github/workflows/ # ci.yml (lint + build on push) and publish.yml (npm publish with provenance)
├── 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 (required by lint)
├── credentials/
│ └── <NodeName>Api.credentials.ts # Credential file
└── dist/ # Built output (generated by npm run build = n8n-node build)
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) — lint enforces this - Use
NodeConnectionTypes.Mainfor inputs/outputs — the plural const object, imported as a value fromn8n-workflow(import { NodeConnectionTypes } from 'n8n-workflow'). The singularNodeConnectionTypeis type-only and cannot be used as a value. Never use the string'main'— it fails thenode-connection-type-literallint rule - 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 oras Typecasts, useimport type; if it's used as a value likethrow new NodeApiError(...)orNodeConnectionTypes.Main, use a regular import) - Dynamic expressions in routing must start with
=prefix:'=/contacts/{{$parameter["id"]}}' - Use routing OR
execute(), never both — if a node definesexecute(), n8n calls it and silently ignores the declarative routing/requestDefaultsconfig (the routing engine is only used when the node has noexecute/supplyData/poll/triggermethod) - 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 or object | 'file:<name>.svg' or { light: 'file:a.svg', dark: 'file:a.dark.svg' } |
iconColor |
string | Preset theme color, e.g. 'orange-red' (use instead of deprecated defaults.color) |
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"]}}' — required by lint |
description |
string | Short description for the node panel |
defaults |
object | { name: 'Display Name' } |
inputs |
array | [NodeConnectionTypes.Main] |
outputs |
array | [NodeConnectionTypes.Main] |
usableAsTool |
boolean | Must be declared (lint error otherwise); set true unless there's a reason not to |
credentials |
array | [{ name: 'credName', required: true }] |
properties |
array | Resource, operation, and field definitions |
For declarative nodes, also add requestDefaults (baseURL, headers — supports dynamic expressions from credentials) and routing keys on operations and parameters. The full pattern language — preSend/postReceive functions, pagination, dynamic property paths with $parent/$index, declarative dropdowns, resourceLocator/resourceMapper/fixedCollection, and the methods object for listSearch/loadOptions/resourceMapping — is in references/declarative-node.md.
For programmatic nodes, also add an async execute() method with proper item looping via this.getInputData() and pairedItem linking — see 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] } }. Advanced _cnd condition operators (gte, startsWith, regex, ...) and the @tool/@feature special keys are documented in references/declarative-node.md.
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>",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Miscellaneous"],
"subcategories": { "Miscellaneous": ["Helpers"] },
"alias": ["synonym", "former-product-name"],
"resources": {
"credentialDocumentation": [{ "url": "" }],
"primaryDocumentation": [{ "url": "" }]
}
}
Set the node field to the npm package name (e.g., "n8n-nodes-acme"), as the official CLI templates do. At runtime n8n only consumes categories, subcategories, resources, and alias — alias strings are matched by the nodes-panel search, so add common synonyms and former product names.
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— and lint enforces naming:namestarts lowercase and ends inApi, the class name ends inApi(OAuth2:OAuth2Api)- Use
authenticate: IAuthenticateGenericfor header/body/query auth - Use
test: ICredentialTestRequestto validate credentials — it runs automatically when the user saves the credential. Customize pass/fail withtest.rules; usetestedByin the node for complex validation. OAuth2 credentials extendingoAuth2Apiare exempt (don't addtestto them) - For session-token APIs, use
preAuthenticationto exchange long-lived credentials for a token before requests - Always use
$credentials(plural) in expressions —$credential(singular) is wrong - The linter requires an
iconproperty usingIcontype from n8n-workflow
3G: The Icon
Use a square SVG — the icon-validation lint rule errors on non-.svg icons (the docs' 60×60 PNG alternative fails lint in CLI-scaffolded projects and the verification scan). Place it alongside the .node.ts file and reference it with icon: 'file:<name>.svg'. For light/dark variants use icon: { light: 'file:icon.svg', dark: 'file:icon.dark.svg' } — the two must be different files. Set a brand-ish accent with the top-level iconColor: '<preset>' (e.g. 'orange-red', one of n8n's preset theme colors); defaults.color is deprecated. Don't reference Font Awesome — download and embed.
3H: Error Handling (Programmatic Nodes)
Use NodeApiError for API errors and NodeOperationError for validation errors (both from n8n-workflow). Inside per-item loops, always pass the item index: throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: i }) — the node-operation-error-itemindex lint rule requires it. 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.
3I: Item Linking (pairedItem / constructExecutionMetaData)
Every output item in a programmatic node must link back to its source input — the missing-paired-item lint rule errors otherwise. 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 },
});
For multi-input nodes add the input index: pairedItem: { item: i, input: 0 }. Without item linking, n8n can't trace data flow between nodes.
3J: 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 (the no-deprecated-workflow-functions lint rule flags them). 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. Use loadOptionsMethod for dropdowns that fetch values from an API at runtime. See references/programmatic-node.md for the complete patterns.
3K: 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 VersionedNodeType — imported from n8n-workflow — with separate v1/, v2/ directories and a base description carrying defaultVersion. See the Mattermost node on GitHub for a real example and references/programmatic-node.md for the template.
Feature-based versioning: gate individual behaviors behind feature flags instead of whole versions — see references/programmatic-node.md.
3L: Hints and Notices
Four mechanisms guide users inside the editor: description-level hints (banners in input/output panes or the NDV, with displayCondition expressions), parameter-level hint text under a field, type: 'notice' inline info boxes between parameters, and dynamic post-execution hints via this.addExecutionHints({ message, location }) inside execute() (programmatic only). See references/declarative-node.md → "Hints and Notices" and references/programmatic-node.md.
Step 4: Validate (Gate Protocol)
After implementing, prove the node works. Run the gates in order; don't proceed past a failure. Full protocol with pass criteria and failure triage: references/validation.md.
npm run lint:fix && npm run lint # Gate 1 — strict-config guard + @n8n/eslint-plugin-community-nodes; exit 0, zero errors
npm run build # Gate 2 — strict tsc + asset copy; '✓ Build successful' + dist paths exist
npx n8n-node cloud-support # Gate 3 — 'Cloud support is ENABLED' (strict mode + default eslint config)
npm run dev # Gate 4 — local n8n at http://localhost:5678; smoke test node + credentials + operations
| Gate | Pass signal |
|---|---|
| 0. Structure sanity | package.json checklist (name, keywords, n8n block, peerDeps) all true |
| 1. Lint | Exit 0, zero errors — never edit eslint.config.mjs, never use inline eslint-disable (the verification scanner ignores them) |
| 2. Build | ✓ Build successful; every n8n.nodes/n8n.credentials path exists in dist/ |
| 3. Cloud support | ✅ Cloud support is ENABLED |
| 4. Runtime | Editor is now accessible; node findable by displayName; credential test passes on save; operations execute |
There is no unit-test command in the n8n-node toolchain — CI runs exactly lint + build, and runtime validation is Gate 4. Don't invent a jest/vitest harness.
Step 5: Release and Verify
npm run release # Gate 5 — release-it: lint+build gates, version bump, changelog, tag, push, GitHub release
npx @n8n/scan-community-package <package-name> # Gate 6 — post-publish: provenance check + re-lint of the published artifact
npm run release does NOT publish to npm from your machine — pushing the version tag triggers the scaffolded .github/workflows/publish.yml, which publishes with npm provenance (mandatory for verified community nodes since May 1, 2026). Plain npm publish is blocked by design via prepublishOnly: n8n-node prerelease — don't remove it. One-time setup: npx n8n-node release --init-workflow if publish.yml is missing, plus npm Trusted Publishing (or an NPM_TOKEN secret).
Read references/publishing.md for the full release flow, verification scope rules (one service per package, no logic nodes, npm↔GitHub identity), and the Creator Portal submission checklist.
Code Standards Summary
- Write in TypeScript; use
import typefor type-only imports (a convention from n8n's codebase — keeps value vs type imports honest) - Use
httpRequestWithAuthentication(not the deprecatedrequestWithAuthentication); useurlnoturiinIHttpRequestOptions - Never mutate incoming data — clone with spread or
structuredClone() - No runtime dependencies. For cloud-eligible nodes the lint allowlist is exactly:
n8n-workflow,@n8n/ai-node-sdk,lodash,moment,p-limit,luxon,zod,crypto/node:crypto, plus relative imports — nothing else (no axios, no form-data; use built-in helpers and globals) - No restricted globals in cloud-eligible nodes: use
sleepfromn8n-workflowinstead ofsetTimeout; noprocess/__dirname; noconsole.log(lint error) n8n-workflowis a peer dependency, not bundled- Follow Resource → Operation pattern with
noDataExpression: trueon selectors; includeactionon every operation option - Name list operations "Get Many" (not "Get All") and use the
returnAll/limitpair - Use
NodeConnectionTypes.Main(plural const, value import) — never the string'main' - Declare
usableAsToolon every node (lint error otherwise); settrueunless there's a reason not to - Use
constructExecutionMetaDatawithitemDatafor item linking; every output item needspairedItem - Implement
continueOnFail()in every execute loop; throwNodeApiError/NodeOperationErrorwith{ itemIndex }in loops — never rawError - The
execute()method returns[returnData]— don't forget the outer array wrapper - Create
GenericFunctions.tsfor shared API request helpers (typemethod: IHttpRequestMethods; includeIHookFunctions/IWebhookFunctions/IPollFunctionsin thethisunion) - Use
displayOptionsfor progressive field disclosure; optional params go in "Additional Fields" collections - Title Case for UI text; sentence case for descriptions/hints; placeholders start with
e.g. - Use
$credentials(plural) in credential expressions; dynamic routing expressions need the=prefix - Use
encodeURIComponent()for user-provided values in routing URLs - Declarative: use routing OR
execute(), never both (definingexecute()silently disables routing);customOperationsis the per-operation escape hatch - Split multi-resource nodes into
*Description.tsfiles per resource, spread into the properties array - Reuse internal parameter
valuenames across operations - Keep
"strict": truein then8nconfig ofpackage.jsonand never editeslint.config.mjs - Pass the validation gates before publishing — see
references/validation.mdandreferences/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. methods.listSearch is required when a list mode uses searchListMethod. 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.
Copy rules (the verification bar checks these):
- Every
placeholderstarts withe.g.—placeholder: 'e.g. nathan@example.com' - Error messages state what happened AND how to fix it, quote the parameter's display name, and append
[Item X]with the item index; never use the words "error", "problem", "failure", or "mistake" in messages or descriptions - Order required fields by importance, then scope broad→narrow; sort optional fields alphabetically; document defaults in descriptions ("Defaults to false")
- Trigger nodes name the event parameter "Trigger on" (no tooltip)
- ID fields backed by a dynamic list are named
<Record> Name or IDwith descriptionChoose a name from the list, or specify an ID using an expression - Sort dropdown options alphabetically (also a lint warning); offer "All" instead of
* - Operation
actionphrasing omits articles and names the resource:action: 'Update row in sheet'
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 - Lint still requires
icon,subtitle, and a declaredusableAsTool(setfalsefor triggers) on the description - Webhook triggers must implement the complete
webhookMethods.defaultlifecycle (checkExists/create/delete) — thewebhook-lifecycle-completelint rule errors otherwise, including on manual-URL webhook nodes
For complete trigger templates with full code examples, read references/programmatic-node.md → "Trigger Node Patterns".
AI Sub-Nodes
To make a model or memory provider pluggable into the AI Agent: implement supplyData(this: ISupplyDataFunctions, itemIndex) instead of execute(), output an AI connection type (outputs: [NodeConnectionTypes.AiLanguageModel] or AiMemory), and declare "aiNodeSdkVersion": 1 plus the @n8n/ai-node-sdk peer dependency in package.json (the ai-node-package-json lint rule enforces the pairing). Four CLI templates scaffold these. Read references/ai-nodes.md.
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).