Multilogin Advanced Automation Techniques: Complete Guide 2025

Master advanced automation techniques in Multilogin to streamline your multi-account operations. Learn API integration, custom scripting, workflow automation, and advanced multi-account management strategies.

Introduction to Advanced Automation

Why Advanced Automation Matters

Operational efficiency:

  • Scale operations: Handle hundreds of accounts simultaneously
  • Reduce manual work: Automate repetitive tasks and processes
  • Minimize errors: Consistent execution across all accounts
  • Increase productivity: Focus on strategy rather than manual execution

Business benefits:

  • Cost reduction: Lower operational costs through automation
  • Risk mitigation: Reduce human error and account detection
  • Competitive advantage: Scale operations faster than competitors
  • Revenue growth: Handle more accounts and generate more income

Multilogin Automation Capabilities

Core automation features:

  • API integration: RESTful API for programmatic access
  • Custom scripting: JavaScript and automation scripts
  • Workflow automation: Pre-built and custom workflows
  • Third-party integrations: Connect with external tools and services

Advanced capabilities:

  • Profile management automation: Bulk operations and profile templates
  • Proxy rotation automation: Dynamic proxy management
  • Cookie and session management: Automated session handling
  • Task scheduling: Time-based and event-driven automation

API Integration Fundamentals

Understanding Multilogin API

API architecture:

  • RESTful design: Standard HTTP methods and JSON responses
  • Authentication: Secure API key and token-based authentication
  • Rate limiting: Built-in rate limits to prevent abuse
  • Documentation: Comprehensive API documentation and examples

Key API endpoints:

  • Profile management: Create, update, delete, and manage profiles
  • Browser automation: Launch browsers and execute scripts
  • Proxy management: Configure and rotate proxies
  • Account monitoring: Track profile usage and performance

Setting Up API Access

API key generation:

  1. Log into your Multilogin dashboard
  2. Navigate to API settings section
  3. Generate new API key with appropriate permissions
  4. Store API key securely (never expose in code)

Authentication setup:

const API_KEY = 'your-api-key-here';
const BASE_URL = 'https://api.multilogin.com/v1';

// Authentication headers
const headers = {
  'Authorization': `Bearer ${API_KEY}`,
  'Content-Type': 'application/json'
};

Testing API connection:

// Test API connectivity
async function testAPIConnection() {
  try {
    const response = await fetch(`${BASE_URL}/profile/list`, {
      method: 'GET',
      headers: headers
    });
    
    if (response.ok) {
      console.log('API connection successful');
      return true;
    } else {
      console.error('API connection failed');
      return false;
    }
  } catch (error) {
    console.error('API error:', error);
    return false;
  }
}

Profile Management Automation

Bulk Profile Operations

Creating multiple profiles:

async function createBulkProfiles(profileData) {
  const results = [];
  
  for (const data of profileData) {
    try {
      const response = await fetch(`${BASE_URL}/profile/create`, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify({
          name: data.name,
          os: data.os,
          browser: data.browser,
          proxy: data.proxy,
          fingerprint: data.fingerprint
        })
      });
      
      const result = await response.json();
      results.push(result);
      
      // Rate limiting - wait between requests
      await new Promise(resolve => setTimeout(resolve, 1000));
      
    } catch (error) {
      console.error(`Failed to create profile ${data.name}:`, error);
      results.push({ error: error.message });
    }
  }
  
  return results;
}

Profile template application:

async function applyProfileTemplate(templateId, profileIds) {
  const results = [];
  
  for (const profileId of profileIds) {
    try {
      const response = await fetch(`${BASE_URL}/profile/${profileId}/apply-template`, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify({
          templateId: templateId,
          overrideExisting: false
        })
      });
      
      const result = await response.json();
      results.push(result);
      
    } catch (error) {
      console.error(`Failed to apply template to profile ${profileId}:`, error);
      results.push({ error: error.message });
    }
  }
  
  return results;
}

Automated Profile Maintenance

Profile health monitoring:

