HTML Widgets — Bot-Built Live Dashboards¶
Any bot with tool access can author a live, interactive widget that renders in your chat and (optionally) pins to your dashboard. Ask for "a little panel that shows X" or "a chart of Y over time" and you'll get back an HTML card that runs in a sandboxed iframe, reads from Spindrel's own API, and updates in real time.
These widgets can:
- Run JavaScript (fetch data, handle clicks, redraw)
- Call the same
/api/v1/...endpoints the bot can call from tools - Re-render automatically when the underlying workspace file changes
Think Grafana-like panels, but authored conversationally and scoped to whichever bot you asked.
How it works, end to end¶
- You ask. "Build me a widget that shows the last 10 messages in this channel."
- The bot writes HTML + JS and calls
emit_html_widget. The tool result is an interactive card. - You pin it (star icon on the card) to the channel's dashboard, the global dashboard, or both.
- It keeps working. The widget's JS runs every time you view the dashboard, fetches fresh data, and renders. When it's backed by a workspace file, editing that file updates the widget within a few seconds.
Asking for one¶
There's no special command — just describe what you want. Good prompts:
- "Build me a little dashboard with three panels: recent messages, running tasks, and bot count."
- "Make a widget that shows the last 20 tool calls and their status."
- "Give me a chart of my token spend by day this week."
- "I want a pinned card with a 'Start overnight run' button that kicks off task pipeline X."
The bot will one-shot the HTML, render it in chat, and you decide whether to pin.
The security model — widgets run as the bot, not as you¶
This is the part that matters most. A widget's JavaScript executes with the emitting bot's permissions — not yours.
Concretely:
- Every widget is stamped with
source_bot_idat emit time. - When the iframe renders, Spindrel mints a short-lived (15 min) bearer token scoped to that bot's API key and injects it into the widget's helper.
- Every API call the widget makes carries that bearer — so scoped endpoints enforce the bot's scopes, not yours.
Why it matters: if a widget could use your session, then any bot you use could effectively write JavaScript that runs with your privileges when you view the widget — including admin routes you never intended to let it touch. That's an unacceptable class of footgun, so we closed it by design.
Where you see it: look for the 🤖 @botname chip in the bottom-left corner of every HTML widget card. That's the bot the widget is acting as. Hover for the full tooltip.
What this means for permissions:
- If a widget you asked for returns "Widget auth failed", the bot has no API key configured. Go to Admin → Bots → this bot → API Permissions and provision one with the scopes the widget needs.
- If a widget returns 403 on some endpoint, the bot's key doesn't have that scope. Broaden the bot's scopes (not yours) and refresh.
- Admin scope on your user account does not lend admin powers to widgets. This is intentional.
Token rotation is automatic and transparent: the renderer re-mints every 12 min so long-running widgets keep working, and screenshots of devtools expire quickly.
Two modes: inline vs workspace-backed¶
| Mode | What it is | When to use |
|---|---|---|
| Inline | The bot writes the full HTML body in one shot. Static snapshot. | One-off views ("show me a quick breakdown of…"). |
| Path | The bot saves a full HTML file in the channel workspace (e.g. dashboards/cpu.html) and points the widget at it. |
Iterative dashboards — you tweak, bot edits the file, widget refreshes in ~3 s. |
For anything you expect to evolve, ask for the path-mode flavor. "Save this as dashboards/morning-standup.html so I can tweak it" is enough of a hint.
Where widgets live¶
Widgets can be pinned in three places:
- Inline in chat — right where the bot emitted them; scrolls with the transcript.
- Channel dashboard — the channel's own pinboard (left rail in the OmniPanel + full grid via the
LayoutDashboardicon in the channel header). - Global dashboard (
/widgets/default) — your cross-channel board. Widgets here still authenticate as their originating bot and still show the@botnamechip.
Regardless of where you pin a widget, it continues to act as the bot that emitted it — context travels with the envelope, not with the render host.
When to use what¶
| You want | Best tool |
|---|---|
| A rich, interactive card not in the built-in widget grammar | HTML widget |
| A chart, a table, a custom layout, a live mini-dashboard | HTML widget (usually path mode) |
| An entity detail / toggle / status card | Built-in component widget (see Widget Templates) |
| A one-off textual answer | Just a normal reply |
| A reusable parameterized widget across many channels | Not yet — HTML widgets v1 is per-emission; roadmap item |
Troubleshooting¶
| Symptom | Usual cause | Fix |
|---|---|---|
Widget auth failed: Bot X has no API key configured banner |
The bot has no scoped key to authenticate as | Admin UI → Bots → provision an API key with the scopes the widget needs |
API 403 errors in the widget body |
Bot's key is missing the scope the endpoint requires | Broaden the bot's scopes (not your user's) |
API 422 errors on /api/v1/channels/null/... |
The bot didn't stamp channel context into the widget | Usually a bot bug — the bot should bake window.spindrel.channelId or a literal UUID into the fetch URL. Report the prompt so we can fix the authoring skill. |
| Widget never shows fresh data | The widget isn't polling — ask the bot to add a setInterval or to use path mode with a file that gets edited |
|
| "Updated Xm ago" chip keeps ticking but nothing changes | The widget is polling an endpoint that hasn't moved. Normal. |
Further reading¶
- Widget Dashboards — how named dashboards and channel dashboards work, the OmniPanel rail, grid presets, and editing. HTML widgets pin onto the same boards as component widgets.
- Widget Templates — for component-grammar widgets authored from YAML. Different system, complementary use case.
- Developer API — the endpoints widgets can call (anything your bot's scoped key can hit).
- Custom Tools & Extensions — if you want to ship HTML widget skills with your own integration.