The dashboard is a single-page HTML app served by the orchestrator's embedded HTTP server. It lets you launch and observe Council sessions, Autoloop runs, and Forge (Ultraapp) builds from a browser — no CLI, no webchat, no plugin tool calls needed.
URL: http://127.0.0.1:18796/dash (local) or whatever public hostname you
front the embedded server with (the recommended setup uses a path-based
reverse proxy, e.g. https://<your-host>/dash).
| Tab | Backed by | Launch endpoint |
|---|---|---|
| Autoloop | SessionManager.autoloopStart() |
POST /autoloop/new |
| Council | SessionManager.councilStart() |
POST /council/new |
| Forge | UltraappManager.createRun() |
POST /ultraapp/new |
Each tab has a + New button in the sidebar. Council and Autoloop open a
modal form (because they need workspace/task input); Forge POSTs an empty
body and drops you into an interview (the spec is built conversationally).
The recommended way to run the dashboard 24/7 is a separate clawo serve
process under launchd — completely decoupled from the OpenClaw gateway. The
gateway's plugin-side embedded server still works (lazy init on first tool
call); when both processes try to bind the default port, the loser gracefully
skips, so the two coexist without conflict.
Example ~/Library/LaunchAgents/com.clawo.serve.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.clawo.serve</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>5</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/opt/homebrew/bin/clawo</string>
<string>serve</string>
<string>--port</string><string>18796</string>
<string>--host</string><string>127.0.0.1</string>
</array>
<key>StandardOutPath</key>
<string>/Users/USER/.openclaw/logs/clawo-serve.log</string>
<key>StandardErrorPath</key>
<string>/Users/USER/.openclaw/logs/clawo-serve.log</string>
</dict>
</plist>Bootstrap:
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.clawo.serve.plist
launchctl print "gui/$(id -u)/com.clawo.serve" | grep stateThe embedded server self-generates a 32-byte token at startup and writes it
to ~/.openclaw/server-token (mode 0600). Same-user processes on the box
read it and present it as Authorization: Bearer <token> (or
?token=<v> query / clawo_auth cookie).
http://127.0.0.1:18796/dash?token=$(cat ~/.openclaw/server-token)
The server sets a clawo_auth cookie on the first query-token request, so
the bookmark /dash works on subsequent visits.
Don't expose the token to the public internet. Instead, gate the public hostname with whatever auth layer you already trust (CF Access passkey, Tailscale, mTLS, etc.) and have the reverse proxy inject the Bearer token on behalf of the user when forwarding to port 18796. The browser authenticates only against your edge auth; the dashboard's own token stays inside the box.
Example sasha-doctor pattern (matches the user-side setup):
// after the edge auth check passes:
if (!req.headers.authorization) {
req.headers.authorization =
"Bearer " + fs.readFileSync("~/.openclaw/server-token", "utf-8").trim();
}
proxyHTTP(req, res, 18796);The /login?token=...&redirect=/dash endpoint exists as a fallback for
quick one-shot setups (works locally and through proxies that DON'T inject
the Bearer for you), but the proxy-injects-Bearer pattern is preferred
because users never see or paste the token.
Token-file write is deferred to the listen()-success callback so a second
process that loses the EADDRINUSE race does NOT clobber the winner's token.
The token is also re-read from disk on every request so that if a different
clawo instance (test runner, nohup launch, etc.) writes a new value mid-life,
the proxy and the server stay in agreement on the next request — no restart
required.
Opening a run whose status is terminated (because its process has
exited, or because you're viewing it cross-process) no longer hangs on
"Waiting…". The dashboard fetches /autoloop/<id>/chat_history, replays
the conversation into the Planner pane, and surfaces a green Resume
run button in the topbar. Clicking it POSTs /autoloop/<id>/resume;
the orchestrator re-attaches the Planner (reusing the persisted Claude
session ID when available, so Claude's context picks up where it left
off) and the dashboard reconnects to /events for live updates.
Runs that pre-date this feature have no chat.jsonl and no persisted
session — they still resume cleanly, but with a blank Planner pane and a
fresh Claude context. New runs going forward retain both.
When the dashboard runs in a different process from where you spawn runs
(e.g. you started a council via the OpenClaw plugin tool from webchat, but
the dashboard is in clawo serve), the run state is invisible across
in-memory boundaries. The dashboard fixes this by unioning in-memory state
with on-disk records on every list call:
- Councils:
~/.openclaw/council-logs/council-*.md— parsed for- **ID**:,- **Time**:,- **Task**:,- **Status**:headers. Legacy transcripts (pre-v4.0) fall back to a filename-derived id. - Autoloops:
~/.claw-orchestrator/autoloop-registry.jsonl— an append-only JSONL index written byautoloopStart(). Stale entries whose ledger directory no longer exists are filtered out at read time. - Forge:
UltraappStore.listRuns()already reads from disk (~/.claw-orchestrator/ultraapps/).
Result: any run you've ever started — from any process — shows up in the sidebar, sorted newest-first, until the underlying files are deleted.
If you front the embedded server with sasha-doctor (or another reverse
proxy), route these paths to 127.0.0.1:18796:
/dashboard,/dash,/login/autoloop/*,/council/*,/ultraapp/*
The dashboard's relative fetch() calls expect the proxy to preserve the
path verbatim — no prefix stripping. /v1/openclaw/* should keep routing
to the OpenClaw gateway, not the embedded server.
To wipe dashboard state without touching real run data:
# Forget all known autoloops (council/forge unchanged).
rm ~/.claw-orchestrator/autoloop-registry.jsonl
# Force the standalone server to mint a fresh auth token.
launchctl kickstart -k "gui/$(id -u)/com.clawo.serve"
# Then visit /login?token=$(cat ~/.openclaw/server-token)&redirect=/dash once
# to refresh the cookie.