| name | openclaw-control-center |
| description | Run a local observability and control dashboard for OpenClaw AI agents with real-time collaboration, task management, and safety-first defaults. |
| triggers | ["set up openclaw control center dashboard","create openclaw observability interface","monitor openclaw agents and tasks","implement openclaw collaboration hall","configure openclaw control center","debug openclaw agent activity","track openclaw usage and spend","manage openclaw task approvals"] |
OpenClaw Control Center
Skill by ara.so — Hermes Skills collection.
OpenClaw Control Center is a TypeScript-based local dashboard that transforms OpenClaw from a black box into a transparent control center. It provides real-time observability for agent activity, token usage, task execution, multi-agent collaboration, and approval workflows—all with safety-first defaults (read-only mode, local token auth, and disabled mutations by default).
What It Does
- Overview: Health dashboard showing agent status, decisions waiting, and operator summaries
- Collaboration Hall: Multi-agent chat workspace with live discussion, assignment, handoff, and review
- Staff Management: Real-time view of active vs. queued agents
- Task Management: Execution chains, approvals, runtime evidence, and blocked work
- Usage Tracking: Token consumption, spend trends, context pressure, quota monitoring
- Memory & Documents: Source-backed workbenches for agent memory and markdown documents
- Safety First: Read-only by default, local token auth required, mutation routes disabled
Installation
git clone https://github.com/TianyiDataScience/openclaw-control-center.git
cd openclaw-control-center
npm install
cp .env.example .env
npm run build
npm test
npm run smoke:ui
npm run smoke:hall
npm run dev:ui
Configuration
Environment Variables (.env)
READONLY_MODE=true
LOCAL_TOKEN_AUTH_REQUIRED=true
LOCAL_API_TOKEN=your-long-random-secret-here
IMPORT_MUTATION_ENABLED=false
IMPORT_MUTATION_DRY_RUN=false
APPROVAL_ACTIONS_ENABLED=false
APPROVAL_ACTIONS_DRY_RUN=true
UI_PORT=4310
UI_BIND_ADDRESS=127.0.0.1
OPENCLAW_CONTROL_UI_URL=http://<tailscale-host>:4310/
Directory Structure
control-center/
├── src/
│ ├── ui/ # Frontend TypeScript/React components
│ ├── server/ # Backend API routes
│ ├── lib/ # Shared utilities
│ └── types/ # TypeScript definitions
├── docs/
│ ├── assets/ # Screenshots and images
│ └── FAQ.md # Troubleshooting guide
├── HALL.md # Shared collaboration style guide
└── .env # Local configuration
Key Commands
Development
npm run dev:ui
npm run dev
npm run build
npm test
npm run smoke:ui
npm run smoke:hall
npm run lint
npm run lint:fix
Accessing the UI
http://127.0.0.1:4310/?section=overview&lang=en
http://127.0.0.1:4310/?section=overview&lang=zh
http://127.0.0.1:4310/?section=collaboration&lang=en
http://127.0.0.1:4310/?section=staff&lang=en
http://127.0.0.1:4310/?section=tasks&lang=en
Real Code Examples
Enabling Write Operations
READONLY_MODE=false
LOCAL_TOKEN_AUTH_REQUIRED=true
LOCAL_API_TOKEN=generate-a-secure-random-token-here
IMPORT_MUTATION_ENABLED=true
APPROVAL_ACTIONS_ENABLED=true
APPROVAL_ACTIONS_DRY_RUN=false
API Authentication Pattern
interface ApiRequest {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
requiresAuth?: boolean;
}
async function callApi({ endpoint, method, body, requiresAuth = true }: ApiRequest) {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (requiresAuth) {
const token = process.env.LOCAL_API_TOKEN;
if (!token) {
throw new Error('LOCAL_API_TOKEN not configured');
}
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`http://127.0.0.1:4310${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.) {
();
}
response.();
}
() {
({
: ,
: ,
: updates,
: ,
});
}
Collaboration Hall Workflow
interface HallMessage {
id: string;
taskId: string;
agentId: string;
content: string;
phase: 'discussion' | 'execution' | 'review' | 'blocked';
timestamp: string;
mentions?: string[];
}
interface ExecutionOrder {
taskId: string;
owners: string[];
currentOwnerIndex: number;
nextHandoff?: string;
}
async function createHallTask(content: string, roster: string[]) {
const task = await callApi({
endpoint: '/api/hall/tasks',
method: 'POST',
body: { content, phase: 'discussion' },
});
await (task., );
executionOrder = ({
: ,
: ,
: {
: [, , ],
},
});
execution = ({
: ,
: ,
});
{ task, executionOrder, execution };
}
Staff Status Monitoring
interface AgentStatus {
agentId: string;
displayName: string;
status: 'active' | 'idle' | 'blocked' | 'queued';
currentTask?: string;
lastActivity: string;
tokenUsage24h: number;
}
async function getStaffStatus(): Promise<AgentStatus[]> {
const response = await callApi({
endpoint: '/api/staff/status',
method: 'GET',
requiresAuth: false,
});
return response.agents.map((agent: any) => ({
agentId: agent.id,
displayName: agent.display_name || agent.id,
status: determineStatus(agent),
currentTask: agent.current_task_id,
lastActivity: agent.last_seen,
: agent. || ,
}));
}
(): [] {
(agent.) ;
(agent.) ;
(agent. > ) ;
;
}
Task Approval Flow
interface ApprovalRequest {
taskId: string;
action: 'approve' | 'reject' | 'request-changes';
comment?: string;
reviewerId: string;
}
async function handleTaskApproval(request: ApprovalRequest) {
if (process.env.APPROVAL_ACTIONS_ENABLED !== 'true') {
throw new Error('Approval actions are disabled');
}
const isDryRun = process.env.APPROVAL_ACTIONS_DRY_RUN === 'true';
const response = await callApi({
endpoint: `/api/tasks/${request.taskId}/approve`,
method: 'POST',
body: {
action: request.action,
comment: request.comment,
reviewer_id: request.reviewerId,
dry_run: isDryRun,
},
requiresAuth: true,
});
if (isDryRun) {
.(, request., request.);
}
response;
}
Usage Tracking
interface UsageMetrics {
period: 'today' | '7d' | '30d';
tokenCount: number;
estimatedCost: number;
topConsumers: Array<{ agentId: string; tokens: number }>;
contextPressure: Array<{ sessionId: string; usage: number; limit: number }>;
}
async function getUsageMetrics(period: UsageMetrics['period']): Promise<UsageMetrics> {
const response = await callApi({
endpoint: `/api/usage/metrics?period=${period}`,
method: 'GET',
requiresAuth: false,
});
return {
period,
tokenCount: response.total_tokens,
estimatedCost: response.estimated_cost_usd,
topConsumers: response.agents
.sort( b. - a.)
.(, ),
: response..(
s. / s. >
),
};
}
Common Patterns
Hall Collaboration Workflow
async function runHallCollaboration(taskDescription: string) {
const task = await callApi({
endpoint: '/api/hall/tasks',
method: 'POST',
body: { content: taskDescription, phase: 'discussion' },
});
console.log('Waiting for agent discussion...');
await pollUntil(
async () => {
const messages = await callApi({
endpoint: `/api/hall/tasks/${task.id}/messages`,
method: 'GET',
requiresAuth: false,
});
return messages.length >= 2;
},
{ intervalMs: 2000, timeoutMs: 60000 }
);
await callApi({
endpoint: `/api/hall/tasks/${task.id}/execution-order`,
: ,
: {
: [, , ],
},
});
({
: ,
: ,
});
eventSource = (
);
eventSource. = {
update = .(event.);
.();
};
(
() => {
status = ({
: ,
: ,
: ,
});
status. === || status. === ;
},
{ : , : }
);
eventSource.();
}
(): <> {
startTime = .();
(!( ())) {
(.() - startTime > options.) {
();
}
( (resolve, options.));
}
}
Memory Inspection
async function inspectAgentMemory(agentId: string) {
const status = await callApi({
endpoint: `/api/memory/${agentId}/status`,
method: 'GET',
requiresAuth: false,
});
console.log(`Memory status for ${agentId}:`, {
isUsable: status.is_usable,
isSearchable: status.is_searchable,
fileCount: status.file_count,
totalSizeKb: status.total_size_kb,
});
const dailyMemory = await callApi({
endpoint: `/api/memory/${agentId}/daily`,
method: 'GET',
requiresAuth: false,
});
console.log('Recent daily entries:', dailyMemory.entries.slice(-5));
if (status.is_searchable) {
searchResults = ({
: ,
: ,
: ,
});
.(, searchResults.);
}
}
Document Management
async function updateSharedDocument(filename: string, content: string) {
if (process.env.READONLY_MODE === 'true') {
throw new Error('Cannot save in READONLY_MODE');
}
return callApi({
endpoint: '/api/documents/shared',
method: 'PUT',
body: {
filename,
content,
},
requiresAuth: true,
});
}
async function getAgentDocument(agentId: string, docType: string) {
return callApi({
endpoint: `/api/documents/agent/${agentId}/${docType}`,
method: 'GET',
requiresAuth: false,
});
}
Troubleshooting
"Role not defined in workspace"
Add agent definitions to ~/.openclaw/openclaw.json:
{
"agents": [
{
"id": "agent-manager",
"display_name": "Manager",
"role": "Coordinates work and assigns tasks"
},
{
"id": "agent-builder",
"display_name": "Builder",
"role": "Implements features and writes code"
}
]
}
Connection Issues
Check connection health in Settings → Connection health card. Common issues:
const health = await callApi({
endpoint: '/api/health',
method: 'GET',
requiresAuth: false,
});
console.log('OpenClaw connection:', health.openclaw_reachable);
console.log('Memory accessible:', health.memory_accessible);
console.log('Task storage:', health.task_storage_ready);
Authentication Failures
Ensure LOCAL_API_TOKEN is set and matches in both .env and UI:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
LOCAL_API_TOKEN=<generated-token>
Hall Execution Not Starting
- Verify agents are defined in OpenClaw roster
- Check that execution order is saved before starting
- Ensure at least one owner is in the queue
- Check logs for runtime dispatch errors
const taskState = await callApi({
endpoint: `/api/hall/tasks/${taskId}`,
method: 'GET',
requiresAuth: false,
});
console.log('Phase:', taskState.phase);
console.log('Owners:', taskState.execution_order?.owners);
console.log('Current owner index:', taskState.execution_order?.current_owner_index);
High Context Pressure
Monitor context usage in Usage → Context pressure card:
const contextMetrics = await callApi({
endpoint: '/api/usage/context-pressure',
method: 'GET',
requiresAuth: false,
});
const critical = contextMetrics.sessions.filter(
(s: any) => s.usage / s.limit > 0.9
);
console.log('Critical sessions:', critical);
Security Best Practices
- Keep defaults: Don't disable
READONLY_MODE or LOCAL_TOKEN_AUTH_REQUIRED unless necessary
- Strong tokens: Use
crypto.randomBytes(32).toString('hex') for LOCAL_API_TOKEN
- Network binding: Keep
UI_BIND_ADDRESS=127.0.0.1 unless you need remote access
- Dry-run first: Test approval actions with
APPROVAL_ACTIONS_DRY_RUN=true before enabling live mutations
- Monitor Security risk summary: Check Settings page for current risk assessment
Further Resources