Skip to main content

Node.js Async Programming in SoftyFlow

This guide explains how to effectively use Node.js asynchronous programming patterns in SoftyFlow scripts, with focus on async/await syntax and best practices for writing robust, performant scripts.

Understanding Asynchronous Programming in SoftyFlow

SoftyFlow's JavaScript execution environment is built on Node.js, which means all I/O operations (database queries, API calls, file operations) are asynchronous. Understanding how to properly handle asynchronous operations is crucial for writing effective scripts.

The Promise-Based Architecture

All SoftyFlow SDK methods return Promises, which represent eventual completion (or failure) of asynchronous operations:

// ❌ Wrong - This won't work as expected
let users = SF.collection.find("users", {status: "active"}); // This returns a Promise object, not the data

// ✅ Correct - Using await to get the actual data
let users = await SF.collection.find("users", {status: "active"});

Async/Await Fundamentals

Basic Async/Await Pattern

The await keyword can only be used in functions marked with async. In SoftyFlow scripts, the execution context is automatically async, so you can use await directly:

// SoftyFlow script - automatically in async context
let user = await SF.user.getUserById("6042551d231e8c8ade19bc09");
let orders = await SF.collection.find("orders", {userId: user._id});

return {
user: user,
orderCount: orders.length,
totalAmount: orders.reduce((sum, order) => sum + order.amount, 0)
};

Sequential vs Parallel Execution

Understanding when operations run sequentially vs in parallel is crucial for performance:

Sequential Execution (Operations Depend on Each Other)

// ✅ Good - When each operation depends on the previous one
let user = await SF.user.getUserById(userId);
let userGroup = await SF.user.getGroupById(user.groupId);
let permissions = await SF.collection.findOne("permissions", {groupId: userGroup._id});

return {
user: user,
group: userGroup,
permissions: permissions
};

Parallel Execution (Independent Operations)

// ✅ Better - When operations are independent, run them in parallel
let [user, recentOrders, supportTickets] = await Promise.all([
SF.user.getUserById(userId),
SF.collection.find("orders", {userId: userId, createdAt: {$gte: lastMonth}}),
SF.collection.find("tickets", {userId: userId, status: "open"})
]);

return {
user: user,
recentOrdersCount: recentOrders.length,
openTicketsCount: supportTickets.length
};

Common SoftyFlow Async Patterns

1. Database Operations

Single Document Operations

// Find and update pattern
let existingRecord = await SF.collection.findOne("customers", {email: customerEmail});

if (existingRecord) {
// Update existing customer
await SF.collection.updateOne("customers",
{email: customerEmail},
{lastLogin: new Date(), loginCount: existingRecord.loginCount + 1}
);
} else {
// Create new customer
await SF.collection.insertOne("customers", {
email: customerEmail,
createdAt: new Date(),
loginCount: 1
});
}

Bulk Operations

// Efficient bulk processing
let usersToUpdate = await SF.collection.find("users", {status: "pending"});

// Prepare bulk operations
let bulkOps = usersToUpdate.map(user => ({
updateOne: {
filter: {_id: user._id},
update: {$set: {status: "active", activatedAt: new Date()}}
}
}));

// Execute all updates in one operation
if (bulkOps.length > 0) {
await SF.collection.bulkWrite("users", bulkOps);
}

return {processed: usersToUpdate.length};

2. API Integration Patterns

// External API call with error handling
try {
let response = await SF.utils.axios.get(`https://api.external-service.com/users/${userId}`, {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
timeout: 5000 // 5 second timeout
});

// Process the response
let externalUser = response.data;

// Update local database with external data
await SF.collection.updateOne("users",
{_id: userId},
{
externalId: externalUser.id,
lastSyncAt: new Date(),
syncStatus: "success"
}
);

return {success: true, externalId: externalUser.id};

} catch (error) {
// Handle API errors gracefully
console.error('External API call failed:', error.message);

// Update sync status
await SF.collection.updateOne("users",
{_id: userId},
{syncStatus: "failed", lastSyncError: error.message}
);

return {success: false, error: error.message};
}

