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
-
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.
-
Publish. The widget publishes
event.typewithcontextset to a new session id. The payload is:{ "schema_version": 1, "session_id": "<uuid>", "audio_urls": ["…"], "meta": { "account": "…", "stage": "…" } } -
Poll. It runs
result.querywith the session id until thedoneWhencolumn matches ortimeoutMspasses.
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
cef dev record-call --as-pubkey <agentServicePubkey>Sign in, submit, and watch the result appear once the deployed agent writes its row.