BaasixBaasix

Custom Workflow Modules

← Back to Documentation Home

Overview

Baasix Workflow Manager allows you to extend script nodes with custom modules and utilities through extensions. This enables you to create reusable libraries that can be accessed in all workflow script nodes.

Key Features

  • Extensible Module System: Register custom JavaScript modules that can be used in script nodes
  • Type Safety: Support for functions, objects, and classes
  • Security: Controlled module access with allowRequire flag
  • Multiple Access Patterns: Access modules via require() or as direct variables
  • Metadata: Track module registration with descriptions and timestamps

Table of Contents

  1. Registering Custom Modules
  2. Using Custom Modules in Scripts
  3. Built-in Available Modules
  4. Module Management API
  5. Best Practices
  6. Example Use Cases

Registering Custom Modules

Basic Registration

Register custom modules in an extension using the registerCustomModule method:

// extensions/baasix-workflow-modules-myutils/index.js
export default {
    id: 'my-custom-modules',
    name: 'My Custom Modules',
    description: 'Custom utilities for workflows',

    register: async ({ services }) => {
        const workflowService = services.workflowService;

        // Register a utility object
        const myUtils = {
            formatPhone(phoneNumber) {
                const cleaned = String(phoneNumber).replace(/\D/g, '');
                const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
                if (match) {
                    return `(${match[1]}) ${match[2]}-${match[3]}`;
                }
                return phoneNumber;
            },

            calculateAge(birthdate) {
                const today = new Date();
                const birth = new Date(birthdate);
                let age = today.getFullYear() - birth.getFullYear();
                const monthDiff = today.getMonth() - birth.getMonth();
                if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
                    age--;
                }
                return age;
            },
        };

        workflowService.registerCustomModule('myUtils', myUtils, {
            description: 'Custom utility functions',
            allowRequire: true,
        });

        console.info('Custom modules registered');
    },
};

Registration Options

workflowService.registerCustomModule(moduleName, moduleExport, options);

Parameters:

  • moduleName (string, required): Name used to access the module
  • moduleExport (any, required): The module export (function, object, or class)
  • options (object, optional):
    • description (string): Description of the module
    • allowRequire (boolean): Allow require() access (default: true)

Registering Different Module Types

1. Object with Functions

const utils = {
    add: (a, b) => a + b,
    multiply: (a, b) => a * b,
};

workflowService.registerCustomModule('mathUtils', utils);

2. Single Function

const formatCurrency = (amount, currency = 'USD') => {
    return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency,
    }).format(amount);
};

workflowService.registerCustomModule('formatCurrency', formatCurrency);

3. Class

class APIClient {
    constructor(baseURL, apiKey) {
        this.baseURL = baseURL;
        this.apiKey = apiKey;
    }

    async get(endpoint) {
        const axios = require('axios');
        return axios.get(`${this.baseURL}${endpoint}`, {
            headers: { 'Authorization': `Bearer ${this.apiKey}` },
        });
    }

    static create(baseURL, apiKey) {
        return new APIClient(baseURL, apiKey);
    }
}

workflowService.registerCustomModule('APIClient', APIClient);

4. Async Functions

const myAsyncUtils = {
    async fetchUserData(userId) {
        const axios = require('axios');
        const response = await axios.get(`https://api.example.com/users/${userId}`);
        return response.data;
    },

    async retry(fn, maxRetries = 3) {
        for (let i = 0; i < maxRetries; i++) {
            try {
                return await fn();
            } catch (error) {
                if (i === maxRetries - 1) throw error;
                await new Promise(resolve => setTimeout(resolve, 1000 * i));
            }
        }
    },
};

workflowService.registerCustomModule('asyncUtils', myAsyncUtils);

Using Custom Modules in Scripts

Once registered, custom modules can be used in workflow script nodes in two ways:

Custom modules are automatically available as variables in the script context:

// In a workflow script node:

// Use registered utility functions directly
const formatted = myUtils.formatPhone(trigger.phoneNumber);
const age = myUtils.calculateAge(trigger.birthdate);

// Use validators
if (myValidators.isEmail(trigger.email)) {
    console.log('Valid email');
}

// Use classes
const client = MyAPIClient.create('https://api.example.com', variables.apiKey);
const data = await client.get('/users');

