Use Case: RPA Workflow Automation

RPA (Robotic Process Automation) commonly replaces repetitive human tasks: periodically logging into internal systems to submit data, aggregating reports from multiple platforms, automatically submitting approval forms, etc. Browser Forest's keepAlive mode keeps browser Sessions long-lived, and with Live View real-time monitoring, it enables fully observable unattended automation workflows.

keepAlive Mode: Long-Lived Sessions

Regular Sessions have an idle timeout (default 60 seconds), suitable for short tasks. In RPA workflows, there may be long waits between operations — setting keepAlive: true makes the Session immune to idle timeout, only terminating when the absolute duration (timeoutSeconds) is reached or when explicitly destroyed.

const res = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: {
    'X-API-Key': 'bf_live_xxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    keepAlive: true,            // Not affected by idle timeout
    timeoutSeconds: 7200,       // Max 2-hour absolute timeout
    userMetadata: {
      workflow: 'monthly-report-submission',
      operator: 'rpa-bot-01',
    },
  }),
});
const session = await res.json();
console.log('RPA Session:', session.id, 'keepAlive:', session.keepAlive);
Note: keepAlive only skips idle timeout checks — the absolute timeout (timeoutSeconds) still applies. For workflows exceeding 2 hours, consider splitting the large task into multiple Sessions executed in segments.

Typical Scenario: Periodic Report Aggregation and Submission

Business scenario: Every morning at dawn, scrape inventory data from multiple supplier portals, aggregate it, fill it into the internal ERP system, and submit for approval. The workflow involves login, pagination, data extraction, form filling, and submission — with potential waits for page loading or CAPTCHA handling in between.

import { chromium } from 'playwright';

const API_KEY = process.env.BF_API_KEY!;
const API_BASE = 'https://bf.mktindex.com/api';

async function dailyInventoryReport() {
  // 1. Create a long-lived Session
  const session = await fetch(`${API_BASE}/v1/sessions`, {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      keepAlive: true,
      timeoutSeconds: 3600,
      userMetadata: {
        workflow: 'daily-inventory',
        date: new Date().toISOString().split('T')[0],
      },
    }),
  }).then(r => r.json());

  const wsUrl = `wss://bf.mktindex.com/ws/session/${session.id}`;
  const browser = await chromium.connectOverCDP(wsUrl);
  const page = await browser.newPage();

  const inventoryData: Record<string, number> = {};

  try {
    // 2. Log into each supplier portal sequentially
    const suppliers = [
      { url: 'https://supplier-a.example.com', user: '[email protected]', pass: process.env.SUPPLIER_A_PASS! },
      { url: 'https://supplier-b.example.com', user: '[email protected]', pass: process.env.SUPPLIER_B_PASS! },
    ];

    for (const supplier of suppliers) {
      await page.goto(`${supplier.url}/login`);
      await page.fill('#email', supplier.user);
      await page.fill('#password', supplier.pass);
      await page.click('button[type=submit]');
      await page.waitForURL('**/dashboard');

      // 3. Paginate and scrape inventory data
      let hasNext = true;
      while (hasNext) {
        const rows = await page.$$eval('table.inventory tbody tr', (trs) =>
          trs.map(tr => ({
            sku: tr.querySelector('.sku')?.textContent?.trim() ?? '',
            qty: parseInt(tr.querySelector('.qty')?.textContent ?? '0'),
          }))
        );
        for (const row of rows) {
          inventoryData[row.sku] = (inventoryData[row.sku] ?? 0) + row.qty;
        }
        const nextBtn = await page.$('a.next-page:not(.disabled)');
        if (nextBtn) await nextBtn.click();
        else hasNext = false;
        await page.waitForTimeout(500);
      }

      // Log out, prepare for next supplier
      await page.click('#logout');
    }

    // 4. Log into internal ERP and submit summary data
    await page.goto('https://erp.internal.company.com/login');
    await page.fill('#username', 'rpa-bot');
    await page.fill('#password', process.env.ERP_PASS!);
    await page.click('#login-btn');
    await page.waitForURL('**/home');

    await page.goto('https://erp.internal.company.com/inventory/submit');
    for (const [sku, qty] of Object.entries(inventoryData)) {
      await page.fill(`input[data-sku="${sku}"]`, String(qty));
    }
    await page.click('#submit-report');
    await page.waitForSelector('.success-banner');
    console.log('Report submitted successfully');

  } finally {
    await browser.close();
    // 5. Task complete, destroy Session
    await fetch(`${API_BASE}/v1/sessions/${session.id}`, {
      method: 'DELETE',
      headers: { 'X-API-Key': API_KEY },
    });
  }
}

dailyInventoryReport().catch(console.error);

Live View Real-Time Monitoring

While an RPA workflow is running, open Live View in the Browser Forest console to see the browser screen in real time — making it easy to manually diagnose stuck or abnormal steps without logging into the server remotely.

FeatureDescription
Real-time ScreenBased on CDP Screencast, multi-frame per second refresh, sub-1-second latency
Log PanelShows Session event stream: created, active, idle, destroyed
CDP ConsoleSend CDP commands directly to manually intervene in abnormal states
Session InfoView keepAlive, userMetadata, proxy, fingerprint, and other config

Access Live View: click a Session row in the console Sessions list, or visit https://bf.mktindex.com/live?id={sessionId} directly.

Track Workflow State with userMetadata

Update userMetadata at key points in the workflow — you can query the current progress anytime via the GET Session endpoint, making it easy to integrate with external monitoring systems.

// Attach initial metadata on creation
const session = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    keepAlive: true,
    timeoutSeconds: 3600,
    userMetadata: {
      workflow: 'invoice-processing',
      status: 'initializing',
      totalInvoices: 0,
      processedInvoices: 0,
    },
  }),
}).then(r => r.json());

// Poll progress from an external monitoring system
async function pollProgress(sessionId: string) {
  const res = await fetch(`https://bf.mktindex.com/api/v1/sessions/${sessionId}`, {
    headers: { 'X-API-Key': API_KEY },
  });
  const s = await res.json();
  if (s.status === 'stopped') {
    console.log('Workflow completed');
    return;
  }
  console.log('Progress:', s.userMetadata?.processedInvoices, '/', s.userMetadata?.totalInvoices);
  setTimeout(() => pollProgress(sessionId), 5000);
}

Scheduled Task Integration

Wrap RPA scripts as scheduled tasks for fully automated unattended execution.

Node.js (using node-cron)

import cron from 'node-cron';

// Run inventory report daily at 2 AM
cron.schedule('0 2 * * *', async () => {
  console.log('Starting daily inventory RPA workflow...');
  try {
    await dailyInventoryReport();
    console.log('Workflow completed successfully');
  } catch (err) {
    console.error('Workflow failed:', err);
    // Send alert notification
    await sendAlert('daily-inventory', err);
  }
});

console.log('RPA scheduler started');

Linux crontab

# crontab -e
# Runs daily at 02:00, logs output
0 2 * * * cd /opt/rpa && node dist/daily-inventory.js >> /var/log/rpa/inventory.log 2>&1

Error Handling and Retry

Network jitter, page load timeouts, CAPTCHAs, and other issues can interrupt RPA workflows. We recommend wrapping retry logic and saving screenshots on failure for easier debugging.

import { chromium, Page } from 'playwright';

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  delayMs = 2000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxRetries) throw err;
      console.warn(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`, err);
      await new Promise(r => setTimeout(r, delayMs));
    }
  }
  throw new Error('unreachable');
}

async function safeLogin(page: Page, url: string, user: string, pass: string) {
  await withRetry(async () => {
    await page.goto(url, { timeout: 30_000 });
    await page.fill('#email', user);
    await page.fill('#password', pass);
    await page.click('button[type=submit]');
    await page.waitForURL('**/dashboard', { timeout: 15_000 });
  });
}

// Save screenshot on failure for debugging
async function captureFailureScreenshot(page: Page, step: string) {
  const filename = `failure-${step}-${Date.now()}.png`;
  await page.screenshot({ path: filename, fullPage: true });
  console.error(`Screenshot saved: ${filename}`);
}

API Parameter Reference

ParameterTypeDefaultDescription
keepAlivebooleanfalseWhen true, skips idle timeout checks — Session remains alive long-term
timeoutSecondsinteger300Absolute Session lifetime ceiling (seconds); set a high value for keepAlive
userMetadataobjectnullCustom key-value pairs for storing workflow name, status, progress, etc.
contextIdstringnullReuse persistent Cookie/Storage to maintain login state across Sessions
proxyobjectnullProxy config for accessing IP-restricted internal or external systems

Context for Persistent Login

For internal systems that require re-login each time, use the Context feature to persist Cookies and Storage — avoid repeating the login process on every RPA execution, improving efficiency and reducing the risk of being flagged by anti-bot mechanisms.

// First time: log in and save Context
const ctxRes = await fetch('https://bf.mktindex.com/api/v1/contexts', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'erp-rpa-session' }),
}).then(r => r.json());
const contextId = ctxRes.id;

// Create Session and log in — Context auto-saves cookies
const session = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ contextId, keepAlive: true, timeoutSeconds: 3600 }),
}).then(r => r.json());

// ... perform login steps ...

// Subsequent runs: reuse saved login state, no re-login needed
const nextSession = await fetch('https://bf.mktindex.com/api/v1/sessions', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ contextId, keepAlive: true, timeoutSeconds: 3600 }),
}).then(r => r.json());
// Browser is now logged in — access internal systems directly