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 called DataFlow. Right now I have two separate operations: login and file transfer. When I search for DataFlow in the node panel, both operations appear as individual nodes which is not what I want.

I want to achieve the same structure that AWS S3 uses where you have one main node and then you can choose different operations from a dropdown menu. Currently my setup creates separate core nodes (DataFlow Login and DataFlow File Transfer) instead of one unified node.

Here’s my current package.json configuration:

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

And here’s my main node structure:

export class DataFlow implements INodeType {
  description: INodeTypeDescription = {
    displayName: 'DataFlow',
    name: 'DataFlow',
    icon: 'file:DataFlow.png',
    group: ['transform'],
    version: 1,
    description: 'Handle login and file operations for DataFlow service.',
    defaults: {
      name: 'DataFlow',
      color: '#2E8B57',
    },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Action',
        name: 'action',
        type: 'options',
        options: [
          {
            displayName: 'Login',
            name: 'Login',
            value: 'Login',
            description: 'Login to your DataFlow account',
          },
          {
            displayName: 'Transfer File',
            name: 'transferFile',
            value: 'transferFile',
            description: 'Transfer a file using DataFlow',
          },
        ],
        default: 'Login',
        description: 'Choose the action to perform',
      },
      {
        displayName: 'Email',
        name: 'email',
        type: 'string',
        default: '',
        displayOptions: {
          show: {
            action: ['Login'],
          },
        },
        description: 'Your 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: ['transferFile'],
          },
        },
        default: '',
        description: 'ID of the workspace to use.',
      },
      {
        displayName: 'Source Format',
        name: 'sourceFormat',
        type: 'string',
        displayOptions: {
          show: {
            action: ['transferFile'],
          },
        },
        default: '',
        description: 'Format of the source file.',
      },
      {
        displayName: 'Destination Format',
        name: 'destinationFormat',
        type: 'string',
        displayOptions: {
          show: {
            action: ['transferFile'],
          },
        },
        default: '',
        description: 'Format for the destination file.',
      },
      {
        displayName: 'File Data',
        name: 'fileData',
        type: 'string',
        displayOptions: {
          show: {
            action: ['transferFile'],
          },
        },
        default: '',
        placeholder: 'Choose file...',
        typeOptions: {
          multipleValues: false,
          multipleValueButtonText: 'Add File',
        },
        required: true,
        description: 'The file that needs to be transferred.',
      },
    ],
  };
}

I tried making separate login.node.ts and transfer.node.ts files and adding them to package.json but that just creates two individual core nodes. What’s the correct way to structure this?

Your TypeScript structure looks right, but the registration is the problem. I hit this exact issue building a custom Slack node. You’re making two separate node classes when you need one class that handles both operations. Don’t use login.node.ts and fileTransfer.node.ts - merge everything into DataFlow.node.ts. In your execute method, use a switch statement or if conditions to handle different actions based on the ‘action’ parameter. Here’s the thing: n8n treats each file in your package.json nodes array as a separate node type. That’s why you’re seeing multiple entries in the panel. Your current property structure with displayOptions is fine - keep that since it’s correctly showing/hiding fields based on the selected action.

The problem’s in your package.json setup. When you list multiple files in the “nodes.main” array, n8n creates separate node types for each one. That’s exactly why you’re getting individual nodes instead of one unified node. You need to merge everything into a single node file. Create one main DataFlow.node.ts file with all your logic, then update your package.json to only reference that file: “nodes”: { “main”: [ “dist/nodes/main/DataFlow.node.js” ] }. Your TypeScript structure looks good with the action dropdown and displayOptions config. Just make sure your execute method handles different operations based on the selected action value. That’s how official n8n nodes like AWS S3 do it - one node file with multiple operations controlled through properties.

your package.json is the problem. you can only have ONE node file in the main array - not two separate ones. remove the login.node.js and fileTransfer.node.js entries and just use your single DataFlow node file. that’s what’s causing multiple nodes to show up in the panel.