Node Structure
Understanding the structure of an n8n node is essential for building robust integrations. This guide covers the INodeType interface and all its components in detail.
INodeType Interface
Every node implements the INodeType interface. Here’s the complete structure:
import type {
IExecuteFunctions ,
INodeExecutionData ,
INodeType ,
INodeTypeDescription ,
} from 'n8n-workflow' ;
export class YourNode implements INodeType {
description : INodeTypeDescription ;
// Optional methods object
methods = {
loadOptions: {},
listSearch: {},
credentialTest: {},
resourceMapping: {},
};
// For programmatic nodes
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [][]> {
// Your logic here
}
// For polling triggers
async poll ( this : IPollFunctions ) : Promise < INodeExecutionData [][] | null > {
// Polling logic
}
// For generic triggers
async trigger ( this : ITriggerFunctions ) : Promise < ITriggerResponse | undefined > {
// Trigger logic
}
// For webhook triggers
async webhook ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
// Webhook logic
}
// Webhook lifecycle methods
webhookMethods = {
default: {
async checkExists ( this : IHookFunctions ) : Promise < boolean > {},
async create ( this : IHookFunctions ) : Promise < boolean > {},
async delete ( this : IHookFunctions ) : Promise < boolean > {},
},
};
}
Node Description
The description property defines all node metadata and UI configuration:
Basic Description
Trigger Description
Declarative Node
export class MyNode implements INodeType {
description : INodeTypeDescription = {
displayName: 'My Node' ,
name: 'myNode' ,
icon: 'file:mynode.svg' ,
group: [ 'transform' ],
version: 1 ,
subtitle: '={{$parameter["operation"]}}' ,
description: 'Interact with My Service API' ,
defaults: {
name: 'My Node' ,
},
inputs: [ NodeConnectionTypes . Main ],
outputs: [ NodeConnectionTypes . Main ],
credentials: [
{
name: 'myServiceApi' ,
required: true ,
},
],
properties: [
// Parameters defined here
],
};
}
Description Properties
Property Type Required Description displayNamestring Yes Name shown in UI namestring Yes Internal node identifier (camelCase) iconstring/Icon Yes Node icon (file:icon.svg or fa:icon-name) groupstring[] Yes Node category (['input'], ['output'], ['transform'], ['trigger']) versionnumber/number[] Yes Node version(s) descriptionstring Yes Brief description for UI subtitlestring No Dynamic subtitle expression defaultsobject Yes Default node settings inputsstring[] Yes Input connection types outputsstring[] Yes Output connection types credentialsarray No Required credentials propertiesarray Yes Node parameters pollingboolean No Set true for polling triggers requestDefaultsobject No For declarative nodes usableAsToolboolean No Can be used as AI agent tool
Execute Function
For programmatic nodes, the execute() function contains your custom logic:
async execute ( this : IExecuteFunctions ): Promise < INodeExecutionData [][] > {
const items = this . getInputData ();
const returnData: INodeExecutionData [] = [];
// Get parameters
const operation = this . getNodeParameter ( 'operation' , 0 ) as string ;
const resource = this . getNodeParameter ( 'resource' , 0 ) as string ;
// Get credentials
const credentials = await this . getCredentials ( 'myServiceApi' );
// Process each item
for ( let i = 0 ; i < items . length ; i ++ ) {
try {
let responseData ;
if ( resource === 'user' ) {
if ( operation === 'create' ) {
const name = this . getNodeParameter ( 'name' , i ) as string ;
const email = this . getNodeParameter ( 'email' , i ) as string ;
responseData = await createUser . call ( this , credentials , {
name ,
email ,
});
}
}
returnData . push ({
json: responseData ,
pairedItems: { item: i },
});
} catch (error) {
if (this.continueOnFail()) {
returnData . push ({
json: { error: error . message },
pairedItems: { item: i },
});
continue ;
}
throw new NodeOperationError (this.getNode(), error. message , {
itemIndex : i ,
});
}
}
return [ returnData ];
}
Key Execute Context Methods
Poll Function (Polling Triggers)
Polling triggers implement the poll() function:
import type { IPollFunctions , INodeExecutionData } from 'n8n-workflow' ;
import { DateTime } from 'luxon' ;
async poll ( this : IPollFunctions ): Promise < INodeExecutionData [][] | null > {
// Get workflow static data for state persistence
const workflowStaticData = this . getWorkflowStaticData ( 'node' );
const now = Math . floor ( DateTime . now (). toSeconds ());
// Initialize on first run
if (this.getMode() !== 'manual') {
workflowStaticData . lastTimeChecked ??= now ;
}
const startDate = workflowStaticData . lastTimeChecked ?? now ;
// Get parameters
const filters = this . getNodeParameter ( 'filters' , {}) as IDataObject ;
// Fetch new items since last poll
const responseData = await fetchNewItems . call (
this ,
startDate ,
filters ,
);
// Return null if no new items
if (! responseData || !responseData.length) {
return null ;
}
// Update last checked time
workflowStaticData. lastTimeChecked = now ;
// Return items
return [ this .helpers. returnJsonArray (responseData)];
}
State Management: Use this.getWorkflowStaticData('node') to persist state between poll intervals. This ensures you only fetch new items.
Methods Object
The methods object provides dynamic functionality:
loadOptions
listSearch
credentialTest
resourceMapping
methods = {
loadOptions: {
// Load dynamic dropdown options
async getUsers ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions []> {
const credentials = await this . getCredentials ( 'myServiceApi' );
const users = await fetchUsers ( credentials );
return users . map ( user => ({
name: user . name ,
value: user . id ,
}));
},
},
};
Webhook Functions
Webhook triggers handle incoming HTTP requests:
webhook = async function ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
const req = this . getRequestObject ();
const body = req . body ;
// Process webhook payload
return {
workflowData: [
this . helpers . returnJsonArray ([ body ]),
],
};
};
webhookMethods = {
default: {
async checkExists ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' );
const webhookData = this . getWorkflowStaticData ( 'node' );
// Check if webhook exists in external service
return webhookData . webhookId !== undefined ;
},
async create ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' );
const credentials = await this . getCredentials ( 'myServiceApi' );
// Create webhook in external service
const webhook = await createWebhook ( credentials , webhookUrl );
// Store webhook ID for later deletion
const webhookData = this . getWorkflowStaticData ( 'node' );
webhookData . webhookId = webhook . id ;
return true ;
},
async delete ( this : IHookFunctions ) : Promise < boolean > {
const webhookData = this . getWorkflowStaticData ( 'node' );
const credentials = await this . getCredentials ( 'myServiceApi' );
// Delete webhook from external service
if ( webhookData . webhookId ) {
await deleteWebhook ( credentials , webhookData . webhookId as string );
delete webhookData . webhookId ;
}
return true ;
},
},
};
Real-World Example: Gmail Trigger
Here’s how the Gmail Trigger node implements polling:
export class GmailTrigger implements INodeType {
description : INodeTypeDescription = {
displayName: 'Gmail Trigger' ,
name: 'gmailTrigger' ,
icon: 'file:gmail.svg' ,
group: [ 'trigger' ],
version: [ 1 , 1.1 , 1.2 , 1.3 ],
polling: true ,
inputs: [],
outputs: [ NodeConnectionTypes . Main ],
credentials: [
{
name: 'gmailOAuth2' ,
required: true ,
},
],
properties: [
// Gmail-specific parameters
],
};
methods = {
loadOptions: {
async getLabels ( this : ILoadOptionsFunctions ) {
const labels = await googleApiRequestAllItems . call (
this ,
'labels' ,
'GET' ,
'/gmail/v1/users/me/labels' ,
);
return labels . map ( label => ({
name: label . name ,
value: label . id ,
}));
},
},
};
async poll ( this : IPollFunctions ) : Promise < INodeExecutionData [][] | null > {
const workflowStaticData = this . getWorkflowStaticData ( 'node' );
const now = Math . floor ( DateTime . now (). toSeconds ());
workflowStaticData . lastTimeChecked ??= now ;
const startDate = workflowStaticData . lastTimeChecked ;
// Fetch new messages
const messages = await fetchMessages . call ( this , startDate );
if ( ! messages . length ) {
return null ;
}
workflowStaticData . lastTimeChecked = now ;
return [ this . helpers . returnJsonArray ( messages )];
}
}
Error Handling Best Practices
Always handle errors appropriately in your nodes.
import { NodeOperationError , NodeApiError } from 'n8n-workflow' ;
try {
// Your operation
const response = await apiCall ();
} catch ( error ) {
// For user-facing errors
throw new NodeOperationError (
this . getNode (),
'Failed to create user' ,
{
itemIndex: i ,
description: error . message ,
},
);
// For API errors
throw new NodeApiError ( this . getNode (), error , {
itemIndex: i ,
});
}
Next Steps
Credentials Learn how to implement authentication
Testing Write comprehensive tests for your nodes