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 SoftyflowDirect link to 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 ArchitectureDirect link to 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 FundamentalsDirect link to Async/Await Fundamentals

Basic Async/Await PatternDirect link to 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 ExecutionDirect link to Sequential vs Parallel Execution

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

Sequential Execution (Operations Depend on Each Other)Direct link to 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)Direct link to 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 PatternsDirect link to Common Softyflow Async Patterns

1. Database OperationsDirect link to 1. Database Operations

Single Document OperationsDirect link to 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 OperationsDirect link to 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 PatternsDirect link to 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 PatternsDirect link to 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 ScriptsDirect link to Best Practices for Softyflow Scripts

1. Error HandlingDirect link to 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 OptimizationDirect link to 2. Performance Optimization

Use Parallel Operations When PossibleDirect link to 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 QueriesDirect link to 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 ManagementDirect link to 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 ProcessingDirect link to 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 PatternsDirect link to Advanced Patterns

1. Retry Logic for External ServicesDirect link to 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 TrackingDirect link to 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 OrchestrationDirect link to 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 ThemDirect link to Common Pitfalls and How to Avoid Them

1. Forgetting to Use AwaitDirect link to 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 IncorrectlyDirect link to 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 ProperlyDirect link to 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 ProcessingDirect link to 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 TipsDirect link to Testing and Debugging Tips

1. Use Console Logging EffectivelyDirect link to 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 DebuggingDirect link to 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 InterfaceDirect link to 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 ConsiderationsDirect link to 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

ConclusionDirect link to 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.