Skip to main content
A reactive Custom Widget lets an operator filter a chart, open a record, configure a process and start a Job from a dashboard or a private Assistant card. It extends the existing return [[cell]] format: add stable IDs and declarative events to the cells that need interaction. Existing text, charts and tables remain valid. Use Dashboards for the surrounding layout and saved widget settings. Use a reusable Component when an action or view needs its own reviewed revision.

Connect inputs, events and destinations

Keep IDs unique across the widget, including form fields. Events and bindings are JSON data, not JavaScript callbacks or expressions. Start an ID with a letter, then use letters, digits, underscores, dots or hyphens, up to 100 characters. Keep generated source self-contained: serializing a function does not preserve variables from its surrounding scope. Targets reuse the normal Inline or Component source configuration. A child view inherits its parent’s target maps unless it supplies its own. Configure executable destinations in the saved widget; a clicked row supplies arguments, not arbitrary code or Component IDs.

Form → Job → one refreshed tile

This example has two cells: a form and a recent-jobs tile. Submit commits the form, starts a Job, polls that exact Job and refreshes only jobResult. The form remains mounted with its values. Prepare an active Job template that accepts a batch input. Its worker reads that value from api.input().templateInputs.batch. Prepare two Components with a visible element named Data: one starts the Job, the other reads its status. The examples below supply their bodies.

1. Configure the widget

Choose Inline code, JavaScript, and set Inputs to:
In Action components (JSON), register the two Components. Replace the identifiers with your verified resources; keep the same template ID in both settings:
View sources may return the cell matrix directly, or wrap it in { matrix } or { rows }. This applies to root views, drill targets and partial refreshes in both chat and dashboards. Action receipts are separate objects and are not view output. These JSON maps are properties of the widget configuration, not cells in its returned matrix. Configured inputs is JSON text in both source modes. The widget parses it once, so api.input() receives the object. Action bindings override target defaults; a completion probe receives its own configured inputs and waitFor.input mappings. You can pin a target’s Component digest when a reviewed revision must remain fixed.

2. Return the form and result tile

Paste this into the widget’s Inline code:
The tile shows the latest five runs of this template, including runs from other operators. Completion polling is separately tied to the exact jobId returned by this submission. On a partial refresh, return exactly the requested IDs. Check targetIds before expensive queries: refreshing one rendered cell still executes the source body, so the source decides which work to skip.

3. Start the Job in the write Component

Use this body in the starting Component’s Data element. It validates the input and records a bounded duplicate-dispatch guard before enqueueing:
The one-hour cache guard protects repeated delivery of the same dispatch ID while the key exists. It is not a transaction with Job creation or business-level exactly-once execution. If enqueueing succeeds but saving its receipt fails, reconcile the Job using the supplied requestId; do not automatically enqueue another one. A deliberate new submit has a new dispatch ID. Enforce business idempotency in the worker for operations that must occur only once.

4. Read the exact Job in the status Component

Jobs list reads use a restricted projection. Query the confirmed jobId with fields: ['jobId', 'status']; output and inputs are not supported list fields. Show the confirmed status or fetch results from a separately verified business resource. Test the result branch with an existing authorized job before publishing. The submit control and its form inputs stay locked while the action runs, while confirmation is pending, and through the configured cooldown. A successful enqueue is not completion. A terminal failure releases the lock with an error; a timeout or network failure leaves the outcome uncertain and never replays the write automatically. When a receipt is available, the button can check status again. A cooldown is a client-side throttle, not a durable server lock or lease. Reads in this example and all status probes are read-only. Keep reusable view Components in restricted mode: an initial Component render without configured target maps retains its existing restricted-mode setting, even if its result later introduces interactive controls. Configured target maps and subsequent interactive refreshes force read-only execution. Writes through an action require explicit readOnly: false on the registered action target and the caller’s normal execution permissions. The UI does not grant access; validate business inputs and authorization in the write path.

Open a detail view and return

Add this destination in Widget destinations (JSON). Its Inline body is complete and reads the selected order ID from its input:
Use this source in the parent widget. Clicking a row replaces the current view while retaining its frame; Back restores the parent and its selected keys.
Use mode: 'panel' to open the destination beside its parent, or mode: 'inline' with an existing parent cell ID in hostId to mount it inside that cell. These modes start independent state: BACK navigates a retained replacement frame, not a parent in a different surface. Dismiss a panel with Escape or an outside click. For deeper drill-down, register another named destination and pass the next record ID explicitly.

Preserve or refill inputs

Initial rendering receives the configured inputs unchanged. Later interactive reads receive that JSON object plus $widget: Read current values from $widget.values before falling back to configured api.input() defaults. set changes declared INPUT state, not the configured arguments. An input binding refers only to an actual INPUT cell in the current frame. In a child view without controls, read passed arguments from api.input() and forward scalar values in the next action’s input map. Do not supply the reserved $widget key in configured interactive inputs. Form fields commit together on submit. A standalone INPUT commits on change by default; optional commit: 'blur' and debounceMs control when it commits. valueMode: 'initial' is the default: returned defaults do not replace an existing value. Use valueMode: 'sync' when low-code should refill an input, such as a delivery method derived from a selected country. A synchronized value applies only when no newer edit or pending draft exists. Ordinary refresh keeps drafts and mounted renderer instances; stale responses cannot overwrite newer committed state. State belongs to the mounted widget. Back retains previous drill-down frames, including drafts and selection, for up to 12 levels. A page reload initializes a new widget; panels and inline destinations have independent state.

Charts, records and multiple selection

Bindings can read input, event, selection or an action result. Paths select existing properties and are not evaluated expressions.
Register widgetTargets.orderDetail and actionTargets.processOrders before using this example. The latter must finish its work before returning, or provide a receipt and a waitFor probe as in the Job example. Table keys must be unique strings or finite numbers. Checking a row changes selection independently of opening it. A refresh retains selected keys only while they are present in the returned table data. A chart’s on.pointClick can use { from: 'event', path: 'datum.status' }. For example, a REFRESH action can set an existing status INPUT and request only the table. Declare that INPUT first: set does not create missing fields. TEXT and BUTTON cells use on.click; tables also support on.selectionChange. Use refresh: { intervalSeconds: 30 } on a cell for its own timer, optionally with targets: ['anotherCell']. Intervals range from 1 to 86400 seconds. Hidden drill-down views pause their reads; overlapping interval reads are skipped. The full widget Refresh setting remains available separately.

Limits and next steps

A widget accepts up to 64 rows, 64 cells per row and 256 elements overall, including at most 64 INPUT fields per FORM and 16 toolbar buttons per TABLE. Each target map accepts up to 32 entries, with at most four nested target levels. Custom TABLE filtering operates on the returned data in the browser; it is not server pagination. Bound queries and make partial datasets visible. Private Assistant cards use the same reactive runtime when they include source configuration. Shared and report views are inactive snapshots. Dynamic conversion to a dashboard preserves the source, configured inputs and action/navigation targets; fixed-snapshot conversion removes interactions and disables controls. Continue with Jobs for execution lifecycle and Dashboards for saved layout and sharing.

Interactive cards in Assistant

Ask the Assistant for a form, a Job action, a result tile and a drill-down. Ask it to validate the card before displaying it. This preflight executes the read-only initial view and validates the complete widget contract without saving or displaying a card. Creating the card after preflight executes and validates the source again; the renderer then displays that saved result. Initial validation does not prove partial refreshes, drill-down destinations, registered actions or browser rendering. Test each view branch in read-only Sandbox with representative $widget.targetIds and $widget.values; assert the exact returned cell IDs and values, including permitted zero and empty inputs. These probes must not launch the registered write actions. Verify those actions separately within the intended test scope. The existing Custom Widget matrix is unchanged. The generator can return { matrix, summary, widgetConfig }; widgetConfig registers the same widgetTargets and actionTargets used in dashboard settings. A single self-contained generator becomes the read-only view source automatically. If generation uses libraries or multiple elements, provide a complete reusable INLINE or COMPONENT source instead. The card first displays the validated result without executing it again. Subsequent interactions execute the registered sources with the viewer’s current permissions. Generating a card does not start its Job: a write needs a registered action with readOnly: false and an operator interaction. View sources and completion probes remain read-only. Fullscreen retains the mounted form and drafts. This complete generator body demonstrates form → Job → refresh one tile, followed by a clickable detail view with Back. Replace VERIFIED_JOB_TEMPLATE_ID with a verified active Job template. The template must validate inputs and enforce the business operation’s idempotency. The result tile reads the exact confirmed Job using $widget.actionResult.jobId. Registered source syntax and initial bindings are checked before publication; each nested source must be a valid independent function body.
The source branches on $widget.targetIds before reading data. After confirmed completion, only total is returned, so the form remains mounted and retains its values. Clicking that tile opens details, then More opens a second level using the received quantity. Back restores each retained frame. The worker receives quantity in api.input().templateInputs. For horizontal bar charts, use a number x-axis and a category y-axis, or omit explicit axes. Text category values on a number axis are rejected before the card is published. Chat artifacts allow at most 8 rows, 4 cells per row, 5000 flat records per chart/table, 12 table columns and 2 MB for rows plus sources. Each target map allows 32 entries, with at most four nested target levels. Inline source code is limited to 120000 characters. Source inputs is JSON object text without $widget; timeout is 5–600 seconds and memory is 32–1024 MB. These limits do not grant execution access. Before sharing a result, verify the real form submit, exact Job completion, retained inputs after partial refresh, two drill levels with Back, and dynamic dashboard conversion. A validated artifact alone does not prove browser rendering or Job success. A page reload restores configured defaults, not unsaved form drafts. Shared/report readers see disabled controls and cannot run actions from the card.

Ordered button commands

For calculator keypads or other stateful commands, use on.click: { type: "REFRESH", target: "display", concurrency: "queue" }. Each queued click reads the preceding merged state; $widget.event.sourceId identifies the key. A synchronized INPUT can retain the display value between clicks. Only return the requested display cell; the keypad remains mounted. The per-frame queue permits 64 pending commands. Overflow discards pending commands and shows an error. Errors, manual refresh, nonqueued refresh actions, remount and Back cancellation discard pending commands. The default is latest overlapping read wins. This does not queue writes or persist a calculation across a page reload.

Dashboard header controls

In the widget settings, Widget header exposes Show refresh (on by default) and Show performance (off by default). Existing widgets use these defaults without a migration. The optional boolean fields are showRefresh and showPerformance inside specificCustomWidgetConfig, or widgetConfig when generating a chat card. Dynamic conversion preserves both settings. Hiding refresh removes the dashboard header button only: timers, cell actions and the error recovery button still work. Hiding performance leaves error, warning and log indicators visible. refreshTime configures a whole-widget timer; per-cell refresh.intervalSeconds remains independent. When neither is configured, the legacy fallback refreshes the whole widget every 6000 seconds (100 minutes). Per-cell timers suppress that fallback. Timer reads pause while the document or replacement frame is hidden. These controls do not change access permissions or read-only behavior.
Last modified on November 7, 2026