How to create a single n8n node with multiple operations instead of separate nodes?

I’m working on a custom n8n node for my service. I want to build it like the AWS S3 node where you have one main node with different operations you can pick from a dropdown.

Right now when I search for my service in the node panel, I see two separate action nodes (login and file transfer). But I want them to be operations under one single node.

I’ve tried making separate node files (login.node.ts and transfer.node.ts) and adding both to package.json, but this just creates two independent nodes which isn’t what I’m looking for.

My current package.json setup:

"nodes": {
  "main": [
    "dist/nodes/main/login.node.js",
    "dist/nodes/main/transfer.node.js"
  ]
}

Here’s my node implementation:

export class MyService implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'MyService',
    name: 'MyService', 
    icon: 'file:MyService.png',
    group: ['input'],
    version: 1,
    description: 'Connect and manage files with MyService',
    defaults: {
      name: 'MyService',
      color: '#FF6B6B',
    },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Action',
        name: 'action',
        type: 'options',
        options: [
          {
            displayName: 'Login',
            name: 'login',
            value: 'login',
            description: 'Connect to your account',
          },
          {
            displayName: 'Transfer File', 
            name: 'transfer',
            value: 'transfer',
            description: 'Send a document to MyService',
          },
        ],
        default: 'login',
        description: 'Choose what to do',
      },
      {
        displayName: 'Email',
        name: 'email',
        type: 'string',
        default: '',
        displayOptions: {
          show: {
            action: ['login'],
          },
        },
        description: 'Your account email',
        required: true,
      },
      {
        displayName: 'API Key',
        name: 'apiKey',
        type: 'string',
        typeOptions: {
          password: true,
        },
        displayOptions: {
          show: {
            action: ['login'],
          },
        },
        default: '',
        description: 'Your API key for access',
        required: true,
      },
      {
        displayName: 'Document Path',
        name: 'docPath',
        type: 'string',
        displayOptions: {
          show: {
            action: ['transfer'],
          },
        },
        default: '',
        description: 'Path to the document file',
      }
    ],
  };
}

How do I structure this properly so I get one node with selectable actions rather than multiple separate nodes?

Been dealing with this exact same issue recently. Your structure’s actually correct - the problem’s likely in your build output or file organization. Check that you’re compiling to a single node file first. When this happened to me, my TypeScript build was generating multiple files even though I thought everything was consolidated. Also double-check your node’s name property matches everywhere. I wasted way too much time debugging this because I had slight naming inconsistencies that made n8n treat them as separate nodes. Your displayOptions logic for showing different fields per action looks solid though. Once you get the single file structure working, that conditional field display will work perfectly for the dropdown interface you want.

Pete’s right about the package.json issue, but there’s more going on here.

Ditch the separate files - you need one single node file. Make a MyService.node.ts with your implementation, then point package.json to that compiled file.

You’re missing the execute method that switches between actions:

async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
  const action = this.getNodeParameter('action', 0) as string;
  
  if (action === 'login') {
    const email = this.getNodeParameter('email', 0) as string;
    const apiKey = this.getNodeParameter('apiKey', 0) as string;
    // login logic here
  } else if (action === 'transfer') {
    const docPath = this.getNodeParameter('docPath', 0) as string;
    // transfer logic here
  }
  
  return [this.helpers.returnJsonArray(results)];
}

That said, custom n8n nodes are a pain and take forever. I’ve been there - ended up switching to Latenode instead. It handles multi-step workflows without custom development, and you can build that same login + transfer flow in minutes with their visual interface.

Check it out at https://latenode.com

Your implementation looks good for a multi-operation node. The main fix is in your package.json - you need to consolidate everything into one node structure instead of referencing separate node files. Just use one entry in the nodes array:

“nodes”: {
“main”: [
“dist/nodes/main/MyService.node.js”
]
}

Make sure you’ve got an execute method in your node that handles the logic for each action based on what the user picks. That’s how you manage different operations from a single node.

The Problem:

You’re building a custom n8n node for your service and want to combine multiple operations (like “login” and “file transfer”) into a single node, instead of having them as separate nodes. Your current setup uses separate .node.ts files and while your displayOptions are correctly set up to show/hide fields based on the selected action, the node is still registering as multiple independent nodes in n8n.

:thinking: Understanding the “Why” (The Root Cause):

The issue isn’t with your conditional display logic (displayOptions). The core problem is that n8n interprets each .node.ts file (or compiled .node.js file) as a separate node. Even though you are cleverly using displayOptions to control which fields are shown depending on the selected action, the underlying structure is incorrect; therefore, n8n doesn’t recognize the logic as a single node with various operations. You need a single node implementation to create the unified node behavior. While your individual node implementations may be correct, the way n8n is using the structure makes it register those implementations as multiple nodes.

:gear: Step-by-Step Guide:

  1. Consolidate Your Node Implementation: Combine your login.node.ts and transfer.node.ts files into a single file, for example, MyService.node.ts. This single file will contain all the logic for both the login and file transfer operations. Remember to adjust imports accordingly.

  2. Implement the execute Method: Add an execute method to your MyService class. This method will handle the logic for each operation based on the selected action from the dropdown.

    async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
        const action = this.getNodeParameter('action', 0) as string;
        let results: INodeExecutionData[] = [];
    
        if (action === 'login') {
            const email = this.getNodeParameter('email', 0) as string;
            const apiKey = this.getNodeParameter('apiKey', 0) as string;
            // Your login logic here.  Example:
            const loginResponse = await this.helpers.request({
                method: 'POST',
                url: 'your-login-endpoint',
                body: { email, apiKey },
            });
            // Store login token or other relevant data in the workflow.
            results.push({json: loginResponse.data});
    
        } else if (action === 'transfer') {
            const docPath = this.getNodeParameter('docPath', 0) as string;
            // Your file transfer logic here.  Example:
            const fileTransferResponse = await this.helpers.request({
                method: 'POST',
                url: 'your-file-transfer-endpoint',
                formData: { file: this.helpers.fs.readFile(docPath) },
            });
            results.push({json: fileTransferResponse.data});
    
        }
    
        return [this.helpers.returnJsonArray(results)];
    }
    
  3. Update package.json: Modify your package.json file to reference only the single compiled node file.

    "nodes": {
      "main": [
        "dist/nodes/main/MyService.node.js"
      ]
    }
    
  4. Rebuild Your Node: Ensure your build process correctly compiles your MyService.node.ts file into a single MyService.node.js file in the location specified in your package.json. Double check your build process is not generating multiple files. Inconsistencies between your TypeScript files and the outputted Javascript files are a common source of errors.

  5. Reinstall and Test: Install your node in n8n again. You should now see a single MyService node with the “Action” dropdown. Test both operations to ensure they function correctly.

:mag: Common Pitfalls & What to Check Next:

  • File Paths: Double-check that the path in your package.json accurately points to the compiled MyService.node.js file.
  • Build Process: Verify your TypeScript compilation process is generating only one output file for your node. A faulty compilation process can lead to multiple .js files being created even though you intend only one.
  • Node Name Consistency: Make absolutely sure the name property in your INodeTypeDescription exactly matches the filename (without the .node.ts extension) and the entry in your package.json.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Yeah, that’s the problem - you’ve got separate node files. Put everything in one .node.ts file instead. Your properties setup is already right - AWS S3 and other multi-operation nodes work the same way. Just make sure your execute method grabs the selected operation with this.getNodeParameter(‘action’, 0) and handles each case.

Your node structure looks right for multi-operations. The real problem is you’re creating separate node classes instead of putting everything in one implementation. I always use a single MyService.node.ts file with one INodeType class containing all those properties. Then package.json just references that compiled file. Your execute method handles the action switching, but your displayOptions setup for showing/hiding fields based on actions is already solid. I’ve used this exact pattern in production - works great. Watch out for one thing though: keep your node name and displayName consistent, and double-check that your compiled JS file actually exists where package.json expects it. Build processes love dumping files in random places.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.