return {
    formatted,
    age,
    data,
};

Method 2: Using require()

You can also use require() to load custom modules:

// In a workflow script node:

const myUtils = require('myUtils');
const myValidators = require('myValidators');

const phone = myUtils.formatPhone(trigger.phoneNumber);
const isValid = myValidators.isEmail(trigger.email);

return { phone, isValid };

Combining Built-in and Custom Modules

// Built-in modules
const _ = require('lodash');
const dayjs = require('dayjs');

// Custom modules
const myUtils = require('myUtils');

// Process data using both
const users = trigger.users;
const validUsers = _.filter(users, user => myValidators.isEmail(user.email));
const formatted = validUsers.map(user => ({
    ...user,
    phone: myUtils.formatPhone(user.phone),
    age: myUtils.calculateAge(user.birthdate),
    joinedDate: dayjs(user.createdAt).format('MMMM DD, YYYY'),
}));

return { users: formatted, count: formatted.length };

Built-in Available Modules

In addition to custom modules, the following built-in modules are always available:

Utility Libraries

  • lodash / _: Array and object manipulation
  • dayjs: Date parsing and formatting
  • axios: HTTP requests

Security & Validation

  • crypto: Cryptographic functions
  • uuid: Generate UUIDs
  • joi: Schema validation
  • validator: String validation
  • bcrypt: Password hashing
  • jsonwebtoken: JWT creation and validation

Built-in JavaScript

  • console, JSON, Math, Date, String, Number, Boolean, Array, Object
  • Promise, setTimeout, setInterval, clearTimeout, clearInterval

Workflow Context

  • context: Full workflow context
  • trigger: Trigger data
  • outputs: Outputs from previous nodes
  • variables: Workflow variables
  • loop: Loop iteration context (when inside a loop)

Module Management API

Check if Module is Available

const isAvailable = workflowService.isModuleAvailable('myUtils');
console.log('Module available:', isAvailable);

Get All Registered Modules

const modules = workflowService.getRegisteredModules();
console.log('Registered modules:', modules);

// Output:
// [
//   {
//     name: 'myUtils',
//     description: 'Custom utility functions',
//     allowRequire: true,
//     registeredAt: '2025-01-10T10:30:00Z'
//   },
//   ...
// ]

Unregister a Module

const success = workflowService.unregisterCustomModule('myUtils');
console.log('Module unregistered:', success);

Best Practices

1. Module Naming

Use descriptive, unique names for your modules:

// Good
registerCustomModule('companyUtils', utils);
registerCustomModule('companyValidators', validators);
registerCustomModule('CompanyAPIClient', APIClient);

// Avoid
registerCustomModule('utils', utils); // Too generic
registerCustomModule('helpers', helpers); // Too vague

2. Error Handling

Always handle errors in your custom functions:

const myUtils = {
    safeParseJSON(jsonString) {
        try {
            return JSON.parse(jsonString);
        } catch (error) {
            console.error('JSON parse error:', error.message);
            return null;
        }
    },

    async safeFetch(url) {
        try {
            const axios = require('axios');
            const response = await axios.get(url);
            return { success: true, data: response.data };
        } catch (error) {
            return { success: false, error: error.message };
        }
    },
};

3. Documentation

Add JSDoc comments to your custom functions:

const myUtils = {
    /**
     * Format a phone number to US format
     * @param {string} phoneNumber - Raw phone number
     * @returns {string} Formatted phone number (XXX) XXX-XXXX
     */
    formatPhone(phoneNumber) {
        // implementation
    },

    /**
     * Calculate age from birthdate
     * @param {string|Date} birthdate - Date of birth
     * @returns {number} Age in years
     */
    calculateAge(birthdate) {
        // implementation
    },
};

4. Async/Await Support

Use async functions for operations that require I/O:

const myUtils = {
    async fetchAndCache(key, url) {
        // Check cache first
        const cached = await redisClient.get(key);
        if (cached) return JSON.parse(cached);

        // Fetch from API
        const axios = require('axios');
        const response = await axios.get(url);

        // Cache result
        await redisClient.setex(key, 3600, JSON.stringify(response.data));

        return response.data;
    },
};

5. Stateless Functions

