Skip to main content

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:
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

PropertyTypeRequiredDescription
displayNamestringYesName shown in UI
namestringYesInternal node identifier (camelCase)
iconstring/IconYesNode icon (file:icon.svg or fa:icon-name)
groupstring[]YesNode category (['input'], ['output'], ['transform'], ['trigger'])
versionnumber/number[]YesNode version(s)
descriptionstringYesBrief description for UI
subtitlestringNoDynamic subtitle expression
defaultsobjectYesDefault node settings
inputsstring[]YesInput connection types
outputsstring[]YesOutput connection types
credentialsarrayNoRequired credentials
propertiesarrayYesNode parameters
pollingbooleanNoSet true for polling triggers
requestDefaultsobjectNoFor declarative nodes
usableAsToolbooleanNoCan 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

// Get input data
const items = this.getInputData();

// Return output
return [returnData];

// Return multiple outputs
return [output1, output2];

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:
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