Files
OpenNoodl/dev-docs/reference/LEARNINGS-NODE-CREATION.md
Richard Osborne 8dd4f395c0 docs: add comprehensive guide for creating Noodl nodes
Documents the complete process learned from building the HTTP Request node:
- Node file structure and required properties
- Registration in noodl-runtime.js
- Adding to nodelibraryexport.js for Node Picker visibility
- Port types, dynamic ports, signals, async operations
- Common issues and troubleshooting
2025-12-08 21:40:01 +01:00

11 KiB

Creating Nodes in OpenNoodl

This guide documents the complete process for creating new nodes in the OpenNoodl runtime, based on learnings from building the HTTP Request node.


Overview

Nodes in Noodl are defined in the noodl-runtime package and need to be:

  1. Created - Define the node in a .js file
  2. Registered - Add to noodl-runtime.js
  3. Indexed - Add to nodelibraryexport.js for Node Picker visibility

Step 1: Create the Node File

Create a new file in the appropriate category folder:

packages/noodl-runtime/src/nodes/std-library/
├── data/           # Data nodes (REST, HTTP, collections)
├── variables/      # Variable nodes (string, number, boolean)
├── user/           # User authentication nodes
└── *.js            # General utility nodes

Basic Node Structure

'use strict';

var MyNode = {
  // REQUIRED: Unique identifier for the node
  name: 'net.noodl.MyNode',
  
  // REQUIRED: Display name in Node Picker and canvas
  displayNodeName: 'My Node',
  
  // OPTIONAL: Documentation URL
  docs: 'https://docs.noodl.net/nodes/category/my-node',
  
  // REQUIRED: Category for organization (Data, Visual, Logic, etc.)
  category: 'Data',
  
  // OPTIONAL: Node color theme
  // Options: 'data' (green), 'visual' (blue), 'component' (purple), 'javascript' (pink), 'default' (gray)
  color: 'data',
  
  // OPTIONAL: Search keywords for Node Picker
  searchTags: ['my', 'node', 'custom', 'example'],
  
  // OPTIONAL: Called when node instance is created
  initialize: function () {
    this._internal.myData = {};
  },
  
  // OPTIONAL: Data shown in debug inspector
  getInspectInfo() {
    return this._internal.inspectData;
  },
  
  // REQUIRED: Define input ports
  inputs: {
    inputName: {
      type: 'string',           // See "Port Types" section below
      displayName: 'Input Name',
      group: 'General',         // Group in property panel
      default: 'default value'
    },
    doAction: {
      type: 'signal',
      displayName: 'Do Action',
      group: 'Actions'
    }
  },
  
  // REQUIRED: Define output ports
  outputs: {
    outputValue: {
      type: 'string',
      displayName: 'Output Value',
      group: 'Results'
    },
    success: {
      type: 'signal',
      displayName: 'Success',
      group: 'Events'
    },
    failure: {
      type: 'signal',
      displayName: 'Failure',
      group: 'Events'
    }
  },
  
  // OPTIONAL: Methods to handle input changes
  methods: {
    setInputName: function (value) {
      this._internal.inputName = value;
      // Optionally trigger output update
      this.flagOutputDirty('outputValue');
    },
    
    // Signal handler - name must match input name with 'Trigger' suffix
    doActionTrigger: function () {
      // Perform the action
      const result = this.processInput(this._internal.inputName);
      this._internal.outputValue = result;
      
      // Update outputs
      this.flagOutputDirty('outputValue');
      this.sendSignalOnOutput('success');
    }
  },
  
  // OPTIONAL: Return output values
  getOutputValue: function (name) {
    if (name === 'outputValue') {
      return this._internal.outputValue;
    }
  }
};

// REQUIRED: Export the node
module.exports = {
  node: MyNode,
  
  // OPTIONAL: Setup function for dynamic ports
  setup: function (context, graphModel) {
    // See "Dynamic Ports" section below
  }
};

Step 2: Register the Node

Add the node to the registerNodes function in packages/noodl-runtime/noodl-runtime.js:

function registerNodes(noodlRuntime) {
  [
    // ... existing nodes ...
    
    // Add your new node
    require('./src/nodes/std-library/data/mynode'),
    
    // ... more nodes ...
  ].forEach((node) => noodlRuntime.registerNode(node));
}

Important: The order in this array doesn't matter, but group related nodes together for readability.


Step 3: Add to Node Picker Index

CRITICAL: This step is often forgotten! Without it, the node won't appear in the Node Picker.

Edit packages/noodl-runtime/src/nodelibraryexport.js and add your node to the appropriate category in the coreNodes array:

const coreNodes = [
  // ... other categories ...
  {
    name: 'Read & Write Data',
    description: 'Arrays, objects, cloud data',
    type: 'data',
    subCategories: [
      // ... other subcategories ...
      {
        name: 'External Data',
        items: ['net.noodl.MyNode', 'REST2']  // Add your node name here
      }
    ]
  },
  // ... more categories ...
];

Port Types

Common Input/Output Types

Type Description Example Use
string Text value URLs, names, content
number Numeric value Counts, sizes, coordinates
boolean True/false Toggles, conditions
signal Trigger without data Action buttons, events
object JSON object API responses, data structures
array List of items Collections, results
color Color value Styling
* Any type Generic ports

Input-Specific Types

Type Description
{ name: 'enum', enums: [...] } Dropdown selection
{ name: 'stringlist' } List of strings (comma-separated)
{ name: 'number', min, max } Number with constraints

Example Enum Input

inputs: {
  method: {
    type: {
      name: 'enum',
      enums: [
        { value: 'GET', label: 'GET' },
        { value: 'POST', label: 'POST' },
        { value: 'PUT', label: 'PUT' },
        { value: 'DELETE', label: 'DELETE' }
      ]
    },
    displayName: 'Method',
    default: 'GET',
    group: 'Request'
  }
}

Dynamic Ports

Dynamic ports are created at runtime based on configuration. This is useful when the number or names of ports depend on user settings.

Setup Function Pattern

module.exports = {
  node: MyNode,
  
  setup: function (context, graphModel) {
    // Only run in editor, not deployed
    if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
      return;
    }

    function updatePorts(nodeId, parameters, editorConnection) {
      const ports = [];
      
      // Always include base ports from node definition
      // Add dynamic ports based on parameters
      if (parameters.items) {
        parameters.items.split(',').forEach((item) => {
          ports.push({
            name: 'item-' + item.trim(),
            displayName: item.trim(),
            type: 'string',
            plug: 'input',
            group: 'Items'
          });
        });
      }
      
      // Send ports to editor
      editorConnection.sendDynamicPorts(nodeId, ports);
    }

    function managePortsForNode(node) {
      updatePorts(node.id, node.parameters || {}, context.editorConnection);

      node.on('parameterUpdated', function (event) {
        if (event.name === 'items') {
          updatePorts(node.id, node.parameters, context.editorConnection);
        }
      });
    }

    // Listen for graph import completion
    graphModel.on('editorImportComplete', () => {
      // Listen for new nodes of this type
      graphModel.on('nodeAdded.net.noodl.MyNode', function (node) {
        managePortsForNode(node);
      });

      // Handle existing nodes
      for (const node of graphModel.getNodesWithType('net.noodl.MyNode')) {
        managePortsForNode(node);
      }
    });
  }
};

Handling Signals

Signals are trigger-based ports (no data, just an event).

Receiving Signals (Input)

// In methods object
methods: {
  // Pattern: inputName + 'Trigger'
  fetchTrigger: function () {
    // Called when 'fetch' signal is triggered
    this.doFetch();
  }
}

Sending Signals (Output)

// Send a signal pulse
this.sendSignalOnOutput('success');
this.sendSignalOnOutput('failure');

Updating Outputs

When an output value changes, you must flag it as dirty:

// Flag a single output
this.flagOutputDirty('outputValue');

// Flag multiple outputs
this.flagOutputDirty('response');
this.flagOutputDirty('statusCode');

// Then send signal if needed
this.sendSignalOnOutput('complete');

Async Operations

For asynchronous operations (API calls, file I/O), use standard async patterns:

methods: {
  fetchTrigger: function () {
    const self = this;
    
    fetch(this._internal.url)
      .then(response => response.json())
      .then(data => {
        self._internal.response = data;
        self.flagOutputDirty('response');
        self.sendSignalOnOutput('success');
      })
      .catch(error => {
        self._internal.error = error.message;
        self.flagOutputDirty('error');
        self.sendSignalOnOutput('failure');
      });
  }
}

Debug Inspector

Provide data for the debug inspector popup:

getInspectInfo() {
  // Return an array of objects with type and value
  return [
    { type: 'text', value: 'Status: ' + this._internal.status },
    { type: 'value', value: this._internal.response }
  ];
}

Testing Your Node

  1. Start the dev server: npm run dev
  2. Open the Node Picker (click in the node graph)
  3. Search for your node by name or search tags
  4. Navigate to the category to verify placement
  5. Add the node and test inputs/outputs
  6. Check console for any errors

Common Issues

Node Not Appearing in Node Picker

Cause: Node not added to nodelibraryexport.js coreNodes array.

Fix: Add the node name to the appropriate subcategory items array.

"Cannot read property of undefined" Errors

Cause: Accessing this._internal before initialize() runs.

Fix: Always check for undefined or initialize values in initialize().

Outputs Not Updating

Cause: Forgot to call flagOutputDirty().

Fix: Call this.flagOutputDirty('portName') after setting internal value.

Signal Not Firing

Cause: Method name doesn't match pattern inputName + 'Trigger'.

Fix: Ensure signal handler method is named correctly (e.g., fetchTrigger for input fetch).


File Checklist for New Nodes

  • Create node file in packages/noodl-runtime/src/nodes/std-library/[category]/
  • Add require() to packages/noodl-runtime/noodl-runtime.js
  • Add node name to packages/noodl-runtime/src/nodelibraryexport.js coreNodes
  • Test node appears in Node Picker
  • Test all inputs/outputs work correctly
  • Verify debug inspector shows useful info

Reference Files

When creating new nodes, reference these existing nodes for patterns:

Node File Good Example Of
REST data/restnode.js Full-featured data node with scripts
HTTP data/httpnode.js Dynamic ports, configuration
String variables/string.js Simple variable node
Counter counter.js Stateful logic node
Condition condition.js Boolean logic

Last Updated: December 2024