webreel
WebReel CLI reference — config format, step types, human-like recording patterns, and troubleshooting. webreel is pre-installed globally in the sandbox.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
WebReel CLI reference — config format, step types, human-like recording patterns, and troubleshooting. webreel is pre-installed globally in the sandbox.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Use Twill Cloud Coding Agent to manage Twill's public v1 API workflows. Create/list/update tasks, stream and cancel jobs, manage scheduled tasks, list repositories, and export Claude teleport sessions.
Record high-quality browser verification videos using WebReel. Two-phase workflow: test with agent-browser to collect CSS selectors, then record with WebReel for polished output.
Automate and test Linux desktop apps (Electron, Tauri, React Native Desktop) in an X11 session using CLI commands for screenshot + mouse + keyboard. Includes coordinate scaling so you can click/type based on a smaller “API-sized” screenshot.
| name | webreel |
| description | WebReel CLI reference — config format, step types, human-like recording patterns, and troubleshooting. webreel is pre-installed globally in the sandbox. |
| allowed-tools | Bash(*) |
| metadata | {"author":"Twill","version":"1.0"} |
webreel is a CLI for recording scripted browser demonstrations as MP4, GIF, or WebM files. It drives a headless Chrome instance, captures frames at ~60fps, and encodes them with ffmpeg. Cursor animation, keystroke HUD overlays, and sound effects are built in.
webreel is pre-installed globally in this sandbox (custom build with bug fixes).
npm install webreel, npx webreel, or any other installation command.webreel CLI directly: webreel record, webreel preview, webreel validate.webreel install.| Command | Description |
|---|---|
webreel record [videos...] | Record video(s) from config |
webreel preview [video] | Run in visible browser without recording |
webreel validate | Check config for errors |
webreel record --verbose | Log each step as it executes |
webreel record --dry-run | Print resolved config without recording |
Videos are defined in a webreel.config.json file:
{
"$schema": "https://webreel.dev/schema/v1.json",
"viewport": { "width": 1920, "height": 1080 },
"clickDwell": 500,
"videos": {
"demo": {
"url": "http://localhost:3000",
"output": "demo.mp4",
"zoom": 2,
"waitFor": ".loaded",
"steps": [...]
}
}
}
| Field | Type | Description |
|---|---|---|
$schema | string | Schema URL for IDE autocompletion |
viewport | { width, height } | Default viewport dimensions |
clickDwell | number | Set to 500. Milliseconds cursor holds before each click |
theme | object | Cursor and HUD customization |
videos | object | Map of video name → video config (required) |
| Field | Type | Description |
|---|---|---|
url | string | Target URL (absolute or relative to baseUrl) |
output | string | Output filename (extension determines format: .mp4, .gif, .webm) |
viewport | object | Override viewport for this video |
zoom | number | CSS zoom level (e.g., 2 for 2x) |
waitFor | string | CSS selector to wait for before starting steps |
steps | array | Array of step objects (required) |
Several steps target DOM elements. Use ONE of:
| Field | Description |
|---|---|
text | Match by visible text content |
selector | Match by CSS selector |
within | (optional) CSS selector to scope search to a parent element |
Use text OR selector, not both. within is always optional.
Every step requires an action field. Optional common fields:
description (string) — human-readable note (not used at runtime, but useful for pause timing comments)Do NOT use delay or defaultDelay — see config patterns below.
Wait for a fixed duration. Use 5x multiplier (see config patterns).
| Field | Type | Required |
|---|---|---|
ms | number | yes |
{ "action": "pause", "ms": 5000, "description": "~1s on screen" }
Click a DOM element. MUST be preceded by moveTo + pause (see config patterns).
| Field | Type | Required |
|---|---|---|
text | string | no |
selector | string | no |
within | string | no |
modifiers | string[] | no |
Provide text or selector (at least one).
{ "action": "click", "text": "Submit" }
{ "action": "click", "selector": "#save-btn" }
{ "action": "click", "text": "Item 3", "modifiers": ["shift"] }
{ "action": "click", "text": "Delete", "within": ".modal" }
Type text into an input. Set charDelay: 120 or higher.
| Field | Type | Required |
|---|---|---|
text | string | yes |
selector | string | no |
within | string | no |
charDelay | number | no (but always set to 120+) |
{
"action": "type",
"text": "user@example.com",
"selector": "#email",
"charDelay": 120
}
Press a keyboard shortcut.
| Field | Type | Required |
|---|---|---|
key | string | yes |
target | string or ElementTarget | no |
Key combo syntax: "cmd+s", "ctrl+shift+p", "Enter", "Escape", "ArrowDown".
{ "action": "key", "key": "cmd+s" }
{ "action": "key", "key": "Enter" }
Move cursor to an element without clicking. Use before every click.
| Field | Type | Required |
|---|---|---|
text | string | no |
selector | string | no |
within | string | no |
{ "action": "moveTo", "text": "Settings" }
{ "action": "moveTo", "selector": "#submit-btn" }
Hover over an element (triggers CSS :hover and mouseenter).
| Field | Type | Required |
|---|---|---|
text | string | no |
selector | string | no |
within | string | no |
{ "action": "hover", "selector": ".tooltip-trigger" }
Scroll the page or a specific element.
| Field | Type | Required |
|---|---|---|
x | number | no |
y | number | no |
text | string | no |
selector | string | no |
within | string | no |
{ "action": "scroll", "y": 400 }
{ "action": "scroll", "y": 300, "selector": ".scrollable-panel" }
Wait for an element to appear in the DOM.
| Field | Type | Required |
|---|---|---|
selector | string | no |
text | string | no |
within | string | no |
timeout | number | no |
{ "action": "wait", "selector": ".results-loaded", "timeout": 5000 }
{ "action": "wait", "text": "Success" }
Navigate to a new URL.
| Field | Type | Required |
|---|---|---|
url | string | yes |
{ "action": "navigate", "url": "/settings" }
Drag from one element to another.
| Field | Type | Required |
|---|---|---|
from | ElementTarget | yes |
to | ElementTarget | yes |
{
"action": "drag",
"from": { "text": "Task A", "within": ".column-todo" },
"to": { "selector": ".card-list", "within": ".column-done" }
}
Select a value in a <select> dropdown.
| Field | Type | Required |
|---|---|---|
selector | string | no |
text | string | no |
within | string | no |
value | string | yes |
{ "action": "select", "selector": "#country", "value": "us" }
Capture a PNG screenshot.
| Field | Type | Required |
|---|---|---|
output | string | yes |
{ "action": "screenshot", "output": "screenshots/final-state.png" }
You MUST follow ALL rules below when building a WebReel config. Configs that violate these rules produce unwatchable robot-like videos. Validate your config against every rule before recording.
Before running webreel record, verify your config passes ALL of these checks:
click step is preceded by a moveTo to the same target (NO bare clicks)moveTo is followed by a pause (2500–4000ms) before the actionpause (5000–12000ms) so viewers see the resultms values use the 5x multiplier (intended screen time × 5)clickDwell: 500 is set at the video leveltype steps have charDelay: 120 or higherdelay fields or defaultDelay — only explicit pause stepsNEVER write a click step without a moveTo + pause before it. This is the #1 mistake. Without moveTo, the cursor teleports instantly to the target — viewers cannot follow what is happening.
BAD (cursor teleports):
{ "action": "click", "text": "Filters" }
GOOD (cursor travels, dwells, then clicks):
{ "action": "moveTo", "text": "Filters" },
{ "action": "pause", "ms": 3000, "description": "Cursor dwells (~0.6s)" },
{ "action": "click", "text": "Filters" }
This pattern applies to EVERY click in the config, no exceptions.
Every user-visible interaction MUST follow this cadence:
WebReel compresses idle time by ~5x. A { "action": "pause", "ms": 5000 } produces ~1 second of video.
Rule: Multiply intended screen time by 5 for the ms value. Add a description noting the intended time:
{
"action": "pause",
"ms": 10000,
"description": "Show results (~2s on screen)"
}
Interactive steps (click, type, scroll, hover, moveTo) are captured at ~1:1 real-time. Only pause is compressed.
delay or defaultDelayThe delay field and defaultDelay are subject to the same compression and do NOT reliably add screen time. Use explicit { "action": "pause" } steps instead.
Set charDelay: 120 (or higher) on every type step. The default is too fast.
Set "clickDwell": 500 at the video level. This adds a brief cursor hold before each click.
All examples follow the required config patterns (moveTo + pause before clicks, 5x pause multiplier, charDelay, clickDwell).
{
"$schema": "https://webreel.dev/schema/v1.json",
"clickDwell": 500,
"videos": {
"form-filling": {
"url": "http://localhost:3000/login",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": "#email",
"steps": [
{ "action": "pause", "ms": 5000, "description": "Show form (~1s)" },
{
"action": "type",
"text": "user@example.com",
"selector": "#email",
"charDelay": 120
},
{
"action": "type",
"text": "supersecret123",
"selector": "#password",
"charDelay": 120
},
{ "action": "pause", "ms": 3000, "description": "Pause before submit" },
{ "action": "moveTo", "text": "Sign In" },
{
"action": "pause",
"ms": 3000,
"description": "Cursor dwells (~0.6s)"
},
{ "action": "click", "text": "Sign In" },
{ "action": "pause", "ms": 10000, "description": "Show result (~2s)" }
]
}
}
}
{
"$schema": "https://webreel.dev/schema/v1.json",
"clickDwell": 500,
"videos": {
"browse-features": {
"url": "http://localhost:3000",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".hero",
"steps": [
{ "action": "pause", "ms": 5000, "description": "Show hero (~1s)" },
{ "action": "scroll", "y": 400 },
{ "action": "pause", "ms": 5000, "description": "Show features (~1s)" },
{ "action": "moveTo", "text": "Learn More" },
{ "action": "pause", "ms": 3000 },
{ "action": "click", "text": "Learn More" },
{ "action": "wait", "text": "Documentation", "timeout": 5000 },
{
"action": "pause",
"ms": 10000,
"description": "Show docs page (~2s)"
}
]
}
}
}
{
"$schema": "https://webreel.dev/schema/v1.json",
"clickDwell": 500,
"videos": {
"keyboard-shortcuts": {
"url": "http://localhost:3000/editor",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".editor",
"steps": [
{ "action": "pause", "ms": 5000, "description": "Show editor (~1s)" },
{ "action": "moveTo", "selector": ".editor" },
{ "action": "pause", "ms": 3000 },
{ "action": "click", "selector": ".editor" },
{ "action": "key", "key": "cmd+a" },
{ "action": "pause", "ms": 3000 },
{ "action": "key", "key": "cmd+b" },
{
"action": "pause",
"ms": 5000,
"description": "Show bold text (~1s)"
},
{ "action": "key", "key": "cmd+s" },
{ "action": "pause", "ms": 5000, "description": "Show saved (~1s)" }
]
}
}
}
NEVER fall back to agent-browser's built-in recording. WebReel produces far better videos with cursor animation and compositing. When webreel fails, debug it — don't switch tools.
Determine if the issue is step execution or frame capture. If steps run but you get ENOENT on the temp mp4 rename, that means 0 frames were captured — the problem is Chrome or ffmpeg, not your config.
Check ffmpeg availability. WebReel looks for ffmpeg at ~/.webreel/bin/ffmpeg/ffmpeg. If missing, symlink the system one:
mkdir -p ~/.webreel/bin/ffmpeg && ln -sf /usr/bin/ffmpeg ~/.webreel/bin/ffmpeg/ffmpeg
Check Chrome flags. In webreel 0.1.4 with Chrome 146+, the --enable-begin-frame-control flag in @webreel/core/dist/chrome.js causes Page.captureScreenshot to hang indefinitely (webreel never calls HeadlessExperimental.beginFrame). Fix:
CHROME_JS=$(find /opt/webreel -name "chrome.js" -path "*/core/*" 2>/dev/null | head -1)
if [ -n "$CHROME_JS" ]; then
sed -i 's/--enable-begin-frame-control//g; s/--run-all-compositor-stages-before-draw//g' "$CHROME_JS"
fi
Add debug logging. If the capture loop hangs, add logging to recorder.js to see where it gets stuck: before timeline.tick(), before captureScreenshot, after captureScreenshot. Check frameCount in stop().
{ "action": "wait", "text": "SomeUniqueText" } to wait for the destination page to load after clicking a link.$TWILL_ENTRYPOINT_LOG_DIR/url-mapping.txt for the correct URL.