nodes.md 10 KB

Creating Workflow Nodes

This guide explains how to create custom workflow nodes for SmartBotic.

Node File Structure

Nodes are JavaScript modules located in nodes/<category>/ directories. Each node file must export:

  • configSchema - JSON Schema for node configuration
  • inputSchema - JSON Schema for input data
  • outputSchema - JSON Schema for output data
  • execute - Async function that performs the node's action

Basic Template

/**
 * @node my-node-id
 * @name My Node Name
 * @category my-category
 * @version 1.0.0
 * @description What this node does
 * @icon icon-name
 */

const configSchema = {
    type: 'object',
    properties: {
        myOption: {
            type: 'string',
            title: 'My Option',
            description: 'Description of this option',
            default: 'default-value'
        }
    },
    required: ['myOption']
};

const inputSchema = {
    type: 'object',
    properties: {
        data: {
            type: 'any',
            description: 'Input data'
        }
    }
};

const outputSchema = {
    type: 'object',
    properties: {
        result: {
            type: 'string',
            description: 'Output result'
        }
    }
};

module.exports = {
    configSchema,
    inputSchema,
    outputSchema,

    async execute(config, input, context) {
        // Your node logic here
        return {
            result: 'success'
        };
    }
};

JSDoc Metadata Tags

The comment block at the top of the file defines node metadata:

Tag Required Description
@node Yes Unique node identifier (kebab-case)
@name Yes Display name shown in UI
@category Yes Category for grouping (e.g., http, data, email)
@version Yes Semantic version (e.g., 1.0.0)
@description Yes Brief description of node functionality
@icon No Lucide icon name (e.g., globe, mail, database)
@trigger No Add this tag if the node is a trigger (starts workflows)

Configuration Schema

The configSchema defines what options users can configure in the node editor.

Supported Field Types

// String field
myString: {
    type: 'string',
    title: 'Label',
    description: 'Help text',
    default: 'default value'
}

// Number field
myNumber: {
    type: 'number',
    title: 'Count',
    default: 10
}

// Boolean field
myBoolean: {
    type: 'boolean',
    title: 'Enable Feature',
    default: false
}

// Dropdown (enum)
myChoice: {
    type: 'string',
    title: 'Select Option',
    enum: ['option1', 'option2', 'option3'],
    default: 'option1'
}

// Object field
myHeaders: {
    type: 'object',
    title: 'Headers',
    additionalProperties: { type: 'string' }
}

Dynamic Options (Credentials)

To create a credential selector:

credentialId: {
    type: 'string',
    title: 'Credential',
    description: 'Select authentication credential',
    dynamicOptions: {
        source: 'credentials',
        filter: { type: 'bearer' }  // Filter by credential type
    }
}

Available credential types: bearer, api_key, basic, imap

Custom Outputs

Define multiple output ports:

const outputs = [
    { name: 'success', displayName: 'Success', type: 'object', color: '#10b981' },
    { name: 'error', displayName: 'Error', type: 'object', color: '#ef4444' }
];

module.exports = {
    configSchema,
    inputSchema,
    outputSchema,
    outputs,
    execute
};

Available APIs

Inside execute(), you have access to the smartbotic global object:

Logging

smartbotic.log.debug('Debug message');
smartbotic.log.info('Info message');
smartbotic.log.warn('Warning message');
smartbotic.log.error('Error message');

HTTP Requests

const response = smartbotic.http.request({
    method: 'GET',  // GET, POST, PUT, PATCH, DELETE
    url: 'https://api.example.com/data',
    headers: { 'Authorization': 'Bearer token' },
    body: JSON.stringify({ key: 'value' }),
    timeout: 30000,
    followRedirects: true
});

// Response: { status, headers, data }

Storage (Database)

// Insert document
const result = smartbotic.storage.insert('collection', { key: 'value' });
// Returns: { success, id }

// Get document
const doc = smartbotic.storage.get('collection', 'document-id');
// Returns: { found, document }

// Query documents
const results = smartbotic.storage.query('collection', { field: 'value' });
// Returns: { success, documents }

// Update document
smartbotic.storage.update('collection', 'document-id', { key: 'new-value' });

// Delete document
smartbotic.storage.delete('collection', 'document-id');

Credentials

const auth = smartbotic.credentials.get(credentialId);
if (auth.success) {
    // auth.headerName = 'Authorization' or 'X-API-Key'
    // auth.headerValue = 'Bearer <token>' or '<api-key>'
    headers[auth.headerName] = auth.headerValue;
}

Utilities

// Generate UUID
const id = smartbotic.utils.uuid();

// Sleep (pause execution)
smartbotic.utils.sleep(1000);  // milliseconds, max 300000 (5 min)

// Template interpolation
const result = smartbotic.utils.interpolate('Hello {{name}}', { name: 'World' });

// Base64 encoding/decoding
const encoded = smartbotic.utils.base64Encode('data');
const decoded = smartbotic.utils.base64Decode(encoded);

// SHA256 hash
const hash = smartbotic.utils.sha256('data');  // Returns hex string

// Object utilities
const picked = smartbotic.utils.pick(obj, ['key1', 'key2']);
const omitted = smartbotic.utils.omit(obj, ['unwantedKey']);

Important Guidelines

Indentation

Use 4 spaces for indentation (consistent with existing nodes).

Avoid Inline Comments

Do NOT use // comments inside schema definitions. The parser may incorrectly interpret them:

// BAD - comment may break parsing
const configSchema = {
    type: 'object',
    properties: {
        url: {
            type: 'string',
            default: 'http://localhost:3000'  // This is the default  <- AVOID
        }
    }
};

// GOOD - no inline comments in schema
const configSchema = {
    type: 'object',
    properties: {
        url: {
            type: 'string',
            default: 'http://localhost:3000'
        }
    }
};

String Quotes

Use single quotes for string values in schemas. The parser converts them to JSON:

// GOOD
title: 'My Title'

// AVOID - embedded quotes can cause issues
description: 'Use "quotes" carefully'  // May break parsing

Error Handling

IMPORTANT: Throw errors instead of returning success: false. This ensures the workflow fails properly:

async execute(config, input, context) {
    // Validate required inputs
    if (!config.requiredField) {
        throw new Error('Required field is missing');
    }

    // Make API call
    const response = smartbotic.http.request({
        method: 'GET',
        url: config.url,
        headers: headers
    });

    // Check response status - throw on error
    if (response.status < 200 || response.status >= 300) {
        const errorMsg = typeof response.data === 'string'
            ? response.data
            : JSON.stringify(response.data);
        throw new Error(`API error (${response.status}): ${errorMsg}`);
    }

    // Return data on success (no try/catch needed for most cases)
    return {
        data: response.data
    };
}

Do NOT catch errors just to return { success: false } - let them propagate so the workflow fails.

Migrating Nodes to Database

After creating or modifying node files, migrate them to the running database:

# Get authentication token
TOKEN=$(curl -s http://localhost:8090/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin"}' | jq -r '.accessToken')

# Migrate nodes
curl -X POST http://localhost:8090/api/v1/nodes/migrate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"nodesPath": "./nodes"}'

Runners automatically receive node updates via gRPC streaming.

Example: HTTP Download Node

/**
 * @node download-file
 * @name Download File
 * @category http
 * @version 1.0.0
 * @description Download a file from URL
 * @icon download
 */

const configSchema = {
    type: 'object',
    properties: {
        url: {
            type: 'string',
            title: 'URL',
            description: 'URL to download from'
        },
        timeout: {
            type: 'number',
            title: 'Timeout (ms)',
            default: 30000
        }
    },
    required: ['url']
};

const inputSchema = {
    type: 'object',
    properties: {
        url: { type: 'string', description: 'Override URL from input' }
    }
};

const outputSchema = {
    type: 'object',
    properties: {
        success: { type: 'boolean' },
        data: { type: 'string' },
        contentType: { type: 'string' }
    }
};

module.exports = {
    configSchema,
    inputSchema,
    outputSchema,

    async execute(config, input, context) {
        const url = input.url || config.url;

        smartbotic.log.info(`Downloading from ${url}`);

        try {
            const response = smartbotic.http.request({
                method: 'GET',
                url: url,
                timeout: config.timeout
            });

            if (response.status < 200 || response.status >= 300) {
                throw new Error(`HTTP ${response.status}`);
            }

            return {
                success: true,
                data: response.data,
                contentType: response.headers['content-type'] || ''
            };
        } catch (error) {
            smartbotic.log.error(`Download failed: ${error.message}`);
            return {
                success: false,
                error: error.message
            };
        }
    }
};

Trigger Nodes

Trigger nodes start workflow executions. Add @trigger to the JSDoc:

/**
 * @node my-trigger
 * @name My Trigger
 * @category triggers
 * @version 1.0.0
 * @description Triggers workflow on event
 * @icon zap
 * @trigger
 */

Triggers typically don't have input schemas (they generate initial data).