Node Types
This guide covers the core interfaces and patterns for implementing n8n nodes.
INodeType Interface
Every n8n node implements the INodeType interface, which defines the node’s structure and behavior.
description
INodeTypeDescription
required
Node metadata, UI configuration, and parameter definitions
Main execution function for programmatic nodesexecute?(this: IExecuteFunctions): Promise<NodeOutput>
Polling function for trigger nodes with polling: truepoll?(this: IPollFunctions): Promise<INodeExecutionData[][] | null>
Generic trigger function for event-based triggerstrigger?(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>
Webhook handler for webhook-based triggerswebhook?(this: IWebhookFunctions): Promise<IWebhookResponseData>
Optional methods for dynamic options, credential testing, and resource mapping
loadOptions: Dynamic parameter options
listSearch: Searchable list options
credentialTest: Credential validation
resourceMapping: Resource field mapping
actionHandler: Custom button actions
Webhook lifecycle methods (checkExists, create, delete)
INodeTypeDescription
The description object defines all node metadata and UI configuration.
export const versionDescription: INodeTypeDescription = {
displayName: 'Edit Fields (Set)',
name: 'set',
icon: 'fa:pen',
iconColor: 'blue',
group: ['input'],
version: [3, 3.1, 3.2, 3.3, 3.4],
description: 'Modify, add, or remove item fields',
subtitle: '={{$parameter["mode"]}}',
defaults: {
name: 'Edit Fields',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
// Parameter definitions...
],
};
Core Properties
Human-readable name shown in the UI
Internal identifier (kebab-case)
Node icon. Can be:
fa:icon-name - FontAwesome icon
file:icon.svg - Custom SVG file
node:package.nodeName - Icon from another node
Icon color from design system: gray, blue, green, red, orange, purple, etc.
Node categories: input, output, transform, trigger, schedule
version
number | number[]
required
Node version(s). Use array for light versioning: [1, 2, 3]
inputs
Array<NodeConnectionType | INodeInputConfiguration>
required
Input connections. Common types:
NodeConnectionTypes.Main - Standard data flow
NodeConnectionTypes.AiLanguageModel - AI model input
NodeConnectionTypes.AiMemory - AI memory input
outputs
Array<NodeConnectionType | INodeOutputConfiguration>
required
Output connections. Same types as inputs.
properties
INodeProperties[]
required
Node parameters definition (see Node Parameters guide)
credentials
INodeCredentialDescription[]
Required credentials for the node
Advanced Properties
Set to true for polling trigger nodes
Webhook configuration for webhook triggers
Default HTTP request options for declarative nodes
Maximum instances of this node allowed in a workflow
Lifecycle hooks: activate, deactivate
Feature flags with version-based conditions
Node Types
Programmatic Nodes
Nodes with custom execution logic implement the execute method.
packages/nodes-base/nodes/Set/v2/SetV2.node.ts
export class SetV2 implements INodeType {
description: INodeTypeDescription = versionDescription;
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const mode = this.getNodeParameter('mode', 0) as Mode;
if (mode === 'manual') {
// Process each item manually
for (let i = 0; i < items.length; i++) {
const options = this.getNodeParameter('options', i, {}) as SetNodeOptions;
const fields = this.getNodeParameter('fields', i, []) as SetField[];
const newItem = await manual.execute.call(
this,
items[i],
i,
options,
fields,
);
returnData.push(newItem);
}
}
return [returnData];
}
}
Declarative Nodes
Declarative nodes use requestDefaults and routing configuration instead of implementing execute.
{
displayName: 'HTTP Request',
name: 'httpRequest',
requestDefaults: {
baseURL: '={{$credentials.domain}}',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{ name: 'User', value: 'user' },
{ name: 'Project', value: 'project' },
],
default: 'user',
noDataExpression: true,
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: { resource: ['user'] },
},
options: [
{
name: 'Get',
value: 'get',
routing: {
request: {
method: 'GET',
url: '/users/={{$parameter.userId}}',
},
},
},
],
default: 'get',
},
],
}
Trigger Nodes
Webhook Triggers
Webhook Trigger Implementation
export class WebhookTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Webhook Trigger',
name: 'webhookTrigger',
group: ['trigger'],
version: 1,
description: 'Starts workflow on webhook event',
defaults: { name: 'Webhook Trigger' },
inputs: [],
outputs: [NodeConnectionTypes.Main],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [],
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
const headerData = this.getHeaderData();
return {
workflowData: [
[
{
json: {
body: bodyData,
headers: headerData,
},
},
],
],
};
}
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
// Check if webhook exists on external service
return true;
},
async create(this: IHookFunctions): Promise<boolean> {
// Create webhook on external service
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
// Delete webhook from external service
return true;
},
},
};
}
Polling Triggers
Polling Trigger Implementation
export class PollingTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Polling Trigger',
name: 'pollingTrigger',
group: ['trigger'],
version: 1,
description: 'Polls for new data',
polling: true,
defaults: { name: 'Polling Trigger' },
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Polling Interval',
name: 'pollInterval',
type: 'number',
default: 60,
description: 'How often to check for new data (seconds)',
},
],
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const staticData = this.getWorkflowStaticData('node');
let lastId = staticData.lastId as number | undefined;
// Fetch new items since lastId
const newItems = await this.helpers.httpRequest({
method: 'GET',
url: `https://api.example.com/items?since=${lastId || 0}`,
});
if (newItems.length === 0) {
return null;
}
// Update state
staticData.lastId = newItems[newItems.length - 1].id;
return [
newItems.map((item: any) => ({
json: item,
})),
];
}
}
Versioned Nodes
For major changes, use the VersionedNodeType class.
packages/nodes-base/nodes/Set/Set.node.ts
import { VersionedNodeType } from 'n8n-workflow';
import { SetV1 } from './v1/SetV1.node';
import { SetV2 } from './v2/SetV2.node';
export class Set extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Set',
name: 'set',
icon: 'fa:pen',
group: ['input'],
description: 'Add or edit fields on an input item',
defaultVersion: 3.4,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new SetV1(baseDescription),
2: new SetV1(baseDescription),
3: new SetV2(baseDescription),
3.1: new SetV2(baseDescription),
3.2: new SetV2(baseDescription),
3.3: new SetV2(baseDescription),
3.4: new SetV2(baseDescription),
};
super(nodeVersions, baseDescription);
}
}
Dynamic Methods
Nodes can implement dynamic behavior through the methods object.
Load Options
Provide dynamic dropdown options:
methods = {
loadOptions: {
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const credentials = await this.getCredentials('myApi');
const users = await this.helpers.httpRequest({
method: 'GET',
url: `${credentials.baseUrl}/users`,
});
return users.map((user: any) => ({
name: user.name,
value: user.id,
}));
},
},
};
List Search
Searchable resource lists:
methods = {
listSearch: {
async searchUsers(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const credentials = await this.getCredentials('myApi');
const response = await this.helpers.httpRequest({
method: 'GET',
url: `${credentials.baseUrl}/users/search`,
qs: { q: filter },
});
return {
results: response.users.map((user: any) => ({
name: user.name,
value: user.id,
url: user.profileUrl,
})),
};
},
},
};
Credential Test
Validate credentials:
methods = {
credentialTest: {
async testApiCredentials(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as IDataObject;
try {
await this.helpers.request({
method: 'GET',
url: `${credentials.baseUrl}/auth/verify`,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
},
});
return {
status: 'OK',
message: 'Authentication successful',
};
} catch (error) {
return {
status: 'Error',
message: 'Invalid credentials',
};
}
},
},
};
Best Practices
Structure
Type Safety
Error Handling
Performance
- Use clear, descriptive
displayName values
- Set appropriate
group for node categorization
- Provide meaningful
description and subtitle
- Use semantic versioning for
version
- Never use
any type - use proper interfaces or unknown
- Avoid type casting with
as except in tests
- Define interfaces for all API responses
- Use type guards for runtime validation
- Use
NodeOperationError for user-facing errors
- Use
NodeApiError for API-related errors
- Provide helpful error messages
- Support
continueOnFail option when appropriate
- Use
getWorkflowStaticData() for persistent state
- Implement pagination for large datasets
- Cache expensive operations when possible
- Use streaming for large file operations
See Also