async function monitorProfileHealth(profileIds) {
  const healthReport = [];
  
  for (const profileId of profileIds) {
    try {
      const response = await fetch(`${BASE_URL}/profile/${profileId}/health`, {
        method: 'GET',
        headers: headers
      });
      
      const health = await response.json();
      healthReport.push({
        profileId: profileId,
        status: health.status,
        lastUsed: health.lastUsed,
        issues: health.issues || []
      });
      
    } catch (error) {
      healthReport.push({
        profileId: profileId,
        status: 'error',
        error: error.message
      });
    }
  }
  
  return healthReport;
}

Automated profile cleanup:

async function cleanupInactiveProfiles(daysInactive = 30) {
  try {
    // Get all profiles
    const listResponse = await fetch(`${BASE_URL}/profile/list`, {
      method: 'GET',
      headers: headers
    });
    
    const profiles = await listResponse.json();
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysInactive);
    
    const inactiveProfiles = profiles.filter(profile => 
      new Date(profile.lastUsed) < cutoffDate
    );
    
    // Archive inactive profiles
    const archiveResults = [];
    for (const profile of inactiveProfiles) {
      const archiveResponse = await fetch(`${BASE_URL}/profile/${profile.id}/archive`, {
        method: 'POST',
        headers: headers
      });
      
      archiveResults.push(await archiveResponse.json());
    }
    
    return {
      archived: archiveResults.length,
      profiles: inactiveProfiles.map(p => p.name)
    };
    
  } catch (error) {
    console.error('Cleanup error:', error);
    return { error: error.message };
  }
}

Browser Automation Scripts

Custom Script Development

Script structure:

// Multilogin automation script template
class MultiloginAutomation {
  constructor(profileId, config = {}) {
    this.profileId = profileId;
    this.config = {
      timeout: 30000,
      retries: 3,
      ...config
    };
  }
  
  async execute(task) {
    let attempts = 0;
    while (attempts < this.config.retries) {
      try {
        const result = await this.runTask(task);
        return result;
      } catch (error) {
        attempts++;
        if (attempts >= this.config.retries) {
          throw error;
        }
        await this.delay(1000 * attempts); // Exponential backoff
      }
    }
  }
  
