| name | openclaw-studio-dashboard |
| description | Expert in OpenClaw Studio - web dashboard for managing OpenClaw Gateway, agents, chat, approvals, and jobs |
| triggers | ["set up openclaw studio dashboard","connect studio to openclaw gateway","create agent in openclaw studio","configure openclaw studio upstream","manage openclaw agents with studio","deploy openclaw studio to production","troubleshoot openclaw studio connection","run openclaw studio locally"] |
OpenClaw Studio Dashboard
Skill by ara.so — Hermes Skills collection.
OpenClaw Studio is a clean web dashboard for OpenClaw Gateway that provides a unified interface to connect gateways, manage agents, chat, handle approvals, and configure jobs. Built with TypeScript/Next.js, it runs a server-owned control plane architecture with SSE streaming for real-time runtime events.
What OpenClaw Studio Does
- Gateway Connection: Connect to local or remote OpenClaw Gateway instances via WebSocket
- Agent Management: Create, configure, and monitor AI agents with tool policies and sandbox settings
- Chat Interface: Stream conversations with PI agents including tool calls and thinking traces
- Approval Workflows: Manage exec approvals and runtime permissions
- Job Configuration: Set up and monitor cron jobs
- Runtime Streaming: Real-time event streaming over SSE with replay/history
Installation & Startup
Quick Start (Recommended)
npx -y openclaw-studio@latest
From Source
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run dev
Setup Helper
npm run studio:setup
Connection Architecture
OpenClaw Studio uses a two-path architecture:
- Browser → Studio: HTTP + SSE (
/api/runtime/*, /api/intents/*)
- Studio → Gateway: Server-owned WebSocket to upstream OpenClaw
Critical concept: ws://localhost:18789 means "gateway on the Studio host", not "gateway on your browser's machine".
Configuration Patterns
Deployment Scenarios
A. Both Local (Same Computer)
npx -y openclaw-studio@latest
cd openclaw-studio
npm run dev
Get gateway token:
openclaw config get gateway.auth.token
B. Gateway in Cloud, Studio Local
Option 1: Tailscale Serve (Recommended)
On gateway host:
tailscale serve --yes --bg --https 443 http://127.0.0.1:18789
In Studio (local laptop):
- Upstream URL:
wss://<gateway-host>.ts.net
- Upstream Token:
<gateway-token>
Option 2: SSH Tunnel
From laptop:
ssh -L 18789:127.0.0.1:18789 user@<gateway-host>
In Studio:
- Upstream URL:
ws://localhost:18789
- Upstream Token:
<gateway-token>
C. Both in Cloud (Always-On)
On Studio VPS:
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run dev
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
Environment Variables
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
STUDIO_ACCESS_TOKEN=your-secure-token-here
OPENCLAW_STATE_DIR=~/.openclaw
HOST=0.0.0.0
Configuration Files
Studio settings are stored in ~/.openclaw/openclaw-studio/:
~/.openclaw/
├── openclaw.json # OpenClaw Gateway config
└── openclaw-studio/
├── settings.json # Gateway URL/token
└── runtime.db # Control-plane runtime DB
API & Key Commands
Studio Setup
npm run dev
npm run build
npm run start
npm run dev:turbo
npm run verify:native-runtime:repair
npm run verify:native-runtime:check
TypeScript API Patterns
Connecting to Gateway
import { useGatewayConnection } from '@/hooks/useGatewayConnection';
export function DashboardPage() {
const { connected, connect, disconnect, status } = useGatewayConnection();
const handleConnect = async () => {
try {
await connect({
url: 'ws://localhost:18789',
token: process.env.GATEWAY_TOKEN
});
} catch (error) {
console.error('Connection failed:', error);
}
};
return (
<div>
<button onClick={handleConnect} disabled={connected}>
{connected ? 'Connected' : 'Connect'}
</button>
<span>Status: {status}</span>
</div>
);
}
Streaming Runtime Events
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const subscription = runtimeStore.subscribe((event) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
});
const history = await runtimeStore.getHistory();
for (const event of history) {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
}
return () => subscription.unsubscribe();
}
});
return (stream, {
: {
: ,
: ,
:
}
});
}
Creating Agents
interface AgentConfig {
name: string;
systemPrompt: string;
toolPolicy: 'all' | 'allowlist' | 'denylist';
allowedTools?: string[];
sandboxEnabled: boolean;
execApprovalRequired: boolean;
}
export async function createAgent(config: AgentConfig) {
const response = await fetch('/api/intents/agent-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_name: config.name,
system_prompt: config.systemPrompt,
tool_policy: config.toolPolicy,
allowed_tools: config.allowedTools,
sandbox_config: {
enabled: config.sandboxEnabled,
mounts: config.sandboxEnabled ? [
{ host: '/tmp', : , : }
] : []
},
: config.
})
});
(!response.) {
error = response.();
(error. || );
}
response.();
}
Chat with Streaming
import { useEffect, useState } from 'react';
export function ChatInterface({ agentId }: { agentId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [thinking, setThinking] = useState<string[]>([]);
useEffect(() => {
const eventSource = new EventSource('/api/runtime/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'tool_call':
setMessages(prev => [...prev, {
role: 'assistant',
content: `Using tool: ${data.tool_name}`,
metadata: { type: 'tool', ...data }
}]);
break;
case 'thinking':
setThinking( => [...prev, data.]);
;
:
( [...prev, {
: data.,
: data.
}]);
([]);
;
}
});
eventSource.();
}, [agentId]);
= () => {
(, {
: ,
: { : },
: .({
: agentId,
: content
})
});
};
(
);
}
Real-World Examples
Example 1: Local Development Setup
#!/bin/bash
GATEWAY_TOKEN=$(openclaw config get gateway.auth.token)
npx -y openclaw-studio@latest
cd openclaw-studio
export NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
npm run dev
Example 2: Production Cloud Setup
#!/bin/bash
cd /opt
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run build
export STUDIO_ACCESS_TOKEN=$(openssl rand -hex 32)
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
npm run start
echo "Studio available at: https://$(tailscale status --json | jq -r '.Self.DNSName')/"
echo "Access token: $STUDIO_ACCESS_TOKEN"
echo "Use: https://your-host.ts.net/?access_token=$STUDIO_ACCESS_TOKEN"
Example 3: Agent with Sandbox & Approvals
import { createAgent } from './lib/agent-api';
async function createSecureCodeAgent() {
const agent = await createAgent({
name: 'secure-code-assistant',
systemPrompt: `You are a secure code assistant.
Always ask before executing commands.
Work within the sandboxed environment only.`,
toolPolicy: 'allowlist',
allowedTools: [
'bash',
'read_file',
'write_file',
'search_files'
],
sandboxEnabled: true,
execApprovalRequired: true
});
console.log('Agent created:', agent.id);
return agent;
}
createSecureCodeAgent().catch(console.error);
Example 4: Cron Job Configuration
interface CronJobConfig {
agentId: string;
schedule: string;
task: string;
enabled: boolean;
}
async function createCronJob(config: CronJobConfig) {
const response = await fetch('/api/intents/job-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: config.agentId,
schedule: config.schedule,
task_prompt: config.task,
enabled: config.enabled
})
});
return response.json();
}
await createCronJob({
agentId: 'agent-123',
schedule: '0 0 * * *',
task: 'Create backup of project files and upload to storage',
enabled: true
});
Common Patterns
Pattern 1: Conditional Gateway URL
export function getGatewayUrl(): string {
if (process.env.NEXT_PUBLIC_GATEWAY_URL) {
return process.env.NEXT_PUBLIC_GATEWAY_URL;
}
const settings = loadStudioSettings();
if (settings?.gatewayUrl) {
return settings.gatewayUrl;
}
return 'ws://localhost:18789';
}
export function isSecureConnection(url: string): boolean {
return url.startsWith('wss://');
}
Pattern 2: Access Token Management
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const requiredToken = process.env.STUDIO_ACCESS_TOKEN;
if (!requiredToken) {
return NextResponse.next();
}
const cookieToken = request.cookies.get('studio_access_token')?.value;
if (cookieToken === requiredToken) {
return NextResponse.next();
}
const queryToken = request.nextUrl.searchParams.get('access_token');
if (queryToken === requiredToken) {
const response = NextResponse.next();
response.cookies.set('studio_access_token', requiredToken, {
httpOnly: ,
: ,
: ,
: * * *
});
response;
}
(, { : });
}
config = {
: [, ]
};
Pattern 3: Runtime Event Handling
import Database from 'better-sqlite3';
export class RuntimeStore {
private db: Database.Database;
private subscribers: Set<(event: RuntimeEvent) => void>;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.subscribers = new Set();
this.initDb();
}
private initDb() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS runtime_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
agent_id TEXT,
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_timestamp
ON runtime_events(agent_id, timestamp);
`);
}
async storeEvent(event: RuntimeEvent) {
const stmt = this..();
stmt.(
event.,
event.,
.(event),
.()
);
..( (event));
}
(?: , limit = ): <[]> {
query = agentId
?
: ;
stmt = ..(query);
rows = agentId
? stmt.(agentId, limit)
: stmt.(limit);
rows.( .(row.)).();
}
() {
..(callback);
{
: ..(callback)
};
}
}
Troubleshooting
Connection Issues
Problem: UI loads but "Connect" fails
openclaw gateway status
cat ~/.openclaw/openclaw-studio/settings.json
Problem: EPROTO / "wrong version number"
const url = 'wss://localhost:18789';
const url = 'ws://localhost:18789';
const url = 'wss://gateway.ts.net';
Problem: Assets 404 under /studio
module.exports = {
basePath: '/studio',
// ...
}
Native Module Issues
Problem: better_sqlite3.node / NODE_MODULE_VERSION mismatch
npm run verify:native-runtime:repair
npm rebuild better-sqlite3
npm install
node -v && node -p "process.versions.modules"
which node && which npm
nvm use 20
npm install
Problem: SQLite errors on startup
ls -la ~/.openclaw/openclaw-studio/
rm ~/.openclaw/openclaw-studio/runtime.db
npm run dev
Access Token Issues
Problem: 401 "Studio access token required"
export STUDIO_ACCESS_TOKEN=your-secure-token
curl https://studio.ts.net/?access_token=your-secure-token
Debugging Connection Path
export function debugConnection() {
console.log('Studio runtime environment:');
console.log('- Node version:', process.version);
console.log('- Platform:', process.platform);
console.log('- CWD:', process.cwd());
console.log('- State dir:', process.env.OPENCLAW_STATE_DIR || '~/.openclaw');
const isServer = typeof window === 'undefined';
console.log('- Running on:', isServer ? 'server' : 'browser');
if (isServer) {
const os = require('os');
console.log('- Hostname:', os.hostname());
console.log('- Network interfaces:',
Object.keys(os.networkInterfaces()));
}
.();
.(, process.. || );
.(, !!process..);
.(, !!process..);
}
Key Files Reference
openclaw-studio/
├── app/
│ ├── api/
│ │ ├── runtime/
│ │ │ └── stream/route.ts # SSE streaming endpoint
│ │ └── intents/
│ │ ├── agent-create/route.ts
│ │ ├── chat/route.ts
│ │ └── job-create/route.ts
│ ├── dashboard/
│ │ ├── page.tsx # Main dashboard
│ │ └── agents/page.tsx
│ └── layout.tsx
├── lib/
│ ├── runtime-store.ts # SQLite runtime DB
│ ├── gateway-client.ts # WebSocket client
│ └── settings.ts # Settings persistence
├── hooks/
│ └── useGatewayConnection.ts
├── docs/
│ ├── ui-guide.md
│ ├── pi-chat-streaming.md
│ ├── permissions-sandboxing.md
│ └── color-system.md
├── next.config.js
├── package.json
└── ARCHITECTURE.md
Additional Resources
- UI Guide:
docs/ui-guide.md - Agent creation, cron jobs, exec approvals
- PI Chat Streaming:
docs/pi-chat-streaming.md - SSE event handling, replay, tool calls
- Permissions:
docs/permissions-sandboxing.md - Tool policies, sandbox config, approval flows
- Architecture:
ARCHITECTURE.md - Modules and data flow
- Discord: https://discord.gg/EFkFHbZw