3. File Processing Patterns

// Generate PDF and save to collection
try {
// Get template and data in parallel
let [template, customerData] = await Promise.all([
SF.file.getFileContentById(templateId),
SF.collection.findOne("customers", {_id: customerId})
]);

// Generate PDF
let pdfFile = await SF.file.generatePDFFromTemplate(null, templateId, {
variables: {
customer: customerData,
generatedAt: new Date().toLocaleDateString(),
invoiceNumber: await SF.utils.nextValue("invoice", 6)
},
SF_pdfName: `invoice-${customerData.code}.pdf`
});

// Save reference in orders collection
await SF.collection.updateOne("orders",
{customerId: customerId},
{pdfFileId: pdfFile._id, pdfGeneratedAt: new Date()}
);

return {pdfGenerated: true, fileId: pdfFile._id};

} catch (error) {
console.error('PDF generation failed:', error);
return {pdfGenerated: false, error: error.message};
}

Best Practices for SoftyFlow Scripts

1. Error Handling

Always wrap potentially failing operations in try-catch blocks:

// ✅ Good - Robust error handling
async function processOrder(orderId) {
try {
let order = await SF.collection.findOne("orders", {_id: orderId});

if (!order) {
throw new Error(`Order ${orderId} not found`);
}

// Process order...
let result = await processPayment(order);

return {success: true, result: result};

} catch (error) {
console.error('Order processing failed:', error.message);

// Log error for debugging
await SF.collection.insertOne("error_logs", {
operation: "processOrder",
orderId: orderId,
error: error.message,
timestamp: new Date()
});

return {success: false, error: error.message};
}
}

2. Performance Optimization

Use Parallel Operations When Possible

// ❌ Slow - Sequential operations
let user = await SF.user.getUserById(userId);
let orders = await SF.collection.find("orders", {userId: userId});
let preferences = await SF.collection.findOne("preferences", {userId: userId});

// ✅ Fast - Parallel operations
let [user, orders, preferences] = await Promise.all([
SF.user.getUserById(userId),
SF.collection.find("orders", {userId: userId}),
SF.collection.findOne("preferences", {userId: userId})
]);

Minimize Database Queries

// ❌ Inefficient - Multiple queries
let results = [];
for (let userId of userIds) {
let user = await SF.user.getUserById(userId);
results.push(user);
}

// ✅ Efficient - Single aggregation query
let results = await SF.collection.aggregate("users", [
{$match: {_id: {$in: userIds}}},
{$project: {firstName: 1, lastName: 1, email: 1}}
]);

3. Variable Management

// ✅ Good - Clear variable organization
let processData = {
// Input data validation
customerId: customerId || null,
orderDate: orderDate || new Date(),
items: Array.isArray(items) ? items : [],

// Calculated values
subtotal: 0,
tax: 0,
total: 0,

// Status tracking
processed: false,
errors: []
};

// Validate required data
if (!processData.customerId) {
processData.errors.push("Customer ID is required");
}

if (processData.items.length === 0) {
processData.errors.push("Order must contain at least one item");
}

if (processData.errors.length > 0) {
return processData; // Return early with errors
}

// Process the order...

4. Conditional Processing

// ✅ Good - Clean conditional logic with async operations
let approvalRequired = false;
let approvers = [];

// Check if approval is needed based on amount
if (orderTotal > 10000) {
approvalRequired = true;
approvers = await SF.user.getUsersInGroupList(["managers", "directors"]);
} else if (orderTotal > 5000) {
approvalRequired = true;
approvers = await SF.user.getUsersinGroup("managers");
}

if (approvalRequired) {
// Create approval task
let approvalTask = await SF.instance.validateTask(currentTaskId, {
requiresApproval: true,
approvers: approvers.map(user => user.email),
amount: orderTotal
});

return {
status: "pending_approval",
approvers: approvers.length,
taskId: approvalTask.id
};
} else {
// Auto-approve small orders
return {
status: "approved",
autoApproved: true
};
}

Advanced Patterns

1. Retry Logic for External Services

