- 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](https://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)
```bash
# Run latest version with npx
npx -y openclaw-studio@latest
# Opens on http://localhost:3000
# Default gateway URL: ws://localhost:18789
```
### From Source
```bash
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run dev
```
### Setup Helper
```bash
# Configure gateway URL/token without opening UI
npm run studio:setup
```
## Connection Architecture
OpenClaw Studio uses a two-path architecture:
1. **Browser → Studio**: HTTP + SSE (`/api/runtime/*`, `/api/intents/*`)
2. **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)
```bash
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm run dev
# Open http://localhost:3000
# In Studio UI:
# - Upstream URL: ws://localhost:18789
# - Upstream Token: <your-gateway-token>
```
Get gateway token:
```bash
openclaw config get gateway.auth.token
```
#### B. Gateway in Cloud, Studio Local
**Option 1: Tailscale Serve (Recommended)**
On gateway host:
```bash
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:
```bash
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:
```bash
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run dev
# If OpenClaw is on same VPS:
# - Upstream URL: ws://localhost:18789
# - Upstream Token: <gateway-token>
# Expose Studio over Tailscale
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
# Access from anywhere: https://<studio-host>.ts.net
```
### Environment Variables
```bash
# Gateway connection (optional, can set in UI)
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
# Studio access control (required for public binds)
STUDIO_ACCESS_TOKEN=your-secure-token-here
# OpenClaw state directory
OPENCLAW_STATE_DIR=~/.openclaw
# Bind host (default: 127.0.0.1)
HOST=0.0.0.0 # Public bind - requires STUDIO_ACCESS_TOKEN
```
### 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
```bash
# Development mode with auto-restart
npm run dev
# Production build
npm run build
npm run start
# Turbo dev mode
npm run dev:turbo
# Verify/repair native dependencies
npm run verify:native-runtime:repair
npm run verify:native-runtime:check
```
### TypeScript API Patterns
#### Connecting to Gateway
```typescript
// In Studio UI components
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
```typescript
// Server-side SSE streaming
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Subscribe to runtime events
const subscription = runtimeStore.subscribe((event) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
});
// Replay history
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 new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
```
#### Creating Agents
```typescript
// Agent creation API
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', container: '/workspace', readonly: false }
] : []
},
exec_approval_required: config.execApprovalRequired
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create agent');
}
return response.json();
}
```
#### Chat with Streaming
```typescript
// Client-side chat with SSE
import { useEffect, useState } from 'react';
export function ChatInterface({ agentId }: { agentId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [thinking, setThinking] = useState<string[]>([]);
useEffect(() => {
// Connect to runtime stream
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 => [...prev, data.content]);
break;
case 'transcript':
setMessages(prev => [...prev, {
role: data.role,
content: data.content
}]);
setThinking([]);
break;
}
});
return () => eventSource.close();
}, [agentId]);
const sendMessage = async (content: string) => {
await fetch('/api/intents/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
message: content
})
});
};
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{thinking.length > 0 && (
<div className="thinking">
{thinking.map((t, i) => <p key={i}>{t}</p>)}
</div>
)}
<input onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}} />
</div>
);
}
```
## Real-World Examples
### Example 1: Local Development Setup
```bash
#!/bin/bash
# setup-local-dev.sh
# Start OpenClaw Gateway (in separate terminal)
# openclaw gateway start
# Get gateway token
GATEWAY_TOKEN=$(openclaw config get gateway.auth.token)
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
# Configure via environment
export NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
npm run dev
# Open http://localhost:3000
# Enter gateway token in UI
```
### Example 2: Production Cloud Setup
```bash
#!/bin/bash
# deploy-studio-cloud.sh
# On your VPS running OpenClaw Gateway
cd /opt
npx -y openclaw-studio@latest
cd openclaw-studio
# Install dependencies
npm install
# Build for production
npm run build
# Set access token for public access
export STUDIO_ACCESS_TOKEN=$(openssl rand -hex 32)
# Expose over Tailscale
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
# Run production server
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
```typescript
// create-secure-agent.ts
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;
}
// Usage
createSecureCodeAgent().catch(console.error);
```
### Example 4: Cron Job Configuration
```typescript
// configure-cron-job.ts
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,
Voir sur GitHub