ai-automation
7 min read

Automating the Un-Automatable: Playwright for Creative Workflows

Playwright isn't just for testing. Learn how to automate creative workflows, batch processing, and authentication-heavy tasks with real examples from production tools.

Playwright is known for testing, but it’s a powerful automation tool for creative workflows beyond test suites. Many creative and repetitive tasks happen in browsers—automating them saves time and enables new workflows. This article explores Playwright patterns for creative workflows, authentication capture, batch processing, error handling, and real-world automation examples.

You script creativity. You automate the un-automatable. In this article, you’ll learn how to use Playwright for creative automation, from Adobe Firefly image generation to website analysis and screenshot capture. These aren’t theoretical examples—they’re production tools that demonstrate real automation patterns.

Beyond Testing: Creative Automation Use Cases

Playwright excels at browser automation, but its use cases extend far beyond testing. Here are three production examples that show different automation patterns.

FireWerk: Adobe Firefly Automation

FireWerk v2.0 automates Adobe Firefly for image and speech generation. The project uses Node.js (ESM), Playwright, Puppeteer, Express, CSV parsing, and dotenv. It demonstrates creative workflow automation where Playwright interacts with a web-based creative tool.

website-analyzer: Automated Analysis

website-analyzer performs automated accessibility and performance testing. It uses Node.js and Playwright to analyze websites across multiple devices, generating Markdown reports. This shows analysis automation—using Playwright to gather data and generate reports.

screenshot-tool-ui: Website Screenshot and Video Capture

screenshot-tool-ui captures website screenshots and videos using Playwright. It demonstrates capture automation—using Playwright to record visual content for documentation, testing, or archival purposes.

Why Playwright Over Other Tools?

Playwright offers several advantages for automation:

  • Cross-browser support: Works with Chromium, Firefox, and WebKit
  • Modern API: Async/await patterns, auto-waiting, and network interception
  • Auto-waiting: Automatically waits for elements to be ready
  • Network interception: Monitor and modify network requests
  • Parallel execution: Run multiple browsers simultaneously
  • Mobile emulation: Test and automate mobile experiences

These features make Playwright ideal for automation beyond testing.

FireWerk: Adobe Firefly Automation

FireWerk demonstrates creative workflow automation with Playwright. The project automates Adobe Firefly’s web interface for image and speech generation, handling authentication, batch processing, and error recovery.

Playwright Setup for Creative Tools

Setting up Playwright for creative tools requires persistent browser contexts to maintain authentication:

import { chromium } from 'playwright';

// Launch browser with persistent context
const browser = await chromium.launch({
  headless: false, // Show browser for creative tools
});

const context = await browser.newContext({
  // Persist authentication
  storageState: 'auth-state.json',
});

const page = await context.newPage();

Authentication Capture and Persistence

Creative tools often require authentication. FireWerk captures and persists authentication state:

// Capture authentication
async function captureAuth() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // Navigate to login page
  await page.goto('https://firefly.adobe.com');
  
  // Perform login (manual or automated)
  await page.fill('#email', process.env.ADOBE_EMAIL);
  await page.fill('#password', process.env.ADOBE_PASSWORD);
  await page.click('button[type="submit"]');
  
  // Wait for authentication
  await page.waitForURL('https://firefly.adobe.com/**');
  
  // Save authentication state
  await context.storageState({ path: 'auth-state.json' });
  
  await browser.close();
}

Image Generation Automation Workflow

FireWerk automates image generation by interacting with Firefly’s interface:

async function generateImage(prompt) {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext({
    storageState: 'auth-state.json',
  });
  const page = await context.newPage();
  
  await page.goto('https://firefly.adobe.com/generate');
  
  // Enter prompt
  await page.fill('textarea[placeholder*="prompt"]', prompt);
  
  // Click generate button
  await page.click('button:has-text("Generate")');
  
  // Wait for generation to complete
  await page.waitForSelector('.generated-image', { timeout: 60000 });
  
  // Download generated image
  const imageUrl = await page.getAttribute('.generated-image', 'src');
  const response = await page.goto(imageUrl);
  await fs.writeFile(`output/${prompt}.png`, await response.body());
  
  await browser.close();
}

Speech Synthesis Automation

FireWerk also automates speech synthesis:

async function generateSpeech(text) {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext({
    storageState: 'auth-state.json',
  });
  const page = await context.newPage();
  
  await page.goto('https://firefly.adobe.com/speech');
  
  // Enter text
  await page.fill('textarea', text);
  
  // Select voice
  await page.selectOption('select[name="voice"]', 'en-US-Female');
  
  // Generate speech
  await page.click('button:has-text("Generate Speech")');
  
  // Wait for audio
  await page.waitForSelector('audio[src]');
  
  // Download audio
  const audioUrl = await page.getAttribute('audio', 'src');
  const response = await page.goto(audioUrl);
  await fs.writeFile(`output/${text}.mp3`, await response.body());
  
  await browser.close();
}

Batch Processing from CSV Files

FireWerk processes batches from CSV files:

import { parse } from 'csv-parse/sync';
import fs from 'fs';

async function processBatch(csvPath) {
  const csvContent = fs.readFileSync(csvPath, 'utf-8');
  const records = parse(csvContent, {
    columns: true,
    skip_empty_lines: true,
  });
  
  for (const record of records) {
    try {
      console.log(`Processing: ${record.prompt}`);
      
      if (record.type === 'image') {
        await generateImage(record.prompt);
      } else if (record.type === 'speech') {
        await generateSpeech(record.text);
      }
      
      // Progress tracking
      console.log(`✓ Completed: ${record.prompt}`);
    } catch (error) {
      console.error(`✗ Failed: ${record.prompt}`, error.message);
      // Continue with next item
    }
  }
}

Web UI Integration

FireWerk includes an Express server for web UI control:

import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/generate', async (req, res) => {
  const { prompt, type } = req.body;
  
  try {
    if (type === 'image') {
      await generateImage(prompt);
      res.json({ success: true, message: 'Image generated' });
    } else if (type === 'speech') {
      await generateSpeech(prompt);
      res.json({ success: true, message: 'Speech generated' });
    }
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

app.listen(3000, () => {
  console.log('FireWerk server running on http://localhost:3000');
});

Error Handling and Retry Strategies

Creative automation needs robust error handling:

async function generateWithRetry(prompt, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await generateImage(prompt);
      return; // Success
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
      }
      
      // Exponential backoff
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

Progress Tracking and Logging

Track progress for long-running batch operations:

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'firewerk.log' }),
  ],
});

async function processBatchWithLogging(csvPath) {
  const records = parse(fs.readFileSync(csvPath, 'utf-8'), {
    columns: true,
    skip_empty_lines: true,
  });
  
  const total = records.length;
  let completed = 0;
  
  for (const record of records) {
    try {
      logger.info(`Processing ${completed + 1}/${total}: ${record.prompt}`);
      
      await generateImage(record.prompt);
      
      completed++;
      logger.info(`Progress: ${completed}/${total} (${(completed/total*100).toFixed(1)}%)`);
    } catch (error) {
      logger.error(`Failed: ${record.prompt}`, error);
    }
  }
  
  logger.info(`Batch complete: ${completed}/${total} succeeded`);
}

Network Interception for Monitoring

Monitor network requests to understand API calls:

async function generateWithMonitoring(prompt) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  // Monitor network requests
  page.on('request', request => {
    if (request.url().includes('api')) {
      console.log('API Request:', request.method(), request.url());
    }
  });
  
  page.on('response', response => {
    if (response.url().includes('api')) {
      console.log('API Response:', response.status(), response.url());
    }
  });
  
  // Perform generation
  await page.goto('https://firefly.adobe.com/generate');
  // ... rest of generation code
}

website-analyzer: Automated Analysis

website-analyzer demonstrates analysis automation with Playwright. It performs accessibility audits, responsive testing, and performance metrics collection.

Multi-Device Viewport Configuration

Test across multiple devices:

const devices = [
  { name: 'Desktop', viewport: { width: 1920, height: 1080 } },
  { name: 'Tablet', viewport: { width: 768, height: 1024 } },
  { name: 'Mobile', viewport: { width: 375, height: 667 } },
];

async function analyzeWebsite(url) {
  const browser = await chromium.launch();
  const results = [];
  
  for (const device of devices) {
    const context = await browser.newContext({
      viewport: device.viewport,
    });
    const page = await context.newPage();
    
    await page.goto(url);
    
    const analysis = await analyzePage(page, device.name);
    results.push(analysis);
    
    await context.close();
  }
  
  await browser.close();
  return results;
}

Accessibility Audit Implementation

Check accessibility issues:

async function analyzePage(page, deviceName) {
  const issues = [];
  
  // Check for missing alt text
  const images = await page.$$('img');
  for (const img of images) {
    const alt = await img.getAttribute('alt');
    if (!alt) {
      issues.push({
        type: 'accessibility',
        severity: 'error',
        message: 'Image missing alt text',
        element: await img.evaluate(el => el.outerHTML),
      });
    }
  }
  
  // Check for semantic HTML
  const headings = await page.$$('h1, h2, h3, h4, h5, h6');
  if (headings.length === 0) {
    issues.push({
      type: 'accessibility',
      severity: 'warning',
      message: 'No headings found',
    });
  }
  
  // Check keyboard navigation
  const focusableElements = await page.$$('a, button, input, select, textarea');
  for (const element of focusableElements) {
    const tabIndex = await element.getAttribute('tabindex');
    if (tabIndex === '-1') {
      issues.push({
        type: 'accessibility',
        severity: 'warning',
        message: 'Focusable element with tabindex="-1"',
      });
    }
  }
  
  return {
    device: deviceName,
    issues,
    issueCount: issues.length,
  };
}

Performance Metrics Collection

Collect performance metrics:

async function collectPerformanceMetrics(page) {
  // Navigate and wait for load
  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  
  // Get performance metrics
  const metrics = await page.evaluate(() => {
    const perfData = performance.getEntriesByType('navigation')[0];
    return {
      domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
      loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
      firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0,
      firstContentfulPaint: performance.getEntriesByType('paint')[1]?.startTime || 0,
    };
  });
  
  // Get resource sizes
  const resources = await page.evaluate(() => {
    return performance.getEntriesByType('resource').map(resource => ({
      name: resource.name,
      size: resource.transferSize,
      duration: resource.duration,
    }));
  });
  
  return {
    metrics,
    resources,
    totalSize: resources.reduce((sum, r) => sum + r.size, 0),
  };
}

Screenshot Capture with Full-Page Scrolling

Capture full-page screenshots:

async function captureFullPageScreenshot(page, outputPath) {
  // Get page dimensions
  const dimensions = await page.evaluate(() => {
    return {
      width: document.documentElement.scrollWidth,
      height: document.documentElement.scrollHeight,
    };
  });
  
  // Set viewport to full page
  await page.setViewportSize({
    width: dimensions.width,
    height: dimensions.height,
  });
  
  // Capture screenshot
  await page.screenshot({
    path: outputPath,
    fullPage: true,
  });
}

Markdown Report Generation

Generate Markdown reports:

function generateMarkdownReport(results) {
  let markdown = '# Website Analysis Report\n\n';
  markdown += `Generated: ${new Date().toISOString()}\n\n`;
  
  for (const result of results) {
    markdown += `## ${result.device}\n\n`;
    markdown += `**Issues Found:** ${result.issueCount}\n\n`;
    
    if (result.issues.length > 0) {
      markdown += '### Issues\n\n';
      for (const issue of result.issues) {
        markdown += `- **${issue.severity.toUpperCase()}**: ${issue.message}\n`;
      }
      markdown += '\n';
    }
    
    if (result.metrics) {
      markdown += '### Performance Metrics\n\n';
      markdown += `- DOM Content Loaded: ${result.metrics.domContentLoaded}ms\n`;
      markdown += `- Load Complete: ${result.metrics.loadComplete}ms\n`;
      markdown += `- First Paint: ${result.metrics.firstPaint}ms\n`;
      markdown += `- First Contentful Paint: ${result.metrics.firstContentfulPaint}ms\n\n`;
    }
  }
  
  return markdown;
}

Authentication Handling Patterns

Authentication is crucial for automating authenticated workflows. Here are patterns for handling authentication with Playwright.

Session Persistence Strategies

Save and load browser contexts with authentication:

// Save authentication state
async function saveAuthState(context, path) {
  await context.storageState({ path });
}

// Load authentication state
async function loadAuthState(browser, path) {
  return await browser.newContext({
    storageState: path,
  });
}

Manage cookies explicitly:

// Save cookies
async function saveCookies(page, path) {
  const cookies = await page.context().cookies();
  await fs.writeFile(path, JSON.stringify(cookies, null, 2));
}

// Load cookies
async function loadCookies(context, path) {
  const cookies = JSON.parse(await fs.readFile(path, 'utf-8'));
  await context.addCookies(cookies);
}

Storage State Management

Storage state includes cookies, localStorage, and sessionStorage:

// Save complete storage state
async function saveStorageState(context, path) {
  await context.storageState({ path });
}

// Use storage state
const context = await browser.newContext({
  storageState: 'auth-state.json',
});

Environment Variable Usage for Credentials

Store credentials securely:

import dotenv from 'dotenv';
dotenv.config();

async function login(page) {
  await page.goto('https://example.com/login');
  await page.fill('#email', process.env.EMAIL);
  await page.fill('#password', process.env.PASSWORD);
  await page.click('button[type="submit"]');
  await page.waitForURL('https://example.com/**');
}

Secure Authentication Flow

Complete secure authentication flow:

async function authenticate(credentials) {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();
  
  try {
    await page.goto('https://example.com/login');
    
    // Fill login form
    await page.fill('#email', credentials.email);
    await page.fill('#password', credentials.password);
    await page.click('button[type="submit"]');
    
    // Wait for authentication
    await page.waitForURL('https://example.com/**', { timeout: 30000 });
    
    // Verify authentication
    const isAuthenticated = await page.evaluate(() => {
      return document.cookie.includes('session');
    });
    
    if (isAuthenticated) {
      // Save authentication state
      await context.storageState({ path: 'auth-state.json' });
      return true;
    } else {
      throw new Error('Authentication failed');
    }
  } finally {
    await browser.close();
  }
}

Batch Processing and Error Handling

Batch processing requires robust error handling and progress tracking.

CSV Parsing with Batch Processing Loop

Process CSV files in batches:

import { parse } from 'csv-parse/sync';

async function processBatch(csvPath, batchSize = 10) {
  const records = parse(await fs.readFile(csvPath, 'utf-8'), {
    columns: true,
    skip_empty_lines: true,
  });
  
  // Process in batches
  for (let i = 0; i < records.length; i += batchSize) {
    const batch = records.slice(i, i + batchSize);
    
    await Promise.all(
      batch.map(record => processRecord(record))
    );
    
    console.log(`Processed batch ${Math.floor(i / batchSize) + 1}`);
  }
}

Error Handling with Retry Logic

Implement retry logic with exponential backoff:

async function processWithRetry(record, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await processRecord(record);
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      
      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Progress Tracking Implementation

Track progress for long operations:

class ProgressTracker {
  constructor(total) {
    this.total = total;
    this.completed = 0;
    this.failed = 0;
  }
  
  increment(success = true) {
    if (success) {
      this.completed++;
    } else {
      this.failed++;
    }
    
    const percentage = ((this.completed + this.failed) / this.total * 100).toFixed(1);
    console.log(`Progress: ${this.completed + this.failed}/${this.total} (${percentage}%)`);
  }
  
  getStats() {
    return {
      completed: this.completed,
      failed: this.failed,
      remaining: this.total - this.completed - this.failed,
    };
  }
}

async function processWithProgress(records) {
  const tracker = new ProgressTracker(records.length);
  
  for (const record of records) {
    try {
      await processRecord(record);
      tracker.increment(true);
    } catch (error) {
      console.error(`Failed: ${record.id}`, error.message);
      tracker.increment(false);
    }
  }
  
  console.log('Final stats:', tracker.getStats());
}

Parallel Execution Pattern

Process multiple items in parallel:

import pLimit from 'p-limit';

async function processParallel(records, concurrency = 5) {
  const limit = pLimit(concurrency);
  
  const promises = records.map(record =>
    limit(async () => {
      try {
        return await processRecord(record);
      } catch (error) {
        console.error(`Failed: ${record.id}`, error.message);
        return null;
      }
    })
  );
  
  const results = await Promise.all(promises);
  return results.filter(r => r !== null);
}

Resource Cleanup

Clean up resources properly:

async function processWithCleanup(records) {
  const browser = await chromium.launch();
  
  try {
    for (const record of records) {
      const context = await browser.newContext();
      const page = await context.newPage();
      
      try {
        await processRecord(page, record);
      } finally {
        await context.close(); // Clean up context
      }
    }
  } finally {
    await browser.close(); // Clean up browser
  }
}

Automation Workflow

Here’s a visual representation of the automation workflow:

sequenceDiagram
    participant User
    participant Script
    participant Playwright
    participant Browser
    participant Target

    User->>Script: Start automation
    Script->>Playwright: Launch browser
    Playwright->>Browser: Create context
    Browser->>Target: Navigate to site
    
    alt Authentication Required
        Browser->>Target: Login
        Target-->>Browser: Set cookies/storage
        Browser->>Script: Save auth state
    end
    
    loop Batch Processing
        Script->>Browser: Process item
        Browser->>Target: Interact with page
        Target-->>Browser: Response
        Browser->>Script: Extract data
        
        alt Error Occurs
            Script->>Script: Retry with backoff
        end
    end
    
    Script->>Script: Generate report
    Script->>User: Complete

Common Pitfalls

Not Handling Authentication Properly

Losing session or not persisting authentication state breaks automation. Always save and load authentication state.

Ignoring Error Recovery

One failure shouldn’t break the entire batch. Implement error recovery and continue processing.

Over-Automating Fragile Workflows

Brittle selectors and timing issues make automation unreliable. Use stable selectors and proper waiting.

Security Concerns with Credentials

Never commit secrets. Use environment variables and secure storage for credentials.

Not Using Auto-Waiting

Adding arbitrary timeouts instead of using Playwright’s auto-waiting leads to flaky automation. Use waitForSelector and other waiting methods.

Ignoring Resource Cleanup

Memory leaks and zombie processes occur without proper cleanup. Always close contexts and browsers.

Not Handling Network Failures

Timeouts and connection errors need handling. Implement retry logic for network operations.

Using Fragile Selectors

Relying on implementation details instead of stable attributes breaks when the page changes. Use data attributes and semantic selectors.

Conclusion

Playwright enables automation beyond testing, from creative workflows to batch processing and analysis. The three examples show different automation patterns: creative generation (FireWerk), analysis (website-analyzer), and capture (screenshot-tool-ui).

Key takeaways: Creative workflow automation with Playwright, authentication handling and persistence, batch processing patterns with error recovery, and real-world production examples. Playwright’s modern API, auto-waiting, and cross-browser support make it ideal for automation tasks.

Identify repetitive browser tasks in your workflow and automate them with Playwright. Start with a simple script, add authentication handling, then scale to batch processing. Automation saves time and enables new workflows that wouldn’t be possible manually.

#Playwright #Automation #Browser Automation #Creative Workflows #Node.js
Share: