Skip to content

Visualize vault data

A read-only widget runs named queries and renders the rows. Start with a built-in kind; write a custom page when you need your own layout or charts. This page assumes the setup from Build a widget.

Built-in kinds

Set kind and a matching config; the runtime renders into <div id="app"> and handles the empty and not-connected states. Your entry HTML is only the mount point.

Kind Renders config
list One query as a list { kind: "list", query, item: { title, subtitle?, meta? }, empty?, limit? }
record One row as labelled fields { kind: "record", query, fields: [{ label, column, format? }], empty? }
dashboard Several panels { kind: "dashboard", panels: [{ title, query, render, value?, label?, columns?, item? }] }

render is metric, bar, table, or list. Item and field values are column names, optionally with a format: "created_at:reltime". Formats: text, multiline, date, reltime, number.

widgets: [
{
id: "insights",
name: "Insights",
cubbyAlias: "analysis",
kind: "dashboard",
queries: [
{ id: "total", label: "Total", sql: "SELECT COUNT(*) AS n FROM analysis_findings" },
{ id: "by_tag", label: "By tag", sql: "SELECT tag, COUNT(*) AS n FROM analysis_findings GROUP BY tag ORDER BY n DESC LIMIT 5" },
{ id: "recent", label: "Recent", sql: "SELECT title, created_at FROM analysis_findings ORDER BY created_at DESC LIMIT 10" },
],
config: {
kind: "dashboard",
panels: [
{ title: "Total findings", query: "total", render: "metric", value: "n" },
{ title: "Top tags", query: "by_tag", render: "bar", value: "n", label: "tag" },
{ title: "Recent", query: "recent", render: "list", item: { title: "title", subtitle: "created_at:reltime" } },
],
},
dir: "./widgets/insights",
entry: "index.html",
},
],

composite combines panels of any kind behind a menu, with a first-run gate; see the widget-runtime reference.

Custom page

function render(result) {
var tag = result.columns.indexOf("tag"), n = result.columns.indexOf("n");
document.getElementById("app").innerHTML = result.rows.map(function (r) {
return "<div>" + String(r[tag]) + ": " + String(r[n]) + "</div>";
}).join("");
}
window.WidgetRuntime.query("by_tag").then(render).catch(function (err) {
if (err && err.name === "AgentNotConnectedError") { /* show Connect → connectAgent() */ }
});

Parameters

Positional parameters bind to ? in the declared SQL:

window.WidgetRuntime.query("since", [Date.now() - 7 * 24 * 3600 * 1000]).then(render);

The page can only run declared queries; it never sends SQL.

Read the Memory Bank

Declare a tool query instead of sql:

queries: [
{ id: "find", label: "Search", tool: "search", limit: 20 },
{ id: "record", label: "Record", tool: "get" },
{ id: "links", label: "Neighbours", tool: "neighbours", limit: 50 },
{ id: "counts", label: "By type", tool: "countByType" },
],
Tool First parameter
search The match text.
get, neighbours A record id.
countByType None.
window.WidgetRuntime.query("find", ["renewal"]).then(render);

The reader sees only the records their access allows.

Live updates

Follow a stream with subscribe; each poll’s new events arrive as one ordered batch:

var stop = window.WidgetRuntime.subscribe(
"run:123",
{ types: ["analysis.updated"], intervalMs: 2500 },
function (events) { window.WidgetRuntime.query("recent").then(render); },
);
// later: stop();
Option Default Meaning
types all Event types to deliver.
intervalMs 2500 Poll interval.
maxBackoffMs 30000 Ceiling for the wait while polls keep failing.
onError console.warn Called on a failed poll.

Polling stops when you call the returned function or when the reader signs out. onIdentityChange(cb) reports sign-in changes (status: anon, connecting, ready).