WebUI Pages
Since 1.3.2 (WebUI 1.8.1), a plugin can place a webui.json in its own directory to declare custom pages for the WebUI — a top workspace or a sidebar entry. The host renders the page, so you do not need to install Node, build a frontend, or modify the main program; edit webui.json and reload the plugin to take effect.
The mechanism only describes "what to show and which API to bind". A page can call only your own plugin APIs; it cannot inject HTML / JS / CSS or request arbitrary addresses.
Minimal working example
webui.json lives in the plugin directory (next to _manifest.json). The example below declares a top statistics page and a sidebar settings page:
{
"schema_version": 1,
"workspace_title": "Message Stats",
"pages": [
{
"id": "overview",
"title": "Stats Overview",
"placement": "workspace",
"icon": "chart",
"queries": { "summary": { "api": "summary", "version": "1" } },
"content": [
{ "type": "stat", "label": "Messages today", "value": { "source": "summary", "field": "count" } }
]
},
{
"id": "settings",
"title": "Stats Settings",
"placement": "sidebar",
"icon": "settings",
"actions": {
"save": {
"api": "save_limit",
"version": "1",
"parameters": {
"limit": { "type": "integer", "required": true, "minimum": 1, "maximum": 1000 }
},
"confirmation": "Change the stats limit?"
}
},
"content": [
{
"type": "card",
"label": "Stats range",
"children": [
{ "type": "input", "name": "limit", "label": "Limit", "value": 100 },
{ "type": "button", "label": "Save", "action": "save" }
]
}
]
}
]
}The APIs a page binds are registered statically with the SDK @API decorator on your MaiBotPlugin subclass:
from maibot_sdk import API, MaiBotPlugin
class MyPlugin(MaiBotPlugin):
today_count = 0
@API("summary", description="Read-only page data", version="1")
async def summary(self):
return {"count": self.today_count}
@API("save_limit", description="Save the stats limit", version="1")
async def save_limit(self, limit: int):
self.limit = limit
return {"saved": True}Where pages appear
placement—"sidebar"puts the page in the "plugin extensions" group of the Mai workspace;"workspace"puts it in the plugin's own top workspaceworkspace_title— the name of the workspace that holdsworkspacepages; a plugin may have severalworkspacepages, and the first one is the default, with the rest going into the top "more" overflow menuid/title/description— page identifier, title, and description. The host generates the path as/extensions/{plugin_id}/{page_id}; a plugin cannot define arbitrary routes or override built-in entriesicon— only the five built-in iconspuzzle,chart,settings,database, andlistare accepted- Entry ordering — users can reorder entries or hide a whole plugin's entries in the "manage plugin pages" area at the bottom of the "Plugin Extensions" page (
/plugin-config); that preference is stored only in the current browser and only affects display, not backend authorization
Two independent channels from "plugin configuration"
webui.json only handles custom pages. It is not a manifest field, does not need to be added to capabilities, and its bound APIs do not need public=True. A plugin's config.toml and its auto-generated config form are still handled by the Plugin Configuration page; the two do not affect each other.
The official example plugin hello_world_plugin ships two pages (a top overview and a sidebar greeting) as a minimal reference; see its webui.json for the complete declaration, with page paths like /extensions/maibot-team.hello-world-plugin/overview.
Local debugging
To try custom pages from source, build dashboard and set MAIBOT_WEBUI_USE_LOCAL_DASHBOARD=1 before starting the main program so the WebUI uses the local build; the frontend dev server uses port 7999, and the backend needs a main-program restart the first time the extension routes are added. A normally installed maibot-dashboard does not update automatically with source changes.
Data and actions
Each page declares the APIs it calls through queries (read-only) and actions (writes):
- API names are the short names of your own plugin's APIs;
versiondefaults to"1"when omitted and must match the version registered with@APIexactly queriesrun in order when the page opens and refreshes; after anactionscall succeeds, queries run again to refresh the data- APIs must belong to the current plugin and be enabled; cross-plugin calls, dynamic APIs, and arbitrary request addresses are unsupported (
public=Truedoes not automatically expose an API to the WebUI) - Action parameters are taken from the current form fields according to
parameters; optional fields left empty are not sent. Parameter types arestring/integer/number/boolean, with optionalrequired,max_length(up to 4000),minimum,maximum, andchoices; there is no implicit type conversion, and undeclared parameters are rejected - Dangerous actions must declare a
confirmationmessage (so must buttons withvariant: "danger"), and the host shows a confirm dialog. Confirmation only prevents misclicks; it is not independent authorization or business validation
Keep queries read-only
The host cannot tell whether a Python method has side effects, so it relies on the declaration. Put anything with side effects in actions, or refreshing the page will repeat the write.
Components
A page is a component tree under content. All colors, spacing, fonts, dark mode, and dashboard styling are controlled by the host; components do not accept className, style, or HTML slots:
stack— vertical layout, attributeslabel,childrengrid— grid layout, attributeslabel,columns(1–4 columns, single column on mobile),childrencard— host card container, attributeslabel,childrentabs— tab pages; every child node must have alabeltext— plain text, attributeslabel,value; HTML is not executedstat— stat card, attributeslabel,valuetable— table, attributeslabel,value,columns.columnslooks like[{ "field": "name", "label": "Name" }]; it binds an array of objects, 50 rows per pagechart— line or bar chart, attributeslabel,value,chart_type(line/bar),x,y. It binds an array of objects, where x is a string or number, y is a number, and there are at most 2000 rowsinput— text or number input, attributesname,label,value; a numeric default renders a number inputdate— date input, attributesname,label,valueselect— dropdown, attributesname,label,value,options.optionslooks like[{ "label": "Week", "value": "week" }], andvaluemust be a non-empty stringswitch— toggle, attributesname,label,value;valuemust be a booleanbutton— button, attributeslabel,action,variant(primary/danger/muted)
value may be a scalar or a data reference { "source": "query alias", "field": "totals.count" }: source must be an alias declared in this page's queries, and an empty field means the query's full return value. Tables and charts must use data references; computed expressions and scripts are unsupported.
Data validation before rendering (done by the frontend; when it fails the whole page shows an invalidData error rather than silently truncating):
tablebinds an array of objects, 50 rows per pagechartbinds an array of objects, at most 2000 rows;xmust be a string or number andymust be a number;chart_typeisline/barswitch'svaluemust be a boolean
Limits and lifecycle
webui.jsonmay be at most 128 KiB, and the host registration payload at most 512 KiB; out-of-bounds paths and symlinked files are rejected- At most 20 pages per plugin; at most 200 components, 8 nesting levels, 10 queries, and 20 actions per page
- At most 30 scalar parameters per binding; at most two in-flight WebUI requests per plugin, so a page's
queriesrun in order and the page data is replaced with the new snapshot only when all of them succeed - API calls time out after 10 seconds, and responses may be at most 512 KiB, 12 levels deep, and 20000 elements. A timeout does not mean the write did not happen; implement idempotency in your own API if you need it
- If a declaration is invalid or references an unregistered API, that plugin's registration fails for this load and the reason is logged; plugins without
webui.jsonare unaffected - Pages and entries go offline when the plugin is uninstalled or disabled; browser navigation updates within 30 seconds, or refresh manually
Verification & troubleshooting
Verify — put webui.json in the plugin directory and reload the plugin; an entry appears under "plugin extensions" in the sidebar or in the top workspace. If the page opens with data, the API bindings are correct.
No entry visible?
- Confirm the plugin is loaded and enabled and that the log has no registration failure for its WebUI declaration
- A
workspacepage lives in the plugin's own top workspace — do not look only in the sidebar
Page errors or empty data?
- Check that the API short names and
versioninqueries/actionsmatch the@APIregistration exactly - Check that the
valuedata reference'ssourceis an alias declared on this page and that thefieldpath exists in the return value
Button does nothing?
- Check that
actionpoints to an alias in this page'sactionsand that the parameters satisfyparameterstypes and ranges - Dangerous actions must declare
confirmation, or the declaration fails to register