async function callExternalServiceWithRetry(url, data, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
let response = await SF.utils.axios.post(url, data, {
timeout: 10000
});

return response.data; // Success, return data

} catch (error) {
console.log(`Attempt ${attempt} failed:`, error.message);

if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}

// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}

// Usage
try {
let result = await callExternalServiceWithRetry("https://api.example.com/process", orderData);
return {success: true, externalId: result.id};
} catch (error) {
return {success: false, error: error.message};
}

2. Batch Processing with Progress Tracking

async function processBatchWithProgress(items, batchSize = 10) {
let results = [];
let processed = 0;
let errors = [];

// Process items in batches
for (let i = 0; i < items.length; i += batchSize) {
let batch = items.slice(i, i + batchSize);

// Process batch in parallel
let batchPromises = batch.map(async (item) => {
try {
let result = await SF.collection.insertOne("processed_items", {
originalId: item.id,
processedAt: new Date(),
data: item
});
return {success: true, id: result.insertedId};
} catch (error) {
return {success: false, error: error.message, item: item};
}
});

let batchResults = await Promise.all(batchPromises);

// Collect results and track progress
for (let result of batchResults) {
if (result.success) {
results.push(result);
} else {
errors.push(result);
}
processed++;
}

// Log progress
console.log(`Processed ${processed}/${items.length} items`);

// Optional: Update progress in database
await SF.collection.updateOne("batch_jobs",
{_id: jobId},
{
processed: processed,
total: items.length,
progress: Math.round((processed / items.length) * 100)
}
);
}

return {
total: items.length,
successful: results.length,
failed: errors.length,
errors: errors
};
}

3. Complex Workflow Orchestration

// Complex workflow with multiple async operations
async function processCompleteOrder(orderData) {
let workflow = {
orderId: null,
customerId: orderData.customerId,
status: "started",
steps: [],
errors: []
};

try {
// Step 1: Validate customer
workflow.steps.push({step: "validate_customer", status: "running"});
let customer = await SF.user.getUserById(orderData.customerId);
if (!customer) {
throw new Error("Customer not found");
}
workflow.steps[0].status = "completed";

// Step 2: Check inventory (parallel for all items)
workflow.steps.push({step: "check_inventory", status: "running"});
let inventoryChecks = await Promise.all(
orderData.items.map(item =>
SF.collection.findOne("inventory", {productId: item.productId})
)
);

let insufficientStock = inventoryChecks.filter((stock, index) =>
!stock || stock.quantity < orderData.items[index].quantity
);

if (insufficientStock.length > 0) {
throw new Error("Insufficient inventory for some items");
}
workflow.steps[1].status = "completed";

// Step 3: Create order record
workflow.steps.push({step: "create_order", status: "running"});
let order = await SF.collection.insertOne("orders", {
customerId: customer._id,
items: orderData.items,
status: "processing",
createdAt: new Date(),
total: orderData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
});
workflow.orderId = order.insertedId;
workflow.steps[2].status = "completed";

// Step 4: Update inventory (sequential to avoid conflicts)
workflow.steps.push({step: "update_inventory", status: "running"});
for (let item of orderData.items) {
await SF.collection.updateOne("inventory",
{productId: item.productId},
{$inc: {quantity: -item.quantity, reserved: item.quantity}}
);
}
workflow.steps[3].status = "completed";

// Step 5: Generate invoice
workflow.steps.push({step: "generate_invoice", status: "running"});
let invoiceNumber = await SF.utils.nextValue("invoice", 6);
let invoice = await SF.file.generatePDFFromTemplate(null, invoiceTemplateId, {
variables: {
customer: customer,
order: order,
invoiceNumber: invoiceNumber,
items: orderData.items
},
SF_pdfName: `invoice-${invoiceNumber}.pdf`
});
workflow.steps[4].status = "completed";

// Step 6: Send confirmation email
workflow.steps.push({step: "send_confirmation", status: "running"});
await SF.utils.axios.post("https://api.email-service.com/send", {
to: customer.email,
template: "order_confirmation",
data: {
customerName: customer.firstName,
orderId: workflow.orderId,
invoiceUrl: invoice.downloadUrl
}
});
workflow.steps[5].status = "completed";

workflow.status = "completed";

} catch (error) {
workflow.status = "failed";
workflow.errors.push({
message: error.message,
step: workflow.steps.findIndex(s => s.status === "running"),
timestamp: new Date()
});

// Rollback if necessary
if (workflow.orderId) {
await SF.collection.updateOne("orders",
{_id: workflow.orderId},
{status: "failed", error: error.message}
);
}
}

// Log workflow completion
await SF.collection.insertOne("workflow_logs", workflow);

return workflow;
}

