Install
openclaw skills install portalUse when asked to make a portal, create a portal, demo a website, product tour, interactive sandbox, or turn any URL into a shareable live browser session. P...
openclaw skills install portalTurn any URL into a shareable live browser session. Viewers get a real browser running in a cloud VM — 10 minutes per session.
openclaw plugins install openclaw-portal
openclaw gateway restart
Watch — AI clicks, scrolls, and narrates a guided demo. Play — Viewer explores freely with AI guardrails blocking unwanted areas.
| Situation | Action |
|---|---|
| User says "make a portal" / "demo this site" | Start at Step 1 below |
| Public site (landing page, docs, marketing) | Skip login, go to Step 3 |
| Authenticated site (dashboard, SaaS, admin) | save_login first (Step 2) |
| Local file / localhost | Zip + base64, pass as ptl.entry.source |
| Chrome extension | Zip extension + set entry.url for test site |
| User wants guided demo | Watch mode → create_script |
| User wants free exploration | Play mode → create_script with play mode |
| User wants to record themselves | record_demo → user records in hosted browser |
| User wants to pick blocked elements | pick_selectors → user clicks in hosted browser |
| Portal is "provisioning" | make_portal auto-polls — just wait for the result |
| Session is pending | Poll get_session — it blocks 30s server-side, keep calling |
| Need session replays | get_portal_sessions → returns conversation logs + recording URLs |
| User needs more credits | buy_credits → opens Stripe checkout |
When any tool returns a URL the user needs to open (verification_url, hosted_url,
portal link, checkout URL), you MUST send it to the user in the current chat.
Do NOT attempt to run shell commands like open or xdg-open — the user is on a
messaging channel (WhatsApp, Telegram, etc), not a local desktop.
Just include the URL in your reply. The user will tap it on their device.
Follow these steps in order. Never skip the review step (Step 4).
Call portal_status. If not authenticated, call portal_login.
Returns verification_url and device_code. Send the verification URL to the user
in the chat so they can open it and sign in.
Poll portal_login_check with the device_code every 5 seconds until approved.
New users get 3 creation credits + 10 view credits.
Ask the user if you're unsure whether the site needs login.
Public site → skip to Step 3.
Needs login → capture auth state:
{"url": "https://app.example.com/login", "description": "Login to Example dashboard"}
Call save_login with the above. Response includes hosted_url — send it to the user
so they can open the hosted browser and log in.
Poll get_session until status is ready. Do NOT ask the user if they're done — the tool tells you. When ready, grab saved_state_id.
Local file → zip the project (exclude node_modules, .git, dist), base64 encode.
Pass contents as ptl.entry.source with entry.type: "local_file".
Offer the user 4 options if they don't specify:
Watch — AI script (most common):
{
"url": "https://stripe.com",
"goals": [
"Show the main value proposition",
"Navigate to pricing page and highlight plans",
"End with social proof or customer logos"
],
"max_pages": 3
}
Use max_pages: 3 to get multi-page demos (homepage + pricing + features). Without it, demos stay on the homepage only.
Writing great goals:
Call create_script. It auto-polls internally and returns the complete draft directly — no need to call get_script.
Watch — Record yourself: Call record_demo. Send the hosted_url to the user so they can record in the hosted browser.
Play — AI selectors: Call create_script with mode: "play".
Play — User picks: Call pick_selectors. Send the hosted_url to the user so they can click elements in the hosted browser.
Never skip this step. Show the user everything from the draft:
Watch mode:
Play mode:
Ask: "What do you want to call this? Look good to go?"
Slugify the name for the URL (lowercase, hyphens, no spaces).
Call make_portal with the full PTL spec. Costs 1 creation credit.
It auto-polls internally until the portal is ready — the result includes the final portal URL. Send it to the user.
configure_portal with cta_text and cta_urlconfigure_embed with allowed_originget_portal_sessionsget_creation_logsThe ptl parameter to make_portal MUST be a JSON object (not a string). Do NOT JSON.stringify it.
Play mode:
{
"ptl": {
"entry": { "url": "https://example.com" },
"experience": {
"mode": "play",
"agent": {
"greeting": "Welcome! Ask me anything.",
"knowledge": "Summary of the site..."
}
},
"guardrails": {
"allowed_urls": ["example.com"],
"disabled_elements": [
{ "selector": "a[href='/login']", "reason": "Auth disabled in demo" }
]
}
}
}
Watch mode:
{
"ptl": {
"entry": { "url": "https://example.com" },
"experience": {
"mode": "watch",
"agent": {
"goal": "Show key features",
"greeting": "Welcome to the demo!",
"scenes": [
{
"script": "Narration text here",
"actions": [
{ "type": "scroll_to_element", "selector": "h2", "inner_text": "Key Features" },
{ "type": "wait", "ms": 2000 },
{ "type": "click", "selector": "a[href='/pricing']", "inner_text": "Pricing" }
]
}
]
}
},
"guardrails": {
"allowed_urls": ["example.com"],
"disabled_elements": []
}
}
}
Supported action types:
| Action | Use for | Required fields |
|---|---|---|
scroll_to_element | Navigate to a section by heading | selector, inner_text |
scroll_down | Generic scroll | selector: "body" |
click | Navigate pages, open tabs/accordions | selector, inner_text |
wait | Pause for emphasis (1500-3000ms) | ms |
type | Type into form inputs | selector, text |
keypress | Keyboard shortcuts | selector, key |
Scenes can have multiple actions (up to 20 per scene). A good scene flows: scroll → wait → click.
Server auto-fills version, region, entry.type. No need to call normalize_ptl or validate_ptl before make_portal — validation is built in.
When reviewing drafts from create_script, improve them before deploying:
scroll_to_element actions, add click actions to navigate between pages (pricing, features, docs).{ "type": "wait", "ms": 2000 } between actions for natural pacing and emphasis.inner_text: On every click and scroll_to_element action — it's the fallback when CSS selectors break on dynamic pages.Portal demos show a real cursor moving on screen. The cursor automatically:
For best results:
scroll_to_element with specific inner_text (not just body) so the cursor targets a visible headingclick actions on prominent buttons/links with clear inner_textwait between actions so viewers can see the cursor position before the next movecreate_script or pick_selectors returnsmake_portalget_session — it blocks 30s server-side. Do NOT ask user if they're doneinner_text on all click and scroll_to_element actions — it's the fallback when selectors fail on dynamic pagesget_portal insteadmax_pages: 3 (or higher) in create_script to get richer navigation beyond the homepage