
Cloudflare Browser
- 33 installs
- 9.9k repo stars
- Updated May 9, 2026
- cloudflare/moltworker
cloudflare-browser controls headless Chrome via Cloudflare CDP.
About
The cloudflare-browser skill automates headless Chrome through Cloudflare Browser Rendering CDP WebSocket with CDP_SECRET and cdpUrl profile configuration. Provides screenshot.js and video.js scripts, Target.targetCreated handling, and Page.navigate captureScreenshot Runtime.evaluate and viewport commands.
- CDP WebSocket browser automation on Workers.
- Screenshot and video capture scripts.
- Target.targetCreated page target handling.
- Viewport scroll and evaluate patterns.
- CDP_SECRET and cdpUrl prerequisites.
Cloudflare Browser by the numbers
- 33 all-time installs (skills.sh)
- Ranked #1,197 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
cloudflare-browser capabilities & compatibility
- Capabilities
- cdp commands and scripts
- Works with
- cloudflare
- Use cases
- web scraping
What cloudflare-browser says it does
Control headless Chrome via Cloudflare Browser Rendering CDP WebSocket
npx skills add https://github.com/cloudflare/moltworker --skill cloudflare-browserAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 9.9k |
| Last updated | May 9, 2026 |
| Repository | cloudflare/moltworker ↗ |
How do I screenshot pages with Cloudflare Browser Rendering?
Control headless Chrome via Cloudflare Browser Rendering CDP WebSocket.
Who is it for?
Agents automating browsers on Cloudflare Workers.
Skip if: Skip for local-only browser tools.
When should I use this skill?
Browser automation on Cloudflare with CDP.
What you get
Captured PNG or MP4 from CDP scripts.
Files
Cloudflare Browser Rendering
Control headless browsers via Cloudflare's Browser Rendering service using CDP (Chrome DevTools Protocol) over WebSocket.
Prerequisites
CDP_SECRETenvironment variable set- Browser profile configured in openclaw.json with
cdpUrlpointing to the worker endpoint:
"browser": {
"profiles": {
"cloudflare": {
"cdpUrl": "https://your-worker.workers.dev/cdp?secret=..."
}
}
}Quick Start
Screenshot
node /path/to/skills/cloudflare-browser/scripts/screenshot.js https://example.com output.pngMulti-page Video
node /path/to/skills/cloudflare-browser/scripts/video.js "https://site1.com,https://site2.com" output.mp4CDP Connection Pattern
The worker creates a page target automatically on WebSocket connect. Listen for Target.targetCreated event to get the targetId:
const WebSocket = require('ws');
const CDP_SECRET = process.env.CDP_SECRET;
const WS_URL = `wss://your-worker.workers.dev/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;
const ws = new WebSocket(WS_URL);
let targetId = null;
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.method === 'Target.targetCreated' && msg.params?.targetInfo?.type === 'page') {
targetId = msg.params.targetInfo.targetId;
}
});Key CDP Commands
| Command | Purpose |
|---|---|
| Page.navigate | Navigate to URL |
| Page.captureScreenshot | Capture PNG/JPEG |
| Runtime.evaluate | Execute JavaScript |
| Emulation.setDeviceMetricsOverride | Set viewport size |
Common Patterns
Navigate and Screenshot
await send('Page.navigate', { url: 'https://example.com' });
await new Promise(r => setTimeout(r, 3000)); // Wait for render
const { data } = await send('Page.captureScreenshot', { format: 'png' });
fs.writeFileSync('out.png', Buffer.from(data, 'base64'));Scroll Page
await send('Runtime.evaluate', { expression: 'window.scrollBy(0, 300)' });Set Viewport
await send('Emulation.setDeviceMetricsOverride', {
width: 1280,
height: 720,
deviceScaleFactor: 1,
mobile: false
});Creating Videos
1. Capture frames as PNGs during navigation 2. Use ffmpeg to stitch: ffmpeg -framerate 10 -i frame_%04d.png -c:v libx264 -pix_fmt yuv420p output.mp4
Troubleshooting
- No target created: Race condition - wait for Target.targetCreated event with timeout
- Commands timeout: Worker may have cold start delay; increase timeout to 30-60s
- WebSocket hangs: Verify CDP_SECRET matches worker configuration
#!/usr/bin/env node
/**
* Cloudflare Browser Rendering - CDP Client Library
*
* Reusable CDP WebSocket client for Cloudflare Browser Rendering.
* Import and use in custom scripts.
*
* Usage:
* const { createClient } = require('./cdp-client');
* const client = await createClient();
* await client.navigate('https://example.com');
* const screenshot = await client.screenshot();
* client.close();
*/
const WebSocket = require('ws');
function createClient(options = {}) {
const CDP_SECRET = options.secret || process.env.CDP_SECRET;
if (!CDP_SECRET) {
throw new Error('CDP_SECRET environment variable not set');
}
const workerUrl = (options.workerUrl || process.env.WORKER_URL).replace(/^https?:\/\//, '');
const wsUrl = `wss://${workerUrl}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;
const timeout = options.timeout || 60000;
return new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl);
let messageId = 1;
const pending = new Map();
let targetId = null;
let targetResolve;
const targetReady = new Promise(r => { targetResolve = r; });
function send(method, params = {}) {
return new Promise((res, rej) => {
const id = messageId++;
const timer = setTimeout(() => {
pending.delete(id);
rej(new Error(`Timeout: ${method}`));
}, timeout);
pending.set(id, { resolve: res, reject: rej, timeout: timer });
ws.send(JSON.stringify({ id, method, params }));
});
}
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.method === 'Target.targetCreated' && msg.params?.targetInfo?.type === 'page') {
targetId = msg.params.targetInfo.targetId;
targetResolve(targetId);
}
if (msg.id && pending.has(msg.id)) {
const { resolve, reject, timeout: timer } = pending.get(msg.id);
clearTimeout(timer);
pending.delete(msg.id);
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
}
});
ws.on('error', reject);
ws.on('open', async () => {
try {
// Wait for target
await Promise.race([
targetReady,
new Promise((_, rej) => setTimeout(() => rej(new Error('No target created')), 10000))
]);
// Client API
const client = {
ws,
targetId,
send,
async navigate(url, waitMs = 3000) {
await send('Page.navigate', { url });
await new Promise(r => setTimeout(r, waitMs));
},
async screenshot(format = 'png') {
const { data } = await send('Page.captureScreenshot', { format });
return Buffer.from(data, 'base64');
},
async setViewport(width = 1280, height = 800, scale = 1, mobile = false) {
await send('Emulation.setDeviceMetricsOverride', {
width, height, deviceScaleFactor: scale, mobile
});
},
async evaluate(expression) {
return send('Runtime.evaluate', { expression });
},
async scroll(y = 300) {
await send('Runtime.evaluate', { expression: `window.scrollBy(0, ${y})` });
await new Promise(r => setTimeout(r, 300));
},
async click(selector) {
await send('Runtime.evaluate', {
expression: `document.querySelector('${selector}')?.click()`
});
},
async type(selector, text) {
await send('Runtime.evaluate', {
expression: `(() => {
const el = document.querySelector('${selector}');
if (el) { el.value = '${text}'; el.dispatchEvent(new Event('input')); }
})()`
});
},
async getHTML() {
const result = await send('Runtime.evaluate', {
expression: 'document.documentElement.outerHTML'
});
return result.result?.value;
},
async getText() {
const result = await send('Runtime.evaluate', {
expression: 'document.body.innerText'
});
return result.result?.value;
},
close() {
ws.close();
}
};
resolve(client);
} catch (err) {
reject(err);
}
});
});
}
module.exports = { createClient };
// CLI mode
if (require.main === module) {
console.log('CDP Client Library - import with: const { createClient } = require("./cdp-client")');
}
#!/usr/bin/env node
/**
* Cloudflare Browser Rendering - Screenshot
* Usage: node screenshot.js <url> [output.png]
*/
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const CDP_SECRET = process.env.CDP_SECRET;
if (!CDP_SECRET) {
console.error('Error: CDP_SECRET environment variable not set');
process.exit(1);
}
const WORKER_URL = process.env.WORKER_URL.replace(/^https?:\/\//, '');
const WS_URL = `wss://${WORKER_URL}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;
const url = process.argv[2];
const output = process.argv[3] || 'screenshot.png';
if (!url) {
console.error('Usage: node screenshot.js <url> [output.png]');
process.exit(1);
}
let messageId = 1;
const pending = new Map();
async function main() {
console.log(`Capturing screenshot of ${url}`);
const ws = new WebSocket(WS_URL);
let targetResolve;
const targetReady = new Promise(r => { targetResolve = r; });
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = messageId++;
const timeout = setTimeout(() => {
pending.delete(id);
reject(new Error(`Timeout: ${method}`));
}, 60000);
pending.set(id, { resolve, reject, timeout });
ws.send(JSON.stringify({ id, method, params }));
});
}
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.method === 'Target.targetCreated' && msg.params?.targetInfo?.type === 'page') {
targetResolve();
}
if (msg.id && pending.has(msg.id)) {
const { resolve, reject, timeout } = pending.get(msg.id);
clearTimeout(timeout);
pending.delete(msg.id);
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
process.exit(1);
});
await new Promise(r => ws.on('open', r));
// Wait for target with timeout
await Promise.race([
targetReady,
new Promise((_, reject) => setTimeout(() => reject(new Error('No target created')), 10000))
]);
try {
// Set viewport
await send('Emulation.setDeviceMetricsOverride', {
width: 1280,
height: 800,
deviceScaleFactor: 2,
mobile: false
});
// Navigate
await send('Page.navigate', { url });
await new Promise(r => setTimeout(r, 5000));
// Screenshot
const { data } = await send('Page.captureScreenshot', { format: 'png' });
const buffer = Buffer.from(data, 'base64');
const outputPath = path.resolve(output);
fs.writeFileSync(outputPath, buffer);
console.log(`✓ Saved to ${outputPath} (${(buffer.length / 1024).toFixed(1)} KB)`);
ws.close();
} catch (err) {
console.error('Error:', err.message);
ws.close();
process.exit(1);
}
}
main();
#!/usr/bin/env node
/**
* Cloudflare Browser Rendering - Video Capture
* Usage: node video.js "url1,url2,url3" [output.mp4] [--fps 10] [--scroll]
*
* Captures frames while browsing multiple URLs and creates an MP4 video.
* Requires: ffmpeg installed
*/
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const CDP_SECRET = process.env.CDP_SECRET;
if (!CDP_SECRET) {
console.error('Error: CDP_SECRET environment variable not set');
process.exit(1);
}
const WORKER_URL = process.env.WORKER_URL.replace(/^https?:\/\//, '');
const WS_URL = `wss://${WORKER_URL}/cdp?secret=${encodeURIComponent(CDP_SECRET)}`;
// Parse args
const args = process.argv.slice(2);
const urlArg = args.find(a => !a.startsWith('--'));
const output = args.find((a, i) => i > 0 && !a.startsWith('--')) || 'output.mp4';
const fps = args.includes('--fps') ? parseInt(args[args.indexOf('--fps') + 1]) : 10;
const doScroll = args.includes('--scroll');
if (!urlArg) {
console.error('Usage: node video.js "url1,url2,url3" [output.mp4] [--fps 10] [--scroll]');
process.exit(1);
}
const urls = urlArg.split(',').map(u => u.trim());
const framesDir = `/tmp/cf-video-frames-${Date.now()}`;
fs.mkdirSync(framesDir, { recursive: true });
let messageId = 1;
const pending = new Map();
async function main() {
console.log(`Creating video from ${urls.length} URL(s)`);
console.log(`Output: ${output}, FPS: ${fps}, Scroll: ${doScroll}\n`);
const ws = new WebSocket(WS_URL);
let targetResolve;
const targetReady = new Promise(r => { targetResolve = r; });
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = messageId++;
const timeout = setTimeout(() => {
pending.delete(id);
reject(new Error(`Timeout: ${method}`));
}, 60000);
pending.set(id, { resolve, reject, timeout });
ws.send(JSON.stringify({ id, method, params }));
});
}
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.method === 'Target.targetCreated' && msg.params?.targetInfo?.type === 'page') {
targetResolve();
}
if (msg.id && pending.has(msg.id)) {
const { resolve, reject, timeout } = pending.get(msg.id);
clearTimeout(timeout);
pending.delete(msg.id);
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
}
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
process.exit(1);
});
await new Promise(r => ws.on('open', r));
await Promise.race([
targetReady,
new Promise((_, reject) => setTimeout(() => reject(new Error('No target created')), 10000))
]);
let frameNum = 0;
async function captureFrames(count, delayMs = 100) {
for (let i = 0; i < count; i++) {
const { data } = await send('Page.captureScreenshot', { format: 'png' });
const filename = `frame_${String(frameNum).padStart(5, '0')}.png`;
fs.writeFileSync(path.join(framesDir, filename), Buffer.from(data, 'base64'));
frameNum++;
await new Promise(r => setTimeout(r, delayMs));
}
}
async function scroll() {
await send('Runtime.evaluate', { expression: 'window.scrollBy(0, 300)' });
await new Promise(r => setTimeout(r, 300));
}
try {
// Set viewport
await send('Emulation.setDeviceMetricsOverride', {
width: 1280,
height: 720,
deviceScaleFactor: 1,
mobile: false
});
for (const url of urls) {
console.log(`→ ${url}`);
await send('Page.navigate', { url });
await new Promise(r => setTimeout(r, 4000));
// Capture frames on page
await captureFrames(15);
if (doScroll) {
await scroll();
await captureFrames(10);
await scroll();
await captureFrames(10);
}
}
ws.close();
console.log(`\n✓ Captured ${frameNum} frames`);
// Stitch with ffmpeg
console.log('Encoding video...');
const outputPath = path.resolve(output);
execSync(
`ffmpeg -y -framerate ${fps} -i "${framesDir}/frame_%05d.png" -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 "${outputPath}"`,
{ stdio: 'pipe' }
);
// Cleanup frames
fs.rmSync(framesDir, { recursive: true });
const stats = fs.statSync(outputPath);
console.log(`✓ Video saved to ${outputPath} (${(stats.size / 1024).toFixed(1)} KB)`);
} catch (err) {
console.error('Error:', err.message);
ws.close();
process.exit(1);
}
}
main();
Related skills
FAQ
What does cloudflare-browser do?
cloudflare-browser controls headless Chrome via Cloudflare CDP.
When should I use cloudflare-browser?
Browser automation on Cloudflare with CDP.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.