Keep your utility functions stateless when possible:

// Good - Stateless
const myUtils = {
    calculate(a, b) {
        return a + b;
    },
};

// Avoid - Stateful (unless necessary)
const myUtils = {
    counter: 0,
    increment() {
        return ++this.counter; // State persists across calls
    },
};

6. Security Considerations

  • Never expose sensitive credentials in modules
  • Validate all inputs in your functions
  • Avoid eval() or Function() constructors
  • Use allowRequire: false for sensitive modules
// Secure way to handle API credentials
class SecureAPIClient {
    constructor() {
        // Don't store credentials in the module
        // Users should pass them from workflow variables
    }

    async request(apiKey, endpoint) {
        if (!apiKey) {
            throw new Error('API key is required');
        }

        // Validate endpoint
        if (!endpoint.startsWith('/')) {
            throw new Error('Invalid endpoint');
        }

        // Make request
        const axios = require('axios');
        return axios.get(`https://api.example.com${endpoint}`, {
            headers: { 'Authorization': `Bearer ${apiKey}` },
        });
    }
}

Example Use Cases

Use Case 1: Data Transformation Pipeline

// Extension registration
const dataTransformers = {
    normalizeEmail(email) {
        return String(email).toLowerCase().trim();
    },

    parseAddress(addressString) {
        const parts = addressString.split(',').map(s => s.trim());
        return {
            street: parts[0] || '',
            city: parts[1] || '',
            state: parts[2] || '',
            zip: parts[3] || '',
        };
    },

    sanitizeName(name) {
        return name
            .trim()
            .replace(/[^a-zA-Z\s]/g, '')
            .replace(/\s+/g, ' ');
    },
};

workflowService.registerCustomModule('dataTransformers', dataTransformers);

// Usage in script node
const users = trigger.users.map(user => ({
    email: dataTransformers.normalizeEmail(user.email),
    name: dataTransformers.sanitizeName(user.name),
    address: dataTransformers.parseAddress(user.addressString),
}));

return { processedUsers: users };

Use Case 2: Business Logic Library

// Extension registration
const businessRules = {
    calculateDiscount(orderTotal, customerType) {
        const discounts = {
            vip: 0.20,
            premium: 0.15,
            regular: 0.05,
        };
        return orderTotal * (discounts[customerType] || 0);
    },

    isEligibleForFreeShipping(orderTotal, country) {
        const thresholds = {
            US: 50,
            CA: 75,
            UK: 100,
        };
        return orderTotal >= (thresholds[country] || 150);
    },

    calculateTax(amount, state) {
        const taxRates = {
            CA: 0.0725,
            NY: 0.08875,
            TX: 0.0625,
        };
        return amount * (taxRates[state] || 0);
    },
};

workflowService.registerCustomModule('businessRules', businessRules);

// Usage in script node
const order = trigger.order;
const discount = businessRules.calculateDiscount(order.total, order.customerType);
const tax = businessRules.calculateTax(order.total - discount, order.state);
const freeShipping = businessRules.isEligibleForFreeShipping(order.total, order.country);

return {
    subtotal: order.total,
    discount,
    tax,
    shipping: freeShipping ? 0 : 10,
    total: order.total - discount + tax + (freeShipping ? 0 : 10),
};

Use Case 3: External API Integration

// Extension registration
class SlackClient {
    constructor(webhookUrl) {
        this.webhookUrl = webhookUrl;
    }

    async sendMessage(text, channel) {
        const axios = require('axios');
        return axios.post(this.webhookUrl, {
            text,
            channel,
        });
    }

    async sendRichMessage(blocks, channel) {
        const axios = require('axios');
        return axios.post(this.webhookUrl, {
            blocks,
            channel,
        });
    }

    static create(webhookUrl) {
        return new SlackClient(webhookUrl);
    }
}

workflowService.registerCustomModule('SlackClient', SlackClient);

// Usage in script node
const slack = SlackClient.create(variables.slackWebhook);

await slack.sendRichMessage([
    {
        type: 'section',
        text: {
            type: 'mrkdwn',
            text: `*New Order:* ${trigger.orderId}`,
        },
    },
    {
        type: 'section',
        fields: [
            { type: 'mrkdwn', text: `*Customer:*\n${trigger.customerName}` },
            { type: 'mrkdwn', text: `*Total:*\n$${trigger.total}` },
        ],
    },
], '#orders');

return { notificationSent: true };

Use Case 4: Validation Library

// Extension registration
const validators = {
    validateOrder(order) {
        const errors = [];

        if (!order.customerId) {
            errors.push('Customer ID is required');
        }

        if (!order.items || order.items.length === 0) {
            errors.push('Order must contain at least one item');
        }

        if (order.total <= 0) {
            errors.push('Order total must be greater than zero');
        }

        return {
            valid: errors.length === 0,
            errors,
        };
    },

    validateUser(user) {
        const errors = [];

        if (!user.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) {
            errors.push('Valid email is required');
        }

        if (!user.name || user.name.length < 2) {
            errors.push('Name must be at least 2 characters');
        }

        if (user.age && (user.age < 18 || user.age > 120)) {
            errors.push('Age must be between 18 and 120');
        }

        return {
            valid: errors.length === 0,
            errors,
        };
    },
};

workflowService.registerCustomModule('validators', validators);

// Usage in script node
const orderValidation = validators.validateOrder(trigger.order);

if (!orderValidation.valid) {
    throw new Error(`Order validation failed: ${orderValidation.errors.join(', ')}`);
}

return { validated: true };

Complete Extension Example

Here's a complete extension that registers multiple types of custom modules:

// extensions/baasix-workflow-modules-company/index.js
import crypto from 'crypto';

// Utility functions
const companyUtils = {
    generateOrderId() {
        const timestamp = Date.now().toString(36);
        const randomStr = Math.random().toString(36).substring(2, 7);
        return `ORD-${timestamp}-${randomStr}`.toUpperCase();
    },

    hashSensitiveData(data) {
        return crypto.createHash('sha256').update(data).digest('hex');
    },

    async retry(fn, options = {}) {
        const maxRetries = options.maxRetries || 3;
        const delay = options.delay || 1000;

        for (let i = 0; i < maxRetries; i++) {
            try {
                return await fn();
            } catch (error) {
                if (i === maxRetries - 1) throw error;
                await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
            }
        }
    },
};

// API Client
class CompanyAPI {
    constructor(baseURL, apiKey) {
        this.baseURL = baseURL;
        this.apiKey = apiKey;
    }

    async request(method, endpoint, data = null) {
        const axios = require('axios');
        const config = {
            method,
            url: `${this.baseURL}${endpoint}`,
            headers: {
                'Authorization': `Bearer ${this.apiKey}`,
                'Content-Type': 'application/json',
            },
        };

        if (data) {
            config.data = data;
        }

        const response = await axios(config);
        return response.data;
    }

    async get(endpoint) {
        return this.request('GET', endpoint);
    }

    async post(endpoint, data) {
        return this.request('POST', endpoint, data);
    }

    static create(baseURL, apiKey) {
        return new CompanyAPI(baseURL, apiKey);
    }
}

// Validators
const companyValidators = {
    isValidOrderNumber(orderNumber) {
        return /^ORD-[A-Z0-9]+-[A-Z0-9]+$/.test(orderNumber);
    },

    isValidProductSKU(sku) {
        return /^[A-Z]{2,4}-\d{4,6}$/.test(sku);
    },

    hasRequiredFields(obj, fields) {
        return fields.every(field => obj[field] !== undefined && obj[field] !== null);
    },
};

export default {
    id: 'company-workflow-modules',
    name: 'Company Workflow Modules',
    description: 'Custom modules for company-specific workflows',

    register: async ({ services }) => {
        const workflowService = services.workflowService;

        if (!workflowService) {
            console.warn('WorkflowService not available');
            return;
        }

        // Register all modules
        workflowService.registerCustomModule('companyUtils', companyUtils, {
            description: 'Company utility functions',
            allowRequire: true,
        });

        workflowService.registerCustomModule('CompanyAPI', CompanyAPI, {
            description: 'Company API client',
            allowRequire: true,
        });

        workflowService.registerCustomModule('companyValidators', companyValidators, {
            description: 'Company validation functions',
            allowRequire: true,
        });

        console.info('✅ Company workflow modules registered');
    },
};

← Back to Documentation Home

On this page