| name | glance |
| description | Create, update, and manage Glance dashboard widgets. Use when user wants to: add something to their dashboard, create a widget, track data visually, show metrics/stats, display API data, or monitor usage. |
| metadata | {"openclaw":{"emoji":"🖥️","homepage":"https://github.com/acfranzen/glance","requires":{"env":["GLANCE_URL"],"bins":["curl"]},"primaryEnv":"GLANCE_URL"}} |
Glance
AI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection.
Features
- Custom Widgets — Create widgets via AI with auto-generated JSX
- Agent Refresh — AI collects data on schedule and pushes to cache
- Dashboard Export/Import — Share widget configurations
- Credential Management — Secure API key storage
- Real-time Updates — Webhook-triggered instant refreshes
Quick Start
cd "$(clawhub list | grep glance | awk '{print $2}')"
git clone https://github.com/acfranzen/glance ~/.glance
cd ~/.glance
npm install
cp .env.example .env.local
npm run dev
npm run build && npm start
Dashboard runs at http://localhost:3333
Configuration
Edit .env.local:
PORT=3333
AUTH_TOKEN=your-secret-token
OPENCLAW_GATEWAY_URL=https://localhost:18789
OPENCLAW_TOKEN=your-gateway-token
DATABASE_PATH=./data/glance.db
Service Installation (macOS)
cat > ~/Library/LaunchAgents/com.glance.dashboard.plist << 'EOF'
<?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.glance.dashboard</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/npm</string>
<string>run</string>
<string>dev</string>
</array>
<key>WorkingDirectory</key>
<string>~/.glance</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>~/.glance/logs/stdout.log</string>
<key>StandardErrorPath</key>
<string>~/.glance/logs/stderr.log</string>
</dict>
</plist>
EOF
mkdir -p ~/.glance/logs
launchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist
launchctl start com.glance.dashboard
launchctl stop com.glance.dashboard
launchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist
Environment Variables
| Variable | Description | Default |
|---|
PORT | Server port | 3333 |
AUTH_TOKEN | Bearer token for API auth | — |
DATABASE_PATH | SQLite database path | ./data/glance.db |
OPENCLAW_GATEWAY_URL | OpenClaw gateway for webhooks | — |
OPENCLAW_TOKEN | OpenClaw auth token | — |
Requirements
- Node.js 20+
- npm or pnpm
- SQLite (bundled)
Widget Skill
Create and manage dashboard widgets. Most widgets use agent_refresh — you collect the data.
Quick Start
curl -s -H "Origin: $GLANCE_URL" "$GLANCE_URL/api/widgets" | jq '.custom_widgets[].slug'
sqlite3 $GLANCE_DATA/glance.db "SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'"
curl -X POST "$GLANCE_URL/api/widgets/my-widget/cache" \
-H "Content-Type: application/json" \
-H "Origin: $GLANCE_URL" \
-d '{"data": {"value": 42, "fetchedAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}}'
browser action:open targetUrl:"$GLANCE_URL"
AI Structured Output Generation (REQUIRED)
When generating widget definitions, use the JSON Schema at docs/schemas/widget-schema.json with your AI model's structured output mode:
- Anthropic: Use
tool_use with the schema
- OpenAI: Use
response_format: { type: "json_schema", schema }
The schema enforces all required fields at generation time — malformed widgets cannot be produced.
Required Fields Checklist
Every widget MUST have these fields (the schema enforces them):
| Field | Type | Notes |
|---|
name | string | Non-empty, human-readable |
slug | string | Lowercase kebab-case (my-widget) |
source_code | string | Valid JSX with Widget function |
default_size | { w: 1-12, h: 1-20 } | Grid units |
min_size | { w: 1-12, h: 1-20 } | Cannot resize smaller |
fetch.type | enum | "server_code" | "webhook" | "agent_refresh" |
fetch.instructions | string | REQUIRED if type is agent_refresh |
fetch.schedule | string | REQUIRED if type is agent_refresh (cron) |
data_schema.type | "object" | Always object |
data_schema.properties | object | Define each field |
data_schema.required | array | MUST include "fetchedAt" |
credentials | array | Use [] if none needed |
Example: Minimal Valid Widget
{
"name": "My Widget",
"slug": "my-widget",
"source_code": "function Widget({ serverData }) { return <div>{serverData?.value}</div>; }",
"default_size": { "w": 2, "h": 2 },
"min_size": { "w": 1, "h": 1 },
"fetch": {
"type": "agent_refresh",
"schedule": "*/15 * * * *",
"instructions": "## Data Collection\nCollect the data...\n\n## Cache Update\nPOST to /api/widgets/my-widget/cache"
},
"data_schema": {
⚠️ Widget Creation Checklist (MANDATORY)
Every widget must complete ALL steps before being considered done:
□ Step 1: Create widget definition (POST /api/widgets)
- source_code with Widget function
- data_schema (REQUIRED for validation)
- fetch config (type + instructions for agent_refresh)
□ Step 2: Add to dashboard (POST /api/widgets/instances)
- custom_widget_id matches definition
- title and config set
□ Step 3: Populate cache (for agent_refresh widgets)
- Data matches data_schema exactly
- Includes fetchedAt timestamp
□ Step 4: Set up cron job (for agent_refresh widgets)
- Simple message: "⚡ WIDGET REFRESH: {slug}"
- Appropriate schedule (*/15 or */30 typically)
□ Step 5: BROWSER VERIFICATION (MANDATORY)
- Open http://localhost:3333
- Widget is visible on dashboard
- Shows actual data (not loading spinner)
- Data values match what was cached
- No errors or broken layouts
⛔ DO NOT report widget as complete until Step 5 passes!
Quick Reference
Widget Package Structure
Widget Package
├── meta (name, slug, description, author, version)
├── widget (source_code, default_size, min_size)
├── fetch (server_code | webhook | agent_refresh)
├── dataSchema? (JSON Schema for cached data - validates on POST)
├── cache (ttl, staleness, fallback)
├── credentials[] (API keys, local software requirements)
├── config_schema? (user options)
└── error? (retry, fallback, timeout)
Fetch Type Decision Tree
Is data available via API that the widget can call?
├── YES → Use server_code
└── NO → Does an external service push data?
├── YES → Use webhook
└── NO → Use agent_refresh (YOU collect it)
| Scenario | Fetch Type | Who Collects Data? |
|---|
| Public/authenticated API | server_code | Widget calls API at render |
| External service pushes data | webhook | External service POSTs to cache |
| Local CLI tools | agent_refresh | YOU (the agent) via PTY/exec |
| Interactive terminals | agent_refresh | YOU (the agent) via PTY |
| Computed/aggregated data | agent_refresh | YOU (the agent) on a schedule |
⚠️ agent_refresh means YOU are the data source. You set up a cron to remind yourself, then YOU collect the data using your tools (exec, PTY, browser, etc.) and POST it to the cache.
API Endpoints
Widget Definitions
| Method | Endpoint | Description |
|---|
POST | /api/widgets | Create widget definition |
GET | /api/widgets | List all definitions |
GET | /api/widgets/:slug | Get single definition |
PATCH | /api/widgets/:slug | Update definition |
DELETE | /api/widgets/:slug | Delete definition |
Widget Instances (Dashboard)
| Method | Endpoint | Description |
|---|
POST | /api/widgets/instances | Add widget to dashboard |
GET | /api/widgets/instances | List dashboard widgets |
PATCH | /api/widgets/instances/:id | Update instance (config, position) |
DELETE | /api/widgets/instances/:id | Remove from dashboard |
Credentials
| Method | Endpoint | Description |
|---|
GET | /api/credentials | List credentials + status |
POST | /api/credentials | Store credential |
DELETE | /api/credentials/:id | Delete credential |
Creating a Widget
Full Widget Package Structure
{
"name": "GitHub PRs",
"slug": "github-prs",
"description": "Shows open pull requests",
"source_code": "function Widget({ serverData }) { ... }",
"default_size": { "w": 2, "h": 2 },
"min_size": { "w": 1, "h": 1 },
"refresh_interval": 300,
"credentials": [
{
"id": "github",
"type": "api_key",
"name":
Fetch Types
| Type | When to Use | Data Flow |
|---|
server_code | Widget can call API directly | Widget → server_code → API |
agent_refresh | Agent must fetch/compute data | Agent → POST /cache → Widget reads |
webhook | External service pushes data | External → POST /cache → Widget reads |
Most widgets should use agent_refresh — the agent fetches data on a schedule and pushes to the cache endpoint.
Step 1: Create Widget Definition
POST /api/widgets
Content-Type: application/json
{
"name": "GitHub PRs",
"slug": "github-prs",
"description": "Shows open pull requests",
"source_code": "function Widget({ serverData }) { ... }",
"default_size": { "w": 2, "h": 2 },
"credentials": [...],
"fetch": { "type": "agent_refresh", "schedule": "*/5 * * * *", ... },
"data_schema": {
"type": "object",
"properties": {
"prs": { "type": "array", "description": "List of PR objects" },
"fetchedAt": { "type": "string", "format": "date-time" }
},
"required": ["prs", "fetchedAt"]
},
"cache": { "ttl_seconds": 300, ... }
}
data_schema (REQUIRED) defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.
⚠️ Always include data_schema when creating widgets. This ensures:
- Data validation on cache POSTs (400 on schema mismatch)
- Clear documentation of expected data structure
- AI agents know the exact format to produce
Step 2: Add to Dashboard
POST /api/widgets/instances
Content-Type: application/json
{
"type": "custom",
"title": "GitHub PRs",
"custom_widget_id": "cw_abc123",
"config": { "owner": "acfranzen", "repo": "libra" }
}
Step 3: Populate Cache (for agent_refresh)
POST /api/widgets/github-prs/cache
Content-Type: application/json
{
"data": {
"prs": [...],
"fetchedAt": "2026-02-03T14:00:00Z"
}
}
⚠️ If the widget has a dataSchema, the cache endpoint validates your data against it. Bad data returns 400 with details. Always check the widget's schema before POSTing:
GET /api/widgets/github-prs
# Response includes dataSchema showing required fields and types
Step 4: Browser Verification (REQUIRED)
⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.
Never consider a widget "done" until you've visually confirmed it renders correctly on the dashboard.
browser({
action: 'open',
targetUrl: 'http://localhost:3333',
profile: 'openclaw'
});
browser({ action: 'snapshot' });
Verification checklist (must ALL be true):
Common issues and fixes:
| Symptom | Cause | Fix |
|---|
| "Waiting for data..." | Cache empty | POST data to /api/widgets/{slug}/cache |
| Widget not visible | Not added to dashboard | POST /api/widgets/instances |
| Wrong/old data | Slug mismatch | Check slug matches between definition and cache POST |
| Broken layout | Bad JSX in source_code | Check widget code for syntax errors |
| "No data" after POST | Schema validation failed | Check data matches data_schema |
If verification fails, fix the issue before reporting success.
Widget Code Template (agent_refresh)
For agent_refresh widgets, use serverData prop (NOT useData hook):
function Widget({ serverData }) {
const data = serverData;
const loading = !serverData;
const error = serverData?.error;
if (loading) return <Loading message="Waiting for data..." />;
if (error) return <ErrorDisplay message={error} />;
return (
<div className="space-y-3">
<List items={data.prs?.map(pr => ({
title: pr.title,
subtitle: `#${pr.number} by ${pr.author}`,
badge: pr.state
})) || []} />
</div>
);
}
Important: The widget wrapper (CustomWidgetWrapper) provides:
- Outer
<Card> container with header (widget title)
- Refresh button and "Updated X ago" footer
- Loading/error states
Your widget code should just render the content — no Card, no CardHeader, no footer.
Key difference: agent_refresh widgets receive data via serverData prop, NOT by calling useData(). The agent pushes data to /api/widgets/{slug}/cache.
Server Code (Legacy Alternative)
Prefer agent_refresh over server_code. Only use server_code when the widget MUST execute code at render time (rare).
const token = await getCredential('github');
const response = await fetch('https://api.github.com/repos/owner/repo/pulls', {
headers: { 'Authorization': `Bearer ${token}` }
});
return await response.json();
Available: fetch, getCredential(provider), params, console
Blocked: require, eval, fs, process, global
Agent Refresh Contract
⚠️ CRITICAL: For agent_refresh widgets, YOU (the OpenClaw agent) are the data collector.
This is NOT an external API or service. YOU must:
- Set up a cron job to remind yourself to collect data on a schedule
- Use your own tools (PTY, exec, browser, etc.) to gather the data
- Parse the output into structured JSON
- POST to the cache endpoint so the widget can display it
The Pattern
┌─────────────────────────────────────────────────────────────┐
│ Cron fires → Agent wakes up → Agent collects data → │
│ Agent POSTs to /cache → Widget displays fresh data │
└─────────────────────────────────────────────────────────────┘
Step-by-Step for agent_refresh Widgets
- Create the widget with
fetch.type = "agent_refresh" and detailed fetch.instructions
- Set up a cron job targeting YOUR main session (message is just the slug):
cron.add({
name: "Widget: My Data Refresh",
schedule: { kind: "cron", expr: "*/15 * * * *" },
payload: {
kind: "systemEvent",
text: "⚡ WIDGET REFRESH: my-widget"
},
sessionTarget: "main"
})
- When you receive the refresh message, look up
fetch.instructions from the DB and spawn a subagent:
const slug = message.replace('⚡ WIDGET REFRESH:', '').trim();
const widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);
sessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });
- The subagent collects the data using your tools:
exec for shell commands
- PTY for interactive CLI tools (like
claude /status)
browser for web scraping
Writing Excellent fetch.instructions
The fetch.instructions field is the single source of truth for how to collect widget data. Write them clearly so any subagent can follow them.
Required sections:
## Data Collection
Exact commands to run with full paths and flags.
Include PTY requirements if interactive.
## Data Transformation
Exact JSON structure expected.
Include field descriptions and examples.
## Cache Update
Full URL, required headers, body format.
## Browser Verification
Confirm the widget renders correctly.
Good example:
## Data Collection
```bash
gog gmail search "in:inbox" --json
Data Transformation
Take first 5-8 emails, generate AI summary (3-5 words) for each:
{
"emails": [{"id": "...", "from": "...", "subject": "...", "summary": "AI summary here", "unread": true}],
"fetchedAt": "ISO timestamp"
}
Cache Update
POST to: http://localhost:3333/api/widgets/recent-emails/cache
Header: Origin: http://localhost:3333
Body: { "data": }
Browser Verification
Open http://localhost:3333 and confirm widget shows emails with AI summaries.
**Bad example (too vague):**
Get emails and post to cache.
### Real Example: Claude Max Usage Widget
This widget shows Claude CLI usage stats. The data comes from running `claude` in a PTY and navigating to `/status → Usage`.
**The agent's job every 15 minutes:**
- Spawn PTY: exec("claude", { pty: true })
- Send: "/status" + Enter
- Navigate to Usage tab (Right arrow keys)
- Parse the output: Session %, Week %, Extra %
- POST to /api/widgets/claude-code-usage/cache
- Kill the PTY session
- ⚠️ VERIFY: Open browser to http://localhost:3333 and confirm widget displays new data
**This is YOUR responsibility as the agent.** The widget just displays whatever data is in the cache.
### Subagent Task Template for Refreshes
When spawning subagents for widget refreshes, always include browser verification:
```javascript
sessions_spawn({
task: `${fetchInstructions}
## REQUIRED: Browser Verification
After posting to cache, verify the widget renders correctly:
1. Open http://localhost:3333 in browser
2. Find the widget on the dashboard
3. Confirm it shows the data you just posted
4. Report any rendering issues
Do NOT report success until browser verification passes.`,
model: 'haiku',
label: `${slug}-refresh`
});
Cache Endpoint
POST /api/widgets/{slug}/cache
Content-Type: application/json
{
"data": {
"packages": 142,
"fetchedAt": "2026-02-03T18:30:00.000Z"
}
}
Immediate Refresh via Webhook
For agent_refresh widgets, users can trigger immediate refreshes via the UI refresh button.
When configured with OPENCLAW_GATEWAY_URL and OPENCLAW_TOKEN environment variables, clicking the refresh button will:
- Store a refresh request in the database (fallback for polling)
- Immediately POST a wake notification to OpenClaw via
/api/sessions/wake
- The agent receives a prompt to refresh that specific widget now
This eliminates the delay of waiting for the next heartbeat poll.
(add to ):