Script Mode
Full Playwright Power. Zero Infrastructure. Loops, conditionals, try/catch, multi-page crawls, data extraction—write real code, not JSON workarounds.
When Steps Mode Isn't Enough
Steps mode is great for simple, linear workflows: go here, click this, screenshot that. But real automation isn't always linear.
You need a loop
Scroll to the bottom of an infinite-scroll page, collecting items until there are no more. Steps mode can't loop.
You need conditionals
Handle a cookie banner if it appears, skip it if it doesn't. Steps mode runs every step regardless.
You need error handling
Retry a flaky selector, catch a timeout, fall back to an alternative flow. Steps mode fails on the first error.
You need to extract data
Pull structured data from a table, transform it, and save it as JSON. Steps mode takes screenshots—it doesn't extract.
Script mode handles all of these. It's Playwright, running on our infrastructure.
Quick Start
// POST to https://api.riddledc.com/v1/run
{
"script": "await page.goto('https://news.ycombinator.com');\n const stories = await page.$$eval('.titleline > a', links =>\n links.map(a => ({ title: a.textContent, url: a.href }))\n );\n await saveJson('top-stories', stories);\n await saveScreenshot('hn-front-page');"
}One API call. You get a JSON file with structured data and a screenshot. No browser to install, no Chromium to update, no server to maintain.
What You Get in the Sandbox
Your script receives a full Playwright environment plus helpers:
// Available in your script:
// page — Playwright Page object (full API)
// context — BrowserContext (cookies, localStorage, permissions)
// browser — Browser instance (create additional contexts)
// saveScreenshot(name) — capture a named PNG, delivered with results
// saveJson(name, data) — save structured data, delivered with results
// waitForWindow() — capture popups and new tabsEverything in the Playwright docs works.
page.goto, page.click, page.evaluate,
page.waitForSelector, page.route—all of it.
Real Examples
Infinite Scroll Scraping
Collect every item from a lazy-loaded feed:
await page.goto('https://example.com/feed');
let items = [];
let previousHeight = 0;
while (true) {
const newItems = await page.$$eval('.feed-item', els =>
els.map(el => ({
title: el.querySelector('h3')?.textContent?.trim(),
url: el.querySelector('a')?.href,
date: el.querySelector('time')?.getAttribute('datetime')
}))
);
items = [...new Map(
[...items, ...newItems].map(i => [i.url, i])
).values()];
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000);
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) break;
previousHeight = currentHeight;
}
await saveJson('feed-items', items);
await saveScreenshot('feed-bottom');Try doing that with JSON steps.
Multi-Viewport Responsive Testing
Test a page across every breakpoint in one job:
const viewports = [
{ name: 'mobile', width: 375, height: 812 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'laptop', width: 1366, height: 768 },
{ name: 'desktop', width: 1920, height: 1080 },
];
for (const vp of viewports) {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await saveScreenshot(`responsive-${vp.name}`);
}Four screenshots, four viewports, one job, one API call.
Complex Login with 2FA Handling
Handle multi-step authentication flows with conditionals:
await page.goto('https://app.example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'your-password');
await page.click('button[type="submit"]');
// Handle email verification if it appears
try {
await page.waitForSelector('#verification-input', { timeout: 5000 });
// Prompt appeared — fill the code you received
await page.fill('#verification-input', '123456');
await page.click('#verify-btn');
} catch {
// No verification prompt — already through
}
await page.waitForSelector('.dashboard', { timeout: 10000 });
await saveScreenshot('authenticated-dashboard');Steps mode can't branch. Script mode can.
Data Extraction + Screenshots in One Job
Scrape a product catalog and screenshot each page:
await page.goto('https://store.example.com/products');
const products = [];
let pageNum = 1;
while (true) {
const pageProducts = await page.$$eval('.product-card', cards =>
cards.map(card => ({
name: card.querySelector('.name')?.textContent?.trim(),
price: card.querySelector('.price')?.textContent?.trim(),
sku: card.dataset.sku,
inStock: !card.classList.contains('out-of-stock')
}))
);
products.push(...pageProducts);
await saveScreenshot(`catalog-page-${pageNum}`);
const nextBtn = await page.$('a.next-page:not(.disabled)');
if (!nextBtn) break;
await nextBtn.click();
await page.waitForLoadState('networkidle');
pageNum++;
}
await saveJson('full-catalog', { total: products.length, products });Multi-page crawl, structured data extraction, and visual proof—all in one job.
Popup and New Window Handling
Capture OAuth popups or payment windows:
await page.goto('https://app.example.com');
const popup = await waitForWindow(() =>
page.click('#google-signin-btn')
);
await popup.fill('input[type="email"]', 'user@gmail.com');
await popup.click('#next');
await saveScreenshot('oauth-popup');Script Mode vs Steps Mode
Use the right tool for the job:
| Steps Mode | Script Mode | |
|---|---|---|
| Loops | No | Yes |
| Conditionals | No | Yes |
| Error handling | Abort only | try/catch, fallbacks |
| Data extraction | Screenshots only | Structured JSON |
| Multi-viewport | One per job | Loop through viewports |
| AI-generated | Easy (JSON) | Possible (code) |
| Best for | Linear workflows | Complex automation |
A good rule of thumb: if you're fighting steps mode to do something, switch to script mode.
Cost Efficiency
Script mode bills the same as any other job—by browser time. The difference is what you can pack into that time.
Steps mode
One linear flow per job.
Script mode
Loops, multiple pages, multiple viewports, data extraction, and screenshots—all in one job.
Pack more work into fewer jobs. Pay for browser time, not API calls.
Calling the API
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"script": "await page.goto(\"https://example.com\"); await saveScreenshot(\"home\");",
"options": {
"timeout": 60000,
"viewport": { "width": 1920, "height": 1080 }
}
}'Scripts run with a default 30-second timeout. Pass options.timeout to extend it
for longer crawls.
All saveScreenshot() and saveJson() outputs are delivered with the
job results—download them the same way you'd download any Riddle output.
Stop Writing Infrastructure
You don't need a Playwright server. You don't need Docker. You don't need to
manage Chromium updates or debug missing .so files.
Write your script. POST it. Get your results.