Declare "mode": "manifest" and a list of pages with widgets. rpc_method values are the names you registered with @plugin.on_dashboard(…):
{
"mode": "manifest",
"pages": [
{
"id": "overview",
"title": "Overview",
"widgets": [
{
"id": "active_users",
"type": "stat_card",
"label": "Active users",
"icon": "📊",
"width": "quarter",
"rpc_method": "get_active_users"
},
{
"id": "messages_chart",
"type": "chart",
"chart_type": "line",
"title": "Messages per day",
"refresh_seconds": 30,
"rpc_method": "get_messages_chart"
},
{
"id": "settings_form",
"type": "form",
"title": "Settings",
"rpc_method": "get_settings",
"save_rpc_method": "save_settings",
"fields": [
{"key": "channel_id", "label": "Welcome channel", "type": "text"},
{"key": "message", "label": "Welcome text", "type": "textarea"}
]
}
]
}
]
}
- Widths:
"quarter","third","half","two_thirds","full"(default). - Auto-refresh:
"refresh_seconds": 30re-fetches periodically; a manual refresh button appears on every widget. - Chart types:
line,bar,area,scatter,pie,doughnut,gauge. Charts also honor"chart_options"withcolors(array),show_legend,x_axis_labelandy_axis_label. - Form field types:
text(default),textarea,number,select,multi_select,toggle,color,date,range. Every field renderslabelandhelp_text.placeholderapplies to text, textarea and number.requiredis enforced on every type except toggle, color and range.selectandmulti_selectneed"options": [{"value": "...", "label": "..."}].numberandrangeacceptmin,maxandstep. - Extra RPC args:
"rpc_params": {"stat": "total"}is merged into theparamsyour handler receives. - Shared handlers: a widget may set
"path"(e.g."kpis.0"or"activity") to extract its slice from a bigger RPC response, so oneget_overviewhandler can drive a whole page of widgets. - Access control: pages and widgets accept
"permission": "viewer" | "manager" | "owner"; manifest values can only raise the bar, never lower it. Saves always require at least manager, enforced server-side regardless of what your UI shows.
Prefer Python over raw JSON? yourbot_sdk.dashboard ships a fluent builder for every widget type — Manifest, Page, StatCard, Chart (with Chart.line / Chart.bar / Chart.pie / Chart.gauge factories), Table, List, ProgressBar, Text, Markdown, Alert and Form (with per-field helpers like .text(), .select(), .toggle()), plus Column and Option for tables and selects. They emit exactly this JSON, and invalid widths / permissions / chart types raise ValueError at build time instead of failing at render.
Supported widget types
| Type | Handler returns |
|---|---|
| stat_card | {"value": 1234, "change": "+12%", "trend": "up", "color": "green"} (trend: up / down / flat; color: green / red / gold / cyan) |
| chart | {"labels": [...], "series": [{"name": "...", "data": [...]}]} |
| table | {"rows": [{...}, ...], "total": 100, "page_size": 20} — declare "columns": [{"key": "...", "label": "..."}] on the widget (required); headers and cell order come from it. Your handler receives params["page"] (1-indexed) when the viewer pages with Prev/Next |
| list | {"items": [{"label": "...", "value": "..."}, ...]} |
| progress_bar | {"value": 75, "max": 100, "label": "…"} |
| text | {"content": "Plain text content"} |
| markdown | {"markdown": "# Heading\n- bullet"} (safe markdown, raw HTML is stripped) |
| alert | {"level": "info", "message": "…"} — level: info / warning / error / success |
| form | get → {"values": {...}}; save → {"ok": true} or {"ok": false, "error": "…"} |
Python handlers
@plugin.on_dashboard("get_active_users")
def active_users(ctx: Context, params: dict):
n = ctx.kv.count(prefix="active_user:")
return {"value": n, "change": "+12%", "trend": "up"}
@plugin.on_dashboard("get_messages_chart")
def messages_chart(ctx: Context, params: dict):
return ctx.metrics.query("messages", period="7d")
@plugin.on_dashboard("get_settings")
def get_settings(ctx: Context, params: dict):
return {"values": {
"channel_id": ctx.kv.get("welcome_channel") or "",
"message": ctx.kv.get("welcome_message") or "Welcome!",
}}
@plugin.on_dashboard("save_settings")
def save_settings(ctx: Context, params: dict):
# Form saves arrive as params["values"], keyed by each field's "key".
values = params.get("values") or {}
message = str(values.get("message", "")).strip()
if len(message) > 500:
return {"ok": False, "error": "Keep the welcome text under 500 characters"}
ctx.kv.set("welcome_channel", values.get("channel_id", ""))
ctx.kv.set("welcome_message", message)
return {"ok": True}
- 8-second timeout per handler call; the widget shows an error past it.
- Fresh Context per call: every dashboard RPC gets a ctx scoped to the server the viewer is looking at, even in pool mode (one shared worker serves every server that installed your plugin).
paramscarriesdiscord_srv_id(delivered as a string — compare withstr(params.get("discord_srv_id"))to stay safe across paths) plus the viewer's identity:discord_user_id,viewer_user_id,viewer_usernameandusername. - Read caching: widget responses are cached ~5 seconds per (plugin, server, method, params); each table page caches separately and writes bypass the cache.
- Serialized: only one dashboard RPC runs at a time per worker, and handlers share the process with your event handlers.
- Cold start: if no worker is running for your plugin yet, the widget shows “Starting plugin…” and retries up to 5 times at 1.5-second intervals.