Skip to content

Build a widget

A widget is a static web page (an entry HTML file plus sibling JS, CSS, and assets) that shows what your agents did and lets people send data back. It runs @cef-ai/widget-runtime, which signs the reader in and exposes window.WidgetRuntime. Your page never handles keys or endpoints.

A widget reads two places:

Read Declared as Reads
A cubby sql query One of the Agent Service’s cubbies, the same database ctx.cubby writes.
The Memory Bank tool query The vault’s Memory Bank: search, get, neighbours, or countByType.

Two ways to ship one

Ship it When Command
With an agent The widget shows that agent’s work. Declare it in the agent’s widgets[]; cef push uploads it.
On its own The widget shows the whole service (a runs board, a report), or a workflow pins it. A widget subproject with its own package.json; cef widget push.

Option A: with an agent

1. Declare it

cef.config.ts
widgets: [
{
id: "hello",
name: "Hello",
description: "Recent messages.",
cubbyAlias: "history",
kind: "custom",
queries: [
{ id: "recent", label: "Recent messages", sql: "SELECT text, ts FROM messages ORDER BY ts DESC LIMIT 20" },
],
dir: "./widgets/hello",
entry: "index.html",
},
],
Field Meaning
id Unique within the agent.
name, description Labels.
cubbyAlias The default cubby for sql queries.
kind custom for your own page, or a built-in kind: list, record, dashboard, submit, conversation, composite. See Visualize vault data.
config The built-in kind’s configuration.
queries Named reads: { id, label?, sql?, cubby?, tool?, limit?, timeoutMs? }. Set exactly one of sql and tool. cubby picks another service cubby for sql.
events Event types the widget uses.
dir The built widget directory. cef build copies it as is; build it yourself first if it needs a build step.
entry The entry file in dir. Use index.html so the widget is served at its directory URL.

2. Write the page

widgets/hello/index.html
<!doctype html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello</title>
<div id="app">Loading…</div>
<script src="./app.js"></script>

Do not add the runtime script or the manifest yourself: cef dev and cef build inject both into <head>.

widgets/hello/app.js
(function () {
var app = document.getElementById("app");
function render(result) {
var i = result.columns.indexOf("text");
app.innerHTML = result.rows.length
? "<ul>" + result.rows.map(function (r) { return "<li>" + String(r[i]) + "</li>"; }).join("") + "</ul>"
: "<p>No messages yet.</p>";
}
function showConnect() {
app.innerHTML = '<button id="cta">Connect</button>';
document.getElementById("cta").onclick = function () {
window.WidgetRuntime.connectAgent().then(load);
};
}
function load() {
window.WidgetRuntime.query("recent").then(render).catch(function (err) {
if (err && err.name === "AgentNotConnectedError") showConnect();
else app.textContent = "Error: " + (err && err.message ? err.message : String(err));
});
}
load();
})();

query(id, params?) resolves to { columns, rows, meta: { rowCount } }. Rows are arrays in column order. Until the reader’s vault has connected the agent, query rejects with AgentNotConnectedError; connectAgent() runs the connection.

3. Run it locally

Terminal window
cef dev hello --as-pubkey <agentServicePubkey>

cef dev serves the widget on 127.0.0.1 with the runtime injected and reloads the browser when files change. The page opens top-level, so the reader signs in with the Manykind passkey wallet and the widget reads their vault. Without --as-pubkey, the widget has no full agent id and connectAgent() and query() fail.

Flag Default Meaning
[widgetId] first declared widget Which widget to serve.
--port <n> a free port Listen port.
--host <host> 127.0.0.1 Listen host.
--env <env> dev Which environment’s endpoints to bake in.
--no-watch — Do not reload on change.

4. Ship it

cef build copies the runtime into each widget, writes the widget’s manifest, and injects both into the entry HTML. cef build logs the runtime version it vendors; it is the version the installed CLI resolves, not your project’s pin. cef push then uploads each widget directory next to the bundle. See Push and deploy.

Option B: on its own

A widget subproject is an ordinary package: its own package.json, its own build, output in dist/. The cef block in package.json declares the widget:

{
"name": "@acme/runs-board",
"version": "0.1.0",
"scripts": { "build": "vite build" },
"cef": {
"id": "runs-board",
"name": "Runs board",
"entry": "index.html",
"scope": "default",
"cubbyAlias": "runs",
"kind": "custom",
"queries": [
{ "id": "latest", "label": "Latest runs", "sql": "SELECT * FROM runs ORDER BY started_at DESC LIMIT 50" }
]
}
}
Terminal window
npm run build
cef widget push --bucket <bucketId> --as-pubkey <agentServicePubkey>

cef widget push writes the manifest into the built entry file and publishes the directory under the service bucket’s widgets root. The id defaults to the last segment of the package name, and the version to the package version. Every kind except custom must render into an element with id="app"; the push refuses an entry without one. All flags are in the CLI reference.

The service’s widgets appear on the Widgets page in ROC, and a workflow can pin one; see Pin it.

How the reader is signed in

Opened Identity
Framed by ROC or another host The host supplies the identity over a postMessage handshake and signs for the widget. A host can also name the vault and the run to show.
Top-level (a direct link, or cef dev) The reader signs in with the Manykind passkey wallet. A reader with an active session is signed in silently; otherwise the widget shows a sign-in control.

A direct link can name the vault and the run in its fragment: …/index.html#vault=<vaultId>&run=<runId>. Naming a vault grants nothing: every read is still checked against the reader’s own access.

To frame a widget in your own page, mount createWidgetHost from @cef-ai/widget-runtime. See the widget-runtime reference.