Custom Workflow Modules
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
- Registering Custom Modules
- Using Custom Modules in Scripts
- Built-in Available Modules
- Module Management API
- Best Practices
- 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 modulemoduleExport(any, required): The module export (function, object, or class)options(object, optional):description(string): Description of the moduleallowRequire(boolean): Allowrequire()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:
Method 1: Direct Access (Recommended)
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 manipulationdayjs: Date parsing and formattingaxios: HTTP requests
Security & Validation
crypto: Cryptographic functionsuuid: Generate UUIDsjoi: Schema validationvalidator: String validationbcrypt: Password hashingjsonwebtoken: JWT creation and validation
Built-in JavaScript
console,JSON,Math,Date,String,Number,Boolean,Array,ObjectPromise,setTimeout,setInterval,clearTimeout,clearInterval
Workflow Context
context: Full workflow contexttrigger: Trigger dataoutputs: Outputs from previous nodesvariables: Workflow variablesloop: 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 vague2. 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');
},
};Related Documentation
- Workflow Routes Documentation - Complete workflow API reference
- Baasix Extensions Documentation - Extension system guide
- Hooks and Endpoints Guide - Custom logic patterns