How this skill is triggered — by the user, by Claude, or both
Slash command
/demo-creator:skills/screenenvThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
---
Screenenv is a Playwright-based tool that runs in Kubernetes Jobs to record browser-based demos. This skill explains how to use it within the demo-creator pipeline.
Screenenv is a headless browser recording tool from HuggingFace that:
Important: Screenenv is NOT an MCP server. You don't call it directly via tools. Instead, you write Playwright scripts that screenenv will execute.
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: Script Development (detailed-script agent) │
│ - Agent writes Playwright Python script │
│ - Uses domain knowledge of the app │
│ - Tools: Read, Write, Grep, Bash │
└─────────────────┬───────────────────────────────────────────┘
│
▼
script.py saved to .demo/{demo_id}/
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Phase 2: Recording (record-demo agent) │
│ - Uses ScreenenvJobManager Python utility │
│ - Creates Helm release with screenenv-job chart │
│ - Passes script.py to K8s Job │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Phase 3: Execution (Kubernetes Job) │
│ - Screenenv runs in k8s Pod │
│ - Executes Playwright script in headless Chrome │
│ - Records screen at 1920x1080 │
│ - Saves video to PVC │
└─────────────────────────────────────────────────────────────┘
When developing demo scripts (in the detailed-script agent), write standard Playwright Python code:
"""
Demo Script: {Feature Name}
Generated by: demo-creator detailed-script agent
"""
from playwright.sync_api import sync_playwright
import time
def run_demo(page):
"""Execute the demo script."""
# Scene 1: Navigate to Feature
print("Scene 1: Navigate to Feature")
page.goto("http://localhost:3000/drugs")
page.wait_for_load_state("networkidle")
time.sleep(2)
# Verify page loaded
assert page.locator('input[placeholder*="Search"]').is_visible()
page.screenshot(path="scene_1.png")
# Scene 2: Interact with UI
print("Scene 2: Apply Filters")
page.click('button:has-text("Filter")')
time.sleep(0.5)
page.fill('input[name="search"]', "EGFR")
time.sleep(0.4)
page.click('button[type="submit"]')
time.sleep(1.5)
page.screenshot(path="scene_2.png")
def setup():
"""Run setup commands before demo (optional)."""
import subprocess
# Example: Seed test data
subprocess.run([
"kubectl", "exec", "-n", "your-namespace",
"deployment/backend", "--",
"python", "scripts/seed_demo_data.py"
], check=True)
def teardown():
"""Run cleanup commands after demo (optional)."""
import subprocess
subprocess.run([
"kubectl", "exec", "-n", "your-namespace",
"deployment/backend", "--",
"python", "scripts/cleanup_demo_data.py"
], check=True)
def main():
"""Main entry point - screenenv executes this."""
setup()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
record_video_dir="./recordings",
record_video_size={"width": 1920, "height": 1080}
)
page = context.new_page()
try:
run_demo(page)
finally:
context.close()
browser.close()
teardown()
if __name__ == "__main__":
main()
Navigation:
page.goto("http://localhost:3000/path")
page.wait_for_load_state("networkidle")
page.wait_for_url("**/drugs")
Clicking:
page.click('button:has-text("Submit")')
page.locator('.submit-btn').click()
page.click('button[aria-label="Close"]')
Typing:
page.fill('input[name="search"]', "text")
page.type('input', "text", delay=100) # With typing delay
Assertions:
assert page.locator('.result').is_visible()
page.wait_for_selector('.result', timeout=5000)
Screenshots:
page.screenshot(path="scene_1.png")
page.locator('.specific-element').screenshot(path="element.png")
Waits:
time.sleep(2) # Explicit wait (use for timing)
page.wait_for_timeout(2000) # Playwright wait
page.wait_for_selector('.element') # Wait for element
page.click('button:has-text("Submit")')page.fill('input[placeholder="Search"]', text)button[aria-label="Close"][data-testid="submit-btn"]The record-demo agent uses the Python utility to manage K8s Jobs:
import sys
sys.path.append("plugins/demo-creator")
from utils.screenenv_job import ScreenenvJobManager, create_and_run_recording
from utils.manifest import Manifest
# Load manifest
manifest = Manifest("{demo_id}")
manifest.load()
# Get script path
script_path = manifest.get_file_path("script.py")
# Option 1: Convenience function (recommended)
result = create_and_run_recording(
demo_id=manifest.demo_id,
script_url=f"http://demo-script-server/scripts/{manifest.demo_id}.py",
output_path=manifest.get_file_path("raw_recording.mp4"),
target_url="http://localhost:3000",
cleanup=True
)
# Option 2: Manual control
manager = ScreenenvJobManager(
namespace="infra",
helm_chart_path="k8s/infra/charts/screenenv-job",
context="k3d-local"
)
# Create job
job_result = manager.create_job(
demo_id=manifest.demo_id,
script_url=f"http://demo-script-server/scripts/{manifest.demo_id}.py",
target_url="http://localhost:3000",
resolution="1920x1080",
frame_rate="30"
)
# Wait for completion
wait_result = manager.wait_for_completion(
demo_id=manifest.demo_id,
poll_interval=5,
max_wait=600
)
# Retrieve recording
if wait_result["status"] == "completed":
success = manager.retrieve_recording(
demo_id=manifest.demo_id,
output_path=manifest.get_file_path("raw_recording.mp4")
)
if success:
print("✅ Recording retrieved")
# Cleanup
manager.cleanup_job(manifest.demo_id)
The screenenv Helm chart is at k8s/infra/charts/screenenv-job/.
| Parameter | Default | Description |
|---|---|---|
demoId | - | Unique demo identifier |
scriptUrl | - | URL to Playwright script |
targetUrl | http://localhost:3000 | App base URL |
resolution | 1920x1080 | Video resolution |
frameRate | 30 | Video frame rate |
image.repository | ghcr.io/huggingface/screenenv | Docker image |
/recordings/{demo_id}/raw_recording.mp4kubectl cp to extract videokubectl get job screenenv-{demo_id} -n infra --context k3d-local
kubectl logs job/screenenv-{demo_id} -n infra --context k3d-local
# Get pod name
POD=$(kubectl get pods -n infra -l job-name=screenenv-{demo_id} \
--context k3d-local -o jsonpath='{.items[0].metadata.name}')
# Check file exists
kubectl exec -n infra $POD --context k3d-local -- \
ls -lh /recordings/{demo_id}/
The scriptUrl must be accessible from inside the K8s cluster. For local development:
print() statementskubectl exec ... -- curl http://localhost:3000--wait --timeout 10mpage.wait_for_selector() that never resolvesheadless=False to verify selectorsassert to catch UI changes that break the scriptmanager.cleanup_job() after retrieval# 1. Load context
import sys
sys.path.append("plugins/demo-creator")
from utils.manifest import Manifest
manifest = Manifest("{demo_id}")
manifest.load()
# Read outline
with open(manifest.get_file_path("outline.md")) as f:
outline = f.read()
# 2. Write Playwright script based on outline
# (Use domain knowledge of the app, not screenenv MCP calls)
script_content = """
from playwright.sync_api import sync_playwright
import time
def run_demo(page):
# Based on outline, write the actual interactions
page.goto("http://localhost:3000/drugs")
# ... etc
"""
# 3. Save script
with open(manifest.get_file_path("script.py"), "w") as f:
f.write(script_content)
# 4. Update manifest
manifest.complete_stage(2, {
"script_path": "script.py",
"estimated_duration_seconds": 30
})
print("✅ Stage 2 complete: Playwright script created")
If you have existing Playwright pytest files, you can use them as a starting point for demo scripts. Read the test, understand the flow, and adapt it for demo purposes.
| Test Pattern | Demo Adaptation |
|---|---|
page.fill('[name="q"]', "text") | page.type('[name="q"]', "text", delay=100) - visible typing |
| No delays between actions | Add time.sleep(1-2) for pacing |
expect(...).to_be_visible() | Remove or minimize assertions |
| Complex error handling | Simple happy-path only |
| Fast execution | Deliberate, watchable pacing |
Original test:
def test_search(self, page):
page.goto("/drugs")
page.fill('[name="q"]', "aspirin")
page.click('button[type="submit"]')
expect(page.locator(".results")).to_be_visible()
Demo script:
def run_demo(page):
print("Scene 1: Navigate to search")
page.goto("http://localhost:3000/drugs")
time.sleep(2)
print("Scene 2: Search for aspirin")
page.type('[name="q"]', "aspirin", delay=100)
time.sleep(0.5)
page.click('button[type="submit"]')
page.wait_for_selector(".results")
time.sleep(2)
.mcp.json for screenenv in pluginsWhen to use this skill:
npx claudepluginhub estsauver/demo-creator --plugin demo-creatorRecords polished UI demo videos of web applications using Playwright, following a three-phase discover-rehearse-record process. Best for creating walkthroughs, tutorials, or feature showcase videos for documentation or presentations.
Records polished UI demo videos with Playwright, including visible cursor overlays, natural pacing, and professional WebM output. Uses a three-stage process of discovery, rehearsal, and recording.
Converts Playwright test traces into polished demo videos with voiceover, subtitles, speed control, and narration using the playwright-recast library.