  async runTask(task) {
    // Task execution logic
    switch (task.type) {
      case 'login':
        return await this.performLogin(task);
      case 'scrape':
        return await this.performScraping(task);
      case 'interact':
        return await this.performInteraction(task);
      default:
        throw new Error(`Unknown task type: ${task.type}`);
    }
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Login automation script:

async function performLogin(task) {
  const { url, username, password, selectors } = task;
  
  // Launch browser with profile
  const browser = await this.launchBrowser();
  const page = await browser.newPage();
  
  try {
    // Navigate to login page
    await page.goto(url, { waitUntil: 'networkidle2' });
    
    // Wait for login form
    await page.waitForSelector(selectors.username, { timeout: 10000 });
    
    // Fill login credentials
    await page.type(selectors.username, username);
    await page.type(selectors.password, password);
    
    // Submit form
    await page.click(selectors.submitButton);
    
    // Wait for successful login
    await page.waitForSelector(selectors.successIndicator, { timeout: 15000 });
    
    return {
      success: true,
      message: 'Login successful',
      cookies: await page.cookies(),
      localStorage: await page.evaluate(() => {
        const items = {};
        for (let i = 0; i < localStorage.length; i++) {
          const key = localStorage.key(i);
          items[key] = localStorage.getItem(key);
        }
        return items;
      })
    };
    
  } catch (error) {
    return {
      success: false,
      error: error.message,
      screenshot: await page.screenshot({ encoding: 'base64' })
    };
  } finally {
    await browser.close();
  }
}

Data Extraction Automation

Web scraping script:

async function performScraping(task) {
  const { url, selectors, pagination } = task;
  
  const browser = await this.launchBrowser();
  const page = await browser.newPage();
  const results = [];
  
  try {
    await page.goto(url, { waitUntil: 'networkidle2' });
    
    let hasNextPage = true;
    let pageNum = 1;
    
    while (hasNextPage && pageNum <= (pagination?.maxPages || 10)) {
      // Extract data from current page
      const pageData = await page.evaluate((selectors) => {
        const items = [];
        const elements = document.querySelectorAll(selectors.item);
        
        elements.forEach(element => {
          const item = {};
          
          // Extract fields based on selectors
          Object.keys(selectors.fields).forEach(field => {
            const selector = selectors.fields[field];
            const el = element.querySelector(selector);
            item[field] = el ? el.textContent.trim() : null;
          });
          
          items.push(item);
        });
        
        return items;
      }, selectors);
      
      results.push(...pageData);
      
      // Check for next page
      if (pagination?.nextButton) {
        const nextButton = await page.$(pagination.nextButton);
        if (nextButton) {
          await nextButton.click();
          await page.waitForTimeout(2000);
          pageNum++;
        } else {
          hasNextPage = false;
        }
      } else {
        hasNextPage = false;
      }
    }
    
    return {
      success: true,
      data: results,
      pagesScraped: pageNum,
      totalItems: results.length
    };
    
  } catch (error) {
    return {
      success: false,
      error: error.message,
      partialData: results
    };
  } finally {
    await browser.close();
  }
}

Workflow Automation

Building Automation Workflows

Workflow definition:

const automationWorkflow = {
  name: 'E-commerce Account Management',
  description: 'Complete workflow for managing multiple e-commerce accounts',
  steps: [
    {
      id: 'login',
      type: 'login',
      config: {
        url: 'https://seller-platform.com/login',
        selectors: {
          username: '#username',
          password: '#password',
          submitButton: '#login-btn',
          successIndicator: '.dashboard'
        }
      },
      next: 'check_inventory'
    },
    {
      id: 'check_inventory',
      type: 'scrape',
      config: {
        selectors: {
          item: '.product-row',
          fields: {
            name: '.product-name',
            stock: '.stock-level',
            price: '.current-price'
          }
        }
      },
      next: 'update_pricing'
    },
    {
      id: 'update_pricing',
      type: 'interact',
      config: {
        actions: [
          {
            type: 'click',
            selector: '.edit-price-btn',
            wait: 1000
          },
          {
            type: 'type',
            selector: '.price-input',
            value: '{{newPrice}}',
            wait: 500
          },
          {
            type: 'click',
            selector: '.save-btn',
            wait: 2000
          }
        ]
      },
      next: 'generate_report'
    },
    {
      id: 'generate_report',
      type: 'report',
      config: {
        template: 'inventory_report',
        format: 'pdf',
        recipients: ['[email protected]']
      }
    }
  ],
  errorHandling: {
    maxRetries: 3,
    retryDelay: 5000,
    fallbackActions: ['notify_admin', 'log_error']
  }
};

Workflow execution engine:

class WorkflowEngine {
  constructor(workflow, profileManager) {
    this.workflow = workflow;
    this.profileManager = profileManager;
    this.results = {};
    this.currentStep = null;
  }
  
  async execute(profileId, initialData = {}) {
    this.results = { ...initialData };
    
    for (const step of this.workflow.steps) {
      this.currentStep = step;
      
      try {
        console.log(`Executing step: ${step.id}`);
        
        const stepResult = await this.executeStep(step, profileId);
        this.results[step.id] = stepResult;
        
        if (!stepResult.success) {
          await this.handleStepFailure(step, stepResult);
          break;
        }
        
        // Check if workflow should continue
        if (step.next && !this.shouldContinue(step, stepResult)) {
          break;
        }
        
      } catch (error) {
        console.error(`Step ${step.id} failed:`, error);
        await this.handleStepError(step, error);
        break;
      }
    }
    
    return {
      success: this.isWorkflowSuccessful(),
      results: this.results,
      executionTime: Date.now() - initialData.startTime
    };
  }
  
  async executeStep(step, profileId) {
    const automation = new MultiloginAutomation(profileId);
    return await automation.execute(step);
  }
  
  shouldContinue(step, result) {
    // Custom logic to determine if workflow should continue
    return result.success;
  }
  
  isWorkflowSuccessful() {
    // Check if all critical steps succeeded
    const criticalSteps = this.workflow.steps.filter(s => s.critical !== false);
    return criticalSteps.every(step => 
      this.results[step.id]?.success
    );
  }
}

Advanced Proxy Management

Dynamic Proxy Rotation

Proxy rotation strategy:

class ProxyRotator {
  constructor(proxyPool) {
    this.proxyPool = proxyPool;
    this.usageStats = new Map();
    this.failureThreshold = 3;
  }
  
  async getOptimalProxy(profileId, targetSite) {
    // Filter available proxies
    const availableProxies = this.proxyPool.filter(proxy => 
      proxy.status === 'active' && 
      !this.isProxyBlocked(proxy, targetSite)
    );
    
    if (availableProxies.length === 0) {
      throw new Error('No available proxies for target site');
    }
    
    // Score proxies based on performance
    const scoredProxies = availableProxies.map(proxy => ({
      ...proxy,
      score: this.calculateProxyScore(proxy, targetSite)
    }));
    
    // Return highest scoring proxy
    scoredProxies.sort((a, b) => b.score - a.score);
    return scoredProxies[0];
  }
  
  calculateProxyScore(proxy, targetSite) {
    let score = 100;
    
    // Reduce score for recent failures
    const recentFailures = this.usageStats.get(proxy.id)?.recentFailures || 0;
    score -= recentFailures * 20;
    
    // Reduce score for high usage
    const usageCount = this.usageStats.get(proxy.id)?.usageCount || 0;
    score -= Math.min(usageCount * 2, 30);
    
    // Boost score for geographic relevance
    if (proxy.country === targetSite.expectedCountry) {
      score += 15;
    }
    
    // Boost score for residential proxies
    if (proxy.type === 'residential') {
      score += 10;
    }
    
    return Math.max(score, 0);
  }
  
  async reportProxyUsage(proxyId, success, targetSite) {
    const stats = this.usageStats.get(proxyId) || {
      usageCount: 0,
      recentFailures: 0,
      lastUsed: null,
      blockedSites: new Set()
    };
    
    stats.usageCount++;
    stats.lastUsed = new Date();
    
    if (!success) {
      stats.recentFailures++;
      
      // Mark as potentially blocked
      if (stats.recentFailures >= this.failureThreshold) {
        stats.blockedSites.add(targetSite);
      }
    } else {
      // Reset failure count on success
      stats.recentFailures = 0;
    }
    
    this.usageStats.set(proxyId, stats);
  }
  
  isProxyBlocked(proxy, targetSite) {
    const stats = this.usageStats.get(proxy.id);
    return stats?.blockedSites?.has(targetSite) || false;
  }
}

Automated proxy health checking:

async function checkProxyHealth(proxyList) {
  const results = [];
  
  for (const proxy of proxyList) {
    try {
      const startTime = Date.now();
      
      // Test proxy connectivity
      const testResponse = await fetch('https://httpbin.org/ip', {
        proxy: {
          host: proxy.host,
          port: proxy.port,
          auth: proxy.auth ? {
            username: proxy.username,
            password: proxy.password
          } : undefined
        },
        timeout: 10000
      });
      
      const responseTime = Date.now() - startTime;
      const data = await testResponse.json();
      
      results.push({
        proxyId: proxy.id,
        status: 'active',
        responseTime: responseTime,
        detectedIP: data.origin,
        expectedIP: proxy.expectedIP,
        ipMatch: data.origin === proxy.expectedIP
      });
      
    } catch (error) {
      results.push({
        proxyId: proxy.id,
        status: 'inactive',
        error: error.message
      });
    }
  }
  
  return results;
}

Integration with Third-Party Tools

CRM Integration

Salesforce integration:

class SalesforceIntegration {
  constructor(sfConfig) {
    this.sf = new jsforce.Connection({
      loginUrl: sfConfig.loginUrl,
      clientId: sfConfig.clientId,
      clientSecret: sfConfig.clientSecret
    });
  }
  
  async syncAccountData(profileData) {
    try {
      await this.sf.login(sfConfig.username, sfConfig.password);
      
      const accounts = await this.sf.query(
        'SELECT Id, Name, Website FROM Account WHERE Website != null'
      );
      
      const syncResults = [];
      
      for (const account of accounts.records) {
        // Find matching profile
        const matchingProfile = profileData.find(p => 
          p.website === account.Website
        );
        
        if (matchingProfile) {
          // Update Salesforce with profile data
          await this.sf.sobject('Account').update({
            Id: account.Id,
            Custom_Status__c: matchingProfile.status,
            Last_Profile_Update__c: new Date().toISOString()
          });
          
          syncResults.push({
            accountId: account.Id,
            profileId: matchingProfile.id,
            status: 'synced'
          });
        }
      }
      
      return syncResults;
      
    } catch (error) {
      console.error('Salesforce sync error:', error);
      throw error;
    }
  }
}

Analytics Integration

Google Analytics automation:

async function setupAnalyticsTracking(profileId, gaConfig) {
  const automation = new MultiloginAutomation(profileId);
  
  const script = `
    // Inject Google Analytics
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
    
    ga('create', '${gaConfig.trackingId}', 'auto');
    ga('set', 'anonymizeIp', true);
    ga('send', 'pageview');
    
    // Track custom events
    window.trackEvent = function(category, action, label) {
      ga('send', 'event', category, action, label);
    };
  `;
  
  return await automation.execute({
    type: 'inject_script',
    script: script
  });
}

Monitoring and Analytics

Performance Monitoring

Automation metrics collection:

class AutomationMonitor {
  constructor() {
    this.metrics = {
      totalExecutions: 0,
      successfulExecutions: 0,
      failedExecutions: 0,
      averageExecutionTime: 0,
      errorRate: 0,
      profilePerformance: new Map()
    };
  }
  
  recordExecution(profileId, success, executionTime, error = null) {
    this.metrics.totalExecutions++;
    
    if (success) {
      this.metrics.successfulExecutions++;
    } else {
      this.metrics.failedExecutions++;
      this.recordError(profileId, error);
    }
    
    // Update average execution time
    const totalTime = this.metrics.averageExecutionTime * (this.metrics.totalExecutions - 1);
    this.metrics.averageExecutionTime = (totalTime + executionTime) / this.metrics.totalExecutions;
    
    // Update error rate
    this.metrics.errorRate = this.metrics.failedExecutions / this.metrics.totalExecutions;
    
    // Profile-specific metrics
    const profileStats = this.metrics.profilePerformance.get(profileId) || {
      executions: 0,
      successes: 0,
      failures: 0,
      avgTime: 0
    };
    
    profileStats.executions++;
    if (success) {
      profileStats.successes++;
    } else {
      profileStats.failures++;
    }
    
    const profileTotalTime = profileStats.avgTime * (profileStats.executions - 1);
    profileStats.avgTime = (profileTotalTime + executionTime) / profileStats.executions;
    
    this.metrics.profilePerformance.set(profileId, profileStats);
  }
  
  recordError(profileId, error) {
    // Log error for analysis
    console.error(`Profile ${profileId} error:`, error);
    
    // Could send to monitoring service
    // this.sendToMonitoringService(profileId, error);
  }
  
  getMetrics() {
    return {
      ...this.metrics,
      profilePerformance: Object.fromEntries(this.metrics.profilePerformance)
    };
  }
}

Alert System

Automated alerting:

class AlertSystem {
  constructor(config) {
    this.config = config;
    this.alertHistory = [];
  }
  
  async checkAndAlert(metrics) {
    const alerts = [];
    
    // Check error rate threshold
    if (metrics.errorRate > this.config.errorRateThreshold) {
      alerts.push({
        type: 'error_rate',
        severity: 'high',
        message: `Error rate ${metrics.errorRate.toFixed(2)} exceeds threshold ${this.config.errorRateThreshold}`,
        data: { errorRate: metrics.errorRate }
      });
    }
    
    // Check execution time threshold
    if (metrics.averageExecutionTime > this.config.executionTimeThreshold) {
      alerts.push({
        type: 'performance',
        severity: 'medium',
        message: `Average execution time ${metrics.averageExecutionTime}ms exceeds threshold`,
        data: { avgTime: metrics.averageExecutionTime }
      });
    }
    
    // Check profile-specific issues
    for (const [profileId, stats] of Object.entries(metrics.profilePerformance)) {
      const profileErrorRate = stats.failures / stats.executions;
      
      if (profileErrorRate > this.config.profileErrorThreshold) {
        alerts.push({
          type: 'profile_error',
          severity: 'medium',
          message: `Profile ${profileId} error rate ${profileErrorRate.toFixed(2)} is high`,
          data: { profileId, errorRate: profileErrorRate }
        });
      }
    }
    
    // Send alerts
    for (const alert of alerts) {
      await this.sendAlert(alert);
      this.alertHistory.push({
        ...alert,
        timestamp: new Date()
      });
    }
    
    return alerts;
  }
  
  async sendAlert(alert) {
    // Send to configured channels (email, Slack, etc.)
    switch (this.config.alertChannel) {
      case 'email':
        await this.sendEmailAlert(alert);
        break;
      case 'slack':
        await this.sendSlackAlert(alert);
        break;
      case 'webhook':
        await this.sendWebhookAlert(alert);
        break;
    }
  }
}

Best Practices and Optimization

Performance Optimization

Script optimization techniques:

  • Minimize DOM interactions: Cache element references
  • Use efficient selectors: Prefer IDs over complex selectors
  • Implement proper waiting: Use specific waits instead of fixed delays
  • Handle rate limiting: Implement exponential backoff
  • Optimize resource usage: Close browsers and clean up resources

Code optimization example:

// Optimized script with best practices
async function optimizedAutomation(profileId, tasks) {
  const browser = await launchBrowser(profileId);
  const context = await browser.newContext();
  const results = [];
  
  try {
    for (const task of tasks) {
      const page = await context.newPage();
      
      try {
        // Use specific waits
        await page.goto(task.url, { waitUntil: 'domcontentloaded' });
        
        // Cache element references
        const elements = await page.$$(task.selector);
        
        for (const element of elements) {
          // Batch operations when possible
          const data = await element.evaluate(el => ({
            text: el.textContent,
            href: el.href,
            className: el.className
          }));
          
          results.push(data);
        }
        
      } finally {
        await page.close();
      }
      
      // Rate limiting
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
    
  } finally {
    await context.close();
    await browser.close();
  }
  
  return results;
}

Error Handling and Recovery

Robust error handling:

class ResilientAutomation {
  constructor(maxRetries = 3, backoffMultiplier = 2) {
    this.maxRetries = maxRetries;
    this.backoffMultiplier = backoffMultiplier;
  }
  
  async executeWithRetry(operation, context = {}) {
    let lastError;
    
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await operation(context);
      } catch (error) {
        lastError = error;
        
        console.warn(`Attempt ${attempt} failed:`, error.message);
        
        if (attempt < this.maxRetries) {
          const delay = Math.pow(this.backoffMultiplier, attempt - 1) * 1000;
          console.log(`Retrying in ${delay}ms...`);
          await new Promise(resolve => setTimeout(resolve, delay));
          
          // Optional: refresh context or reset state
          await this.resetContext(context);
        }
      }
    }
    
    throw new Error(`Operation failed after ${this.maxRetries} attempts. Last error: ${lastError.message}`);
  }
  
  async resetContext(context) {
    // Reset browser state, clear cookies, etc.
    if (context.page) {
      await context.page.reload();
    }
  }
}

Security Considerations

Secure automation practices:

  • Never store credentials in code: Use environment variables or secure vaults
  • Implement proper logging: Avoid logging sensitive information
  • Use HTTPS: Ensure all communications are encrypted
  • Regular security audits: Review and update security measures
  • Access controls: Implement proper API key management

Conclusion

Advanced automation in Multilogin transforms how you manage multiple accounts. By mastering API integration, custom scripting, workflow automation, and monitoring, you can achieve unprecedented efficiency and scale.

Key takeaways:

  • API integration: Build powerful integrations with external systems
  • Custom scripting: Create tailored automation for your specific needs
  • Workflow automation: Streamline complex multi-step processes
  • Monitoring and analytics: Track performance and identify optimization opportunities
  • Best practices: Implement robust error handling and security measures

Next steps:

  1. Start with basic API integration
  2. Build custom scripts for your use cases
  3. Implement workflow automation
  4. Set up monitoring and alerting
  5. Continuously optimize and improve
Start Advanced Automation →

Resources