How to create a single n8n node with multiple operations using dropdown selection?

I’m working on building a custom n8n node for my service (let’s call it MyService). I want users to find one main node when they search, but then be able to choose different operations from a dropdown menu inside that node.

Right now I have two separate operations: login and document upload. When I search for MyService in the node panel, I can see both operations, but I want them grouped under one main node like how AWS S3 node works.

I don’t want two separate core nodes (like MyService Login and MyService Document Upload). Instead I want one MyService node with an operation selector.

Here’s my current setup:

package.json configuration:

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

Main node implementation:

export class MyService implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'MyService',
    name: 'MyService',
    icon: 'file:MyService.png',
    group: ['transform'],
    version: 1,
    description: 'Handle authentication and file processing operations.',
    defaults: {
      name: 'MyService',
      color: '#2E8B57',
    },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Action',
        name: 'action',
        type: 'options',
        options: [
          {
            displayName: 'Login',
            name: 'Login',
            value: 'Login',
            description: 'Authenticate with your credentials',
          },
          {
            displayName: 'Document Upload',
            name: 'documentUpload',
            value: 'documentUpload',
            description: 'Upload and process a document',
          },
        ],
        default: 'Login',
        description: 'Choose the action to perform',
      },
      {
        displayName: 'Email',
        name: 'email',
        type: 'string',
        default: '',
        displayOptions: {
          show: {
            action: ['Login'],
          },
        },
        description: 'Your account email address',
        required: true,
      },
      {
        displayName: 'API Key',
        name: 'apiKey',
        type: 'string',
        typeOptions: {
          password: true,
        },
        displayOptions: {
          show: {
            action: ['Login'],
          },
        },
        default: '',
        description: 'Your API key for authentication',
        required: true,
      },
      {
        displayName: 'Workspace ID',
        name: 'workspaceId',
        type: 'string',
        displayOptions: {
          show: {
            action: ['documentUpload'],
          },
        },
        default: '',
        description: 'ID of the target workspace.',
      },
      {
        displayName: 'Document Type',
        name: 'documentType',
        type: 'string',
        displayOptions: {
          show: {
            action: ['documentUpload'],
          },
        },
        default: '',
        description: 'Type of document being uploaded.',
      },
      {
        displayName: 'Document Data',
        name: 'documentData',
        type: 'string',
        displayOptions: {
          show: {
            action: ['documentUpload'],
          },
        },
        default: '',
        placeholder: 'Choose document...',
        typeOptions: {
          multipleValues: false,
          multipleValueButtonText: 'Add Document',
        },
        required: true,
        description: 'The document file to upload.',
      },
    ],
  };
}

I tried making separate login.node.ts and upload.node.ts files and adding them to package.json, but that just creates two individual core nodes which isn’t what I want.

What’s the correct way to structure this so I get one unified node with operation selection?

Your properties look good, but you’re missing the execute method. After you fix package.json to point to a single file, make sure execute() handles the action parameter correctly. Try const action = this.getNodeParameter('action', 0) as string; then use a switch statement on that value to run either login or upload logic. That’s how most n8n nodes handle it.

Your structure looks good, but you’re missing the actual execution logic. Only reference one node file in your package.json, not multiple ones. Create a single MyService.node.ts file and add the execute method to handle operations. In execute, grab the selected action with this.getNodeParameter('action', 0) and branch your logic from there. I hit the same issues building custom nodes. Keep everything in one class with conditional execution based on the action parameter - works way better. Make sure your execute method returns the right response format for each operation. Your displayOptions config will automatically show the correct fields for each action.

You’re on the right track with the unified node approach, but your package.json is the problem. You’re registering multiple node files instead of just one. Fix your package.json like this: “nodes”: { “main”: [ “dist/nodes/main/MyService.node.js” ] }. Then put all your operation logic in one MyService.node.ts file. Use the execute method with a switch statement or conditional logic to handle different actions based on what’s selected in the dropdown. Don’t export multiple node classes - just export one INodeType class with all your operations. Your displayOptions setup for showing/hiding fields looks good and should work fine once you consolidate everything into that single node file.