Common Pitfalls and How to Avoid Them

1. Forgetting to Use Await

// ❌ Wrong - Missing await
let user = SF.user.getUserById(userId); // This is a Promise, not the user data
console.log(user.firstName); // Error: Cannot read property 'firstName' of undefined

// ✅ Correct
let user = await SF.user.getUserById(userId);
console.log(user.firstName); // Works correctly

2. Using Async Operations in Array Methods Incorrectly

// ❌ Wrong - map() doesn't wait for async operations
let users = userIds.map(async (id) => {
return await SF.user.getUserById(id);
}); // This returns an array of Promises, not users

// ✅ Correct - Use Promise.all()
let users = await Promise.all(
userIds.map(id => SF.user.getUserById(id))
);

3. Not Handling Errors Properly

// ❌ Risky - Unhandled errors can crash the script
let result = await SF.collection.insertOne("orders", orderData);

// ✅ Safe - Always handle potential errors
try {
let result = await SF.collection.insertOne("orders", orderData);
return {success: true, orderId: result.insertedId};
} catch (error) {
console.error('Order creation failed:', error);
return {success: false, error: error.message};
}

4. Inefficient Sequential Processing

// ❌ Slow - Processing items one by one
for (let item of items) {
await processItem(item); // Each item waits for the previous one
}

// ✅ Fast - Process items in parallel (when possible)
await Promise.all(items.map(item => processItem(item)));

// ✅ Balanced - Process in batches to avoid overwhelming the system
for (let i = 0; i < items.length; i += 10) {
let batch = items.slice(i, i + 10);
await Promise.all(batch.map(item => processItem(item)));
}

Testing and Debugging Tips

1. Use Console Logging Effectively

console.log('Starting order processing for:', orderId);

let order = await SF.collection.findOne("orders", {_id: orderId});
console.log('Retrieved order:', JSON.stringify(order, null, 2));

if (!order) {
console.error('Order not found:', orderId);
return {error: "Order not found"};
}

console.log('Order processing completed successfully');

2. Return Detailed Information for Debugging

return {
success: true,
orderId: order._id,
customerEmail: customer.email,
itemsProcessed: items.length,
totalAmount: total,
processingTime: Date.now() - startTime,
// Include debug info in development
debug: {
inventoryChecks: inventoryResults,
emailSent: emailResult.success,
invoiceGenerated: !!invoiceFile
}
};

3. Use the SoftyFlow Testing Interface

  • Add test variables with realistic sample data
  • Test each script component separately
  • Verify variable types and values
  • Check error handling paths

Performance Considerations

  1. Minimize Database Queries: Use aggregation and bulk operations when possible
  2. Use Parallel Processing: Run independent operations concurrently
  3. Implement Timeouts: Set reasonable timeouts for external API calls
  4. Cache Frequently Used Data: Store commonly accessed data in variables
  5. Batch Operations: Process multiple items together rather than individually
  6. Monitor Script Execution Time: Keep scripts lean and efficient

Conclusion

Mastering async/await patterns in SoftyFlow allows you to build robust, efficient, and maintainable process automation scripts. Remember to:

  • Always use await with SDK methods
  • Handle errors gracefully with try-catch blocks
  • Use parallel processing for independent operations
  • Implement proper logging and debugging practices
  • Test thoroughly using the SoftyFlow testing interface

Following these patterns and best practices will help you create reliable automation scripts that scale with your business processes.