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);
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.
| Feature | Description |
|---|---|
| Real-time Screen | Based on CDP Screencast, multi-frame per second refresh, sub-1-second latency |
| Log Panel | Shows Session event stream: created, active, idle, destroyed |
| CDP Console | Send CDP commands directly to manually intervene in abnormal states |
| Session Info | View 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
| Parameter | Type | Default | Description |
|---|---|---|---|
| keepAlive | boolean | false | When true, skips idle timeout checks — Session remains alive long-term |
| timeoutSeconds | integer | 300 | Absolute Session lifetime ceiling (seconds); set a high value for keepAlive |
| userMetadata | object | null | Custom key-value pairs for storing workflow name, status, progress, etc. |
| contextId | string | null | Reuse persistent Cookie/Storage to maintain login state across Sessions |
| proxy | object | null | Proxy 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