Overview
The FunctionMember class executes custom JavaScript/TypeScript functions within workflows. It’s the most flexible member type for implementing custom business logic.
import { FunctionMember } from '@ensemble-edge/conductor';
const fn = new FunctionMember({
name: 'calculate-total',
config: {
handler: async (input: { items: number[]; tax: number }) => {
const subtotal = input.items.reduce((sum, price) => sum + price, 0);
const total = subtotal * (1 + input.tax);
return { subtotal, total };
}
}
});
const result = await fn.execute({
items: [10, 20, 30],
tax: 0.1
});
Constructor
new FunctionMember(options: FunctionMemberOptions)
options
FunctionMemberOptions
required
Function member configuration (extends MemberOptions)Function configurationModule path (for external functions)
Export name (for module functions)
Function-specific timeout
interface FunctionConfig {
handler?: (input: any, context: ExecutionContext) => Promise<any>;
module?: string;
export?: string;
timeout?: number;
}
Methods
execute()
Execute the function with input.
async execute(input: any): Promise<any>
Input data for the function
Returns: Promise<any> - Function result
Example:
const result = await fn.execute({
amount: 100,
currency: 'USD'
});
console.log(result);
Configuration Examples
Inline Handler
- member: validate-email
type: Function
config:
handler: |
(input) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return {
valid: emailRegex.test(input.email),
email: input.email
};
}
input:
email: ${input.userEmail}
External Module
- member: process-payment
type: Function
config:
module: './functions/payment.ts'
export: 'processPayment'
input:
amount: ${input.total}
method: ${input.paymentMethod}
// functions/payment.ts
export async function processPayment(input: {
amount: number;
method: string;
}): Promise<{ success: boolean; transactionId: string }> {
// Payment processing logic
const transactionId = await chargeCard(input.amount, input.method);
return {
success: true,
transactionId
};
}
With Context Access
const fn = new FunctionMember({
name: 'with-context',
config: {
handler: async (input, context) => {
console.log('Execution ID:', context.executionId);
console.log('Environment:', context.env);
console.log('Previous outputs:', context.memberOutputs);
return { processed: true };
}
}
});
Common Patterns
- member: transform-data
type: Function
config:
handler: |
(input) => ({
items: input.rawData.map(item => ({
id: item._id,
name: item.displayName,
price: parseFloat(item.cost),
available: item.inStock > 0
}))
})
input:
rawData: ${fetch-data.output.results}
Validation
- member: validate-order
type: Function
config:
handler: |
(input) => {
const errors = [];
if (!input.customerId) {
errors.push('Customer ID required');
}
if (input.items.length === 0) {
errors.push('Order must have items');
}
if (input.total <= 0) {
errors.push('Invalid total amount');
}
return {
valid: errors.length === 0,
errors
};
}
input:
customerId: ${input.customerId}
items: ${input.items}
total: ${input.total}
Aggregation
- member: aggregate-results
type: Function
config:
handler: |
(input) => {
const results = input.results;
return {
total: results.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
avgDuration: results.reduce((sum, r) => sum + r.duration, 0) / results.length
};
}
input:
results: [
${process-a.output},
${process-b.output},
${process-c.output}
]
Conditional Logic
- member: determine-action
type: Function
config:
handler: |
(input) => {
if (input.amount > 1000) {
return { action: 'manual_review', reason: 'high_value' };
} else if (input.riskScore > 0.8) {
return { action: 'flag', reason: 'high_risk' };
} else {
return { action: 'approve', reason: 'normal' };
}
}
input:
amount: ${input.orderTotal}
riskScore: ${assess-risk.output.score}
API Wrapping
- member: call-legacy-api
type: Function
config:
handler: |
async (input, context) => {
const response = await fetch(context.env.LEGACY_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${context.env.LEGACY_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: input.userId,
action: input.action
})
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
}
input:
userId: ${input.userId}
action: 'update_profile'
TypeScript Functions
Strong Typing
// functions/types.ts
export interface ProcessOrderInput {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
price: number;
}>;
}
export interface ProcessOrderOutput {
success: boolean;
orderId: string;
total: number;
status: 'pending' | 'confirmed' | 'failed';
}
// functions/order.ts
import type { ProcessOrderInput, ProcessOrderOutput } from './types';
export async function processOrder(
input: ProcessOrderInput
): Promise<ProcessOrderOutput> {
const total = input.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// Process order logic...
return {
success: true,
orderId: input.orderId,
total,
status: 'confirmed'
};
}
- member: process-order
type: Function
config:
module: './functions/order.ts'
export: 'processOrder'
input:
orderId: ${input.orderId}
customerId: ${input.customerId}
items: ${input.items}
Reusable Functions
// functions/utils.ts
export function calculateTax(amount: number, rate: number): number {
return amount * rate;
}
export function formatCurrency(amount: number, currency: string): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}
export async function sendEmail(
to: string,
subject: string,
body: string
): Promise<boolean> {
// Email sending logic
return true;
}
Error Handling
Try-Catch
- member: safe-operation
type: Function
config:
handler: |
async (input) => {
try {
const result = await riskyOperation(input.data);
return { success: true, result };
} catch (error) {
console.error('Operation failed:', error);
return {
success: false,
error: error.message,
fallback: getDefaultValue()
};
}
}
Validation Errors
- member: validate-and-process
type: Function
config:
handler: |
(input) => {
if (!input.email || !input.email.includes('@')) {
throw new Error('Invalid email address');
}
if (input.age < 18) {
throw new Error('Must be 18 or older');
}
return { valid: true, processed: true };
}
Async Operations
Parallel Execution
- member: fetch-multiple
type: Function
config:
handler: |
async (input) => {
const [users, products, orders] = await Promise.all([
fetch(`${input.apiUrl}/users`).then(r => r.json()),
fetch(`${input.apiUrl}/products`).then(r => r.json()),
fetch(`${input.apiUrl}/orders`).then(r => r.json())
]);
return { users, products, orders };
}
Sequential with Dependencies
- member: sequential-process
type: Function
config:
handler: |
async (input) => {
const user = await fetchUser(input.userId);
const preferences = await fetchPreferences(user.id);
const recommendations = await generateRecommendations(
user,
preferences
);
return { user, preferences, recommendations };
}
Caching Results
- member: expensive-calculation
type: Function
config:
handler: |
(input) => {
// Expensive computation
const result = complexCalculation(input.data);
return { result };
}
cache:
enabled: true
ttl: 3600000 # 1 hour
key: ${input.data}
Memoization
// functions/fibonacci.ts
const cache = new Map<number, number>();
export function fibonacci(n: number): number {
if (n <= 1) return n;
if (cache.has(n)) {
return cache.get(n)!;
}
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
Testing
import { FunctionMember } from '@ensemble-edge/conductor';
import { describe, it, expect } from 'vitest';
describe('FunctionMember', () => {
it('executes inline function', async () => {
const fn = new FunctionMember({
name: 'double',
config: {
handler: (input: { value: number }) => ({
result: input.value * 2
})
}
});
const result = await fn.execute({ value: 5 });
expect(result.result).toBe(10);
});
it('handles async functions', async () => {
const fn = new FunctionMember({
name: 'async-fn',
config: {
handler: async (input: { delay: number }) => {
await new Promise(resolve => setTimeout(resolve, input.delay));
return { completed: true };
}
}
});
const result = await fn.execute({ delay: 100 });
expect(result.completed).toBe(true);
});
it('passes context', async () => {
const fn = new FunctionMember({
name: 'with-context',
config: {
handler: async (input, context) => ({
executionId: context.executionId,
hasEnv: !!context.env
})
}
});
const result = await fn.execute({});
expect(result.executionId).toBeDefined();
expect(result.hasEnv).toBe(true);
});
});
Best Practices
- Keep functions focused - Single responsibility
- Use TypeScript - Type safety and IDE support
- Handle errors - Try-catch for risky operations
- Validate input - Check assumptions
- Return consistent shapes - Predictable output
- Use external modules - For complex logic
- Cache expensive operations - Improve performance
- Document expectations - Input/output contracts
- Test thoroughly - Unit test functions
- Avoid side effects - Pure functions when possible