← All articles
Engineering

Create Custom n8n Nodes

Jan 5, 2026 · 8 min read
Create Custom n8n Nodes

If you've been working with n8n and found yourself needing functionality that doesn't exist in the standard node library, creating custom nodes is the solution. In this tutorial, we'll walk through building a custom XSLT processor node from scratch using the n8n CLI tool. This hands-on guide will take you from initial setup through deployment on a self-hosted n8n instance.

What We're Building

Our custom node will be an XSLT processor that takes two inputs:

  • An XML string
  • An XSLT stylesheet template

The node will transform the XML according to the stylesheet and output the result. This is a practical example that demonstrates all the key concepts needed to build any custom n8n node.

Prerequisites

Before we begin, make sure you have:

  • Node.js and npm installed
  • Basic familiarity with TypeScript
  • A text editor (we'll use Visual Studio Code)
  • Optional: Docker for deployment to a containerized n8n instance

Step 1: Install the n8n CLI

The first step is to install the n8n node CLI tool globally. This tool provides utilities for scaffolding and developing custom nodes.

npm install -g n8n-node-cli

Step 2: Understanding the XSLT Processor Library

For our XSLT transformation functionality, we'll be using a TypeScript XSLT processor from GitHub (specifically from the user @DesignLiquido). This library will handle the actual XML transformation logic within our custom node.

Step 3: Create a New Project

Create an empty folder for your project and open it in Visual Studio Code. Navigate to your desired location and create the project directory:

mkdir n8n-xslt-node
cd n8n-xslt-node
code .

Step 4: Initialize the Custom Node

Open a terminal in VS Code and run the n8n node creation command. Important: your package name must start with n8n-nodes- for n8n to recognize it as a custom node package.

n8n-node-cli new

You'll be prompted for:

  • Package name: Enter n8n-nodes-xslt
  • Node type: Select "other" (since we're not building a standard HTTP API node)

The CLI will initialize a Git repository for you, scaffold the project structure, install necessary dependencies, and prompt you to add node_modules to .gitignore (select yes).

Step 5: Understanding the Project Structure

Once scaffolding completes, examine the generated files.

package.json contains your project metadata and scripts:

{
  "name": "n8n-nodes-xslt",
  "scripts": {
    "build": "...",
    "watch": "...",
    "dev": "...",
    "lint": "...",
    "lint:fix": "...",
    "release": "..."
  },
  "devDependencies": {
    "eslint": "...",
    "n8n-node-cli": "..."
  },
  "dependencies": {
    "n8n-workflow": "..."
  }
}

Key scripts you'll use:

  • npm run dev — runs a local n8n instance with your node for testing
  • npm run build — compiles your TypeScript code
  • npm run watch — watches for changes and rebuilds automatically

The /nodes directory is where your custom node code lives. You'll find:

  • icon.svg / icon-dark.svg — the icon displayed in the n8n interface
  • ExampleNode.node.json — node metadata and configuration
  • ExampleNode.node.ts — the TypeScript implementation

Step 6: Implement the XSLT Processor

Now let's customize the scaffolded code to create our XSLT processor.

Install the XSLT processing library:

npm install xslt-processor

Open the ExampleNode.node.ts file (you'll rename this later). Your node class must implement the INode interface:

export class XsltProcessor implements INode {
  description: INodeType;

  constructor() {
    this.description = {
      displayName: 'XSLT Processor',
      name: 'xsltProcessor',
      icon: 'file:icon.svg',
      group: ['transform'],
      version: 1,
      description: 'Transform XML using XSLT stylesheets',
      defaults: {
        name: 'XSLT Processor',
      },
      inputs: ['main'],
      outputs: ['main'],
      properties: [
        // Node properties defined here
      ],
    };
  }
}

Properties define the input fields users will see in the n8n interface:

properties: [
  {
    displayName: 'XML Input',
    name: 'xmlInput',
    type: 'string',
    default: '',
    required: true,
    description: 'The XML string to transform',
  },
  {
    displayName: 'XSLT Stylesheet',
    name: 'xsltStylesheet',
    type: 'string',
    default: '',
    required: true,
    description: 'The XSLT stylesheet for transformation',
  },
]

These properties can be hardcoded values entered directly in the node, expressions that reference outputs from previous nodes, or a combination of both.

The execute method is where your node's logic runs. It's an async function that processes items:

async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
  const items = this.getInputData();
  const returnData: INodeExecutionData[] = [];

  // n8n processes items individually
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
    try {
      // Get the input values for this item
      const xmlInput = this.getNodeParameter('xmlInput', itemIndex, '') as string;
      const xsltStylesheet = this.getNodeParameter('xsltStylesheet', itemIndex, '') as string;

      // Perform the XSLT transformation
      const xsltProcessor = new XsltProcessor();
      const xml = xsltProcessor.xmlParse(xmlInput);
      const xslt = xsltProcessor.xmlParse(xsltStylesheet);
      const transformedDoc = xsltProcessor.xsltProcess(xml, xslt);
      const result = xsltProcessor.xmlSerialize(transformedDoc);

      // Return the transformed result
      returnData.push({
        json: { transformedXml: result },
      });

    } catch (error) {
      // Error handling for this item
      if (this.continueOnFail()) {
        returnData.push({
          json: { error: error.message },
        });
        continue;
      }
      throw error;
    }
  }

  return [returnData];
}

Key concepts: n8n processes arrays by running your code for each item individually, use this.getNodeParameter() to retrieve user inputs, the try/catch structure allows for proper error handling, and this.continueOnFail() lets workflows continue even if one item fails.

Step 7: Rename and Configure Your Node

Rename your files to match your node name:

  • Rename ExampleNode.node.ts to XsltProcessor.node.ts
  • Rename ExampleNode.node.json to XsltProcessor.node.json
  • Update the class name inside the TypeScript file

Update the JSON configuration file:

{
  "name": "xsltProcessor",
  "displayName": "XSLT Processor",
  "description": "Transform XML documents using XSLT stylesheets",
  "icon": "file:icon.svg"
}

Step 8: Customize the Icon (Optional)

Replace the default icon.svg and icon-dark.svg files with your own icons. Keep the filenames as icon.svg and icon-dark.svg, use SVG format, and keep icons simple and recognizable at small sizes. Update the icon reference in your node description accordingly.

Step 9: Test Your Node in Development Mode

Run the development server:

npm run dev

This compiles your TypeScript code, starts a local n8n instance, and makes your custom node available for testing. The local instance typically runs on http://localhost:5678.

To create a test workflow: create a new workflow, add a manual trigger node, search for your "XSLT Processor" node, and configure it with test XML and XSLT.

Example XML:

<catalog>
  <book>
    <title>XML Developer's Guide</title>
    <author>John Doe</author>
    <price>44.95</price>
  </book>
</catalog>

Example XSLT:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <html>
      <body>
        <h2>Book Catalog</h2>
        <table border="1">
          <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Price</th>
          </tr>
          <xsl:for-each select="catalog/book">
            <tr>
              <td><xsl:value-of select="title"/></td>
              <td><xsl:value-of select="author"/></td>
              <td><xsl:value-of select="price"/></td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Execute the workflow and verify the output.

Step 10: Deploy to Your Docker Instance

Once you're satisfied with your node, you'll want to deploy it to your production n8n instance. This guide assumes you're running n8n in Docker.

Important: before deploying, shut down the npm run dev process! The development server occupies port 5678, which will conflict with your Docker instance.

n8n looks for custom nodes in a specific directory structure. For Docker deployments, navigate to your n8n home directory (typically /home/n8n in the container), create a custom directory if it doesn't exist, and place your custom nodes inside it:

/home/n8n/
  └── custom/
      └── nodes/
          └── XsltProcessor/
              ├── icon.svg
              ├── icon-dark.svg
              ├── XsltProcessor.node.js (compiled)
              └── XsltProcessor.node.json

Deployment Process

Step 1: Build your node. From your project root:

npm run build

This compiles your TypeScript to JavaScript in the dist directory.

Step 2: Access your Docker container. Using Docker Desktop's Files GUI, navigate to /home/n8n/ and create the custom directory if needed. Or with the CLI:

docker exec -it <container-name> /bin/sh
cd /home/n8n
mkdir -p custom

Step 3: Copy the nodes directory. Using Docker Desktop, navigate to /home/n8n/custom, click "Import", and select your local dist/nodes folder. Or with the CLI:

docker cp dist/nodes <container-name>:/home/n8n/custom/

Step 4: Configure a Docker volume (recommended) to persist your custom nodes across container restarts:

version: '3.8'
services:
  n8n:
    image: n8nio/n8n
    volumes:
      - n8n_data:/home/n8n
      - ./custom:/home/n8n/custom
    ports:
      - "5678:5678"
    environment:
      - N8N_CUSTOM_EXTENSIONS=/home/n8n/custom

volumes:
  n8n_data:

Step 5: Restart n8n.

docker restart <container-name>

Or using Docker Compose: docker-compose restart.

Step 6: Verify the deployment. Access your n8n instance (typically http://localhost:5678), create a new workflow, add a manual trigger, search for "XSLT" or your node name — you should see your custom XSLT Processor node with your custom icon. Test it with the same XML and XSLT from development.

Troubleshooting Common Issues

Node not appearing in n8n. Verify your package name starts with n8n-nodes-, check that files are in the correct directory structure, ensure the .node.json file is present alongside the .node.js file, and restart the n8n instance after copying files.

Permission issues in Docker. If you encounter permission errors:

docker exec -it <container-name> /bin/sh
chown -R node:node /home/n8n/custom

Icons not displaying. Verify icon files are named icon.svg and icon-dark.svg, check that the icon reference in your code matches the filenames, and ensure icons are in the same directory as your node files.

Workflow execution errors. Check the n8n logs for detailed error messages, verify all npm dependencies are listed in your package.json, and test with simple inputs first, then increase complexity.

Key Takeaways

The n8n CLI is your friend. The n8n-node-cli tool handles all the boilerplate — use n8n-node-cli new to start a new project, always include n8n-nodes- in your package name, and the scaffolded template provides a solid foundation.

Understanding the structure. Focus on properties (the UI fields users interact with), the execute method (your core processing logic), and item-based processing (n8n loops through arrays automatically).

Development workflow. Use npm run dev for rapid testing, test thoroughly before deploying, and keep your development and production environments separate.

Deployment considerations. File organization matters — follow the /custom/nodes structure, Docker volumes ensure persistence, and always restart n8n after adding or updating custom nodes.

Advanced patterns. For more complex nodes, study the GitHub node samples for patterns, organize code into separate files and classes, follow TypeScript best practices, and consider error handling and edge cases.

Going Further

The XSLT processor we built demonstrates the fundamentals, but custom nodes can be much more sophisticated: external API integrations to connect to any web service, database operations for direct queries and updates, file processing to handle PDFs, images, and documents, and complex multi-step workflows with branching logic.

The patterns you've learned here apply to all custom node development. The main difference in more complex nodes is the organization — you might have multiple TypeScript files, helper classes, and more elaborate property configurations.

Conclusion

You now have the knowledge to create custom n8n nodes from scratch. Whether you're building specialized data processors, integrating proprietary systems, or extending n8n's capabilities in unique ways, the process remains the same: scaffold with the CLI, implement your logic in the execute method, test in development mode, and deploy to your production instance.

Custom nodes transform n8n from a powerful workflow tool into a fully extensible automation platform tailored to your specific needs. Start building, experiment freely, and don't hesitate to dive into n8n's source code for inspiration.

Happy automating!

This article also includes a full video walkthrough:

Video walkthrough: Create Custom n8n Nodes

Watch the full video walkthrough on YouTube →