← Back to Home

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 tabs

Everything 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 ModeScript Mode
LoopsNoYes
ConditionalsNoYes
Error handlingAbort onlytry/catch, fallbacks
Data extractionScreenshots onlyStructured JSON
Multi-viewportOne per jobLoop through viewports
AI-generatedEasy (JSON)Possible (code)
Best forLinear workflowsComplex 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.