
Building n8n Custom Nodes: The Guide I Wish I Had
Declarative vs programmatic nodes, credentials, pagination, binary data, and the packaging steps that turn a folder of TypeScript into an installable community node.
Custom nodes are how n8n stops being a limited toy and becomes your team's automation runtime. Start declarative, drop to programmatic when you need pagination or binary data, package as a community node, and version it like any other library.
- Declarative nodes cover most REST APIs with no TypeScript logic at all.
- Programmatic nodes are required for pagination loops, binary data, and non-trivial auth flows.
- Credentials are a separate class — test them with the built-in credentialTest field.
- Publish as an npm package prefixed with n8n-nodes- and install via the community-nodes UI.
- Semver your nodes; breaking changes bump major and require a migration note in the README.
Declarative vs programmatic — pick correctly
n8n supports two node styles. Declarative nodes are JSON-ish descriptions of an HTTP request; n8n handles the fetch, auth, and error mapping. Programmatic nodes give you an execute() function where you write TypeScript directly.
Rule of thumb: if your integration is one HTTP call per operation and returns JSON, go declarative — you'll finish in an hour and the node will keep working when n8n upgrades. If you need pagination loops, streaming, binary data, or conditional branching, go programmatic.
The minimum declarative node
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
export class Acme implements INodeType {
description: INodeTypeDescription = {
displayName: 'Acme',
name: 'acme',
icon: 'file:acme.svg',
group: ['transform'],
version: 1,
description: 'Interact with the Acme API',
defaults: { name: 'Acme' },
inputs: ['main'],
outputs: ['main'],
credentials: [{ name: 'acmeApi', required: true }],
requestDefaults: {
baseURL: 'https://api.acme.io/v1',
headers: { 'Content-Type': 'application/json' },
},
properties: [
{ displayName: 'Resource', name: 'resource', type: 'options',
options: [{ name: 'Customer', value: 'customer' }], default: 'customer', noDataExpression: true },
{ displayName: 'Operation', name: 'operation', type: 'options',
displayOptions: { show: { resource: ['customer'] } },
options: [{ name: 'Get', value: 'get', action: 'Get a customer',
routing: { request: { method: 'GET', url: '=/customers/{{$parameter.id}}' } } }],
default: 'get', noDataExpression: true },
{ displayName: 'Customer ID', name: 'id', type: 'string', default: '',
displayOptions: { show: { resource: ['customer'], operation: ['get'] } } },
],
};
}That's a working node. No execute(), no manual fetch, no error handling — n8n does all of it from the routing declaration.
Credentials as first-class objects
import { ICredentialType, INodeProperties } from 'n8n-workflow';
export class AcmeApi implements ICredentialType {
name = 'acmeApi';
displayName = 'Acme API';
properties: INodeProperties[] = [
{ displayName: 'API Key', name: 'apiKey', type: 'string', typeOptions: { password: true }, default: '' },
];
authenticate = {
type: 'generic' as const,
properties: { headers: { Authorization: '=Bearer {{$credentials.apiKey}}' } },
};
test = {
request: { baseURL: 'https://api.acme.io/v1', url: '/whoami' },
};
}The test block powers the 'Test connection' button in the UI. Ship it — users will thank you the first time they mistype a key.
Pagination — the pattern for programmatic nodes
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const out: INodeExecutionData[] = [];
let cursor: string | undefined;
do {
const res = await this.helpers.httpRequestWithAuthentication.call(this, 'acmeApi', {
method: 'GET',
url: 'https://api.acme.io/v1/orders',
qs: { cursor, limit: 200 },
json: true,
});
for (const item of res.data) out.push({ json: item });
cursor = res.next_cursor;
} while (cursor);
return [out];
}Always cap the loop (e.g. while (cursor && out.length < 50_000)) and expose a limit parameter. Runaway pagination is the most common way custom nodes take down an n8n instance.
Binary data without exploding memory
If your node fetches or produces files, use n8n's binary helpers instead of loading everything into a JS Buffer:
const stream = await this.helpers.httpRequest({ method: 'GET', url, encoding: 'stream' });
const binary = await this.helpers.prepareBinaryData(stream, fileName, mimeType);
return [[{ json: {}, binary: { data: binary } }]];This streams to n8n's configured binary storage (filesystem or S3) instead of the workflow's JSON payload. On queue-mode installs, this is the difference between a working node and OOM-killed workers.
Packaging and publishing
- Package name must start with
n8n-nodes-(e.g.n8n-nodes-acme). - Set
n8nfield in package.json pointing todist/nodesanddist/credentials. - Add keyword
n8n-community-node-package— this is what the installer searches for. - Run
npm run build(tsc + copy icons), thennpm publish --access public. - In n8n Settings → Community Nodes → Install, paste the package name.
Version aggressively. If you change a parameter name or an operation's response shape, bump major and document the migration. n8n users cannot 'roll back' a workflow the way developers roll back code.
Frequently asked
Can I keep custom nodes private?
Yes — publish to a private npm registry (Verdaccio, GitHub Packages) and configure the n8n instance to install from it. Community Nodes UI supports scoped and private packages.
Do I need to write tests?
For declarative nodes, spot-check in the UI is usually enough. For programmatic nodes with pagination or state, add Jest tests that mock the HTTP layer — they catch regressions on n8n version bumps.
Can custom nodes call other nodes?
Not directly. Compose in the workflow instead. If you need reusability across workflows, build a sub-workflow and call it from a Workflow node.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
Self-Hosting n8n on Kubernetes: HA, Queue Mode, Autoscaling, and Backups That Actually Restore
The complete production playbook for n8n on Kubernetes — queue mode topology, KEDA autoscaling on Redis depth, Postgres HA and PITR, encryption-key rotation, zero-downtime upgrades, and the backup drill you must actually run.
Workflow Automation Blueprints: 12 Patterns Every Team Ships (and the Envelope That Ties Them Together)
The 12 automation blueprints I reuse across every client — lead routing, invoice processing, content ops, incident response — with the exact triggers, guards, envelopes, DLQs, and human-in-the-loop patterns that make them survive production.
The n8n Error Handling Playbook: Retries, DLQs, Circuit Breakers, and Replay
The full production playbook for n8n reliability: how to classify failures, design retry policies that don't melt upstreams, build a Postgres DLQ with safe replay, run circuit breakers per integration, and wire the observability that catches incidents before your customers do.
Browse every article by Islam Gamal on the author page.