# Creating Workflow Nodes This guide explains how to create custom workflow nodes for SmartBotic. ## Node File Structure Nodes are JavaScript modules located in `nodes//` 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 ```javascript /** * @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 ```javascript // 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: ```javascript 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: ```javascript 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 ```javascript smartbotic.log.debug('Debug message'); smartbotic.log.info('Info message'); smartbotic.log.warn('Warning message'); smartbotic.log.error('Error message'); ``` ### HTTP Requests ```javascript 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) ```javascript // 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 ```javascript const auth = smartbotic.credentials.get(credentialId); if (auth.success) { // auth.headerName = 'Authorization' or 'X-API-Key' // auth.headerValue = 'Bearer ' or '' headers[auth.headerName] = auth.headerValue; } ``` ### Utilities ```javascript // 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: ```javascript // 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: ```javascript // 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: ```javascript 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: ```bash # 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 ```javascript /** * @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: ```javascript /** * @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).