Skip to content

Onboard data with a widget

An onboarding widget gets data into a vault. It captures input, stores large files as vault objects, publishes an event that your agent handles, and shows the agent’s result. Everything is written as the signed-in reader, into their vault; your agent sees it only because the vault has connected it.

This page assumes the setup from Build a widget.

The submit kind

submit renders a form and an audio control, then runs capture → upload → publish → poll → show result. You write no upload or signing code.

widgets: [
{
id: "record-call",
name: "Record a call",
cubbyAlias: "calls",
kind: "submit",
queries: [
{ id: "result", label: "Result", sql: "SELECT status, summary FROM calls_sessions WHERE session_id = ?" },
],
config: {
kind: "submit",
title: "Record a call",
submitLabel: "Submit",
form: [
{ name: "account", label: "Account", type: "text", required: true },
{ name: "stage", label: "Stage", type: "select", options: ["Discovery", "Demo", "Close"] },
],
audio: { mode: "both", required: true, segmentSeconds: 25, softCapSeconds: 1800 },
event: { type: "call.recorded", audioField: "audio_urls", formEnvelope: "meta" },
result: {
query: "result",
poll: { intervalMs: 3000, timeoutMs: 300000, doneWhen: { column: "status", equals: "done" } },
render: "summary",
summary: [{ label: "Summary", path: "summary" }],
},
},
dir: "./widgets/record-call",
entry: "index.html",
},
],
Config Meaning
title, intro, submitLabel, processingLabel, connectPrompt Copy. Defaults: Submit, Processing…, Connect your account to continue.
form[] { name, label, type: "text" | "number" | "select", options?, required? }
audio.mode upload (pick a file), record (microphone), or both.
audio.segmentSeconds Length of each uploaded chunk.
audio.softCapSeconds Warn on recordings longer than this.
event.type The event to publish.
event.audioField Payload field that receives the list of audio URLs.
event.formEnvelope Nest form values under this field; omit to merge them into the payload.
result.query A declared query, called with the session id as its only parameter.
result.poll intervalMs, timeoutMs, and doneWhen: { column, equals }.
result.render message or summary (labelled paths from the row).

What happens on submit

  1. Upload. The audio is split into WAV segments and uploaded as objects into the reader’s vault, in the widget’s scope. Each segment gets a short-lived URL that a model can fetch.

  2. Publish. The widget publishes event.type with context set to a new session id. The payload is:

    { "schema_version": 1, "session_id": "<uuid>", "audio_urls": ["…"], "meta": { "account": "…", "stage": "…" } }
  3. Poll. It runs result.query with the session id until the doneWhen column matches or timeoutMs passes.

Your agent’s side: handle call.recorded, pass the URLs to a model (see Models), and write a row with session_id, status = 'done', and summary into the cubby. Declare the payload in eventSchemas so the contract is in the manifest.

conversation is the turn-by-turn variant: it publishes a start event, one event per recorded turn, and an optional confirm event, and polls a turns query. See the widget-runtime reference.

Custom page, no audio

publish(type, payload, context?, options?) writes an event as the reader:

document.getElementById("save").onclick = function () {
window.WidgetRuntime.publish("contact.added", {
schema_version: 1,
name: document.getElementById("name").value,
}).then(function (r) { console.log("published", r.eventId); });
};

To start a workflow, target it. A workflow handles workflow.start, not your trigger’s own event type, and an untargeted workflow.start would start every workflow connected in the scope:

await window.WidgetRuntime.publish("workflow.start", input, requestId, {
target: asPubkey + ":my-workflow",
});

Then follow the run with subscribe(requestId, …); see Visualize vault data.

Test it

Terminal window
cef dev record-call --as-pubkey <agentServicePubkey>

Sign in, submit, and watch the result appear once the deployed agent writes its row.