Skip to main content

Widget–Workflow Integration

Widgets are front-end components that display live, workflow-driven data. They connect to the Workflow Engine through a two-layer integration:

  • Backend β€” widgetWorkflowBridge manages widget-to-instance subscriptions and processes context into renderable data.
  • Frontend β€” useWidgetRun manages the socket connection, execution lifecycle, and data priority resolution.

Architecture overview​

Execution modes​

ModeTriggerData deliveryUse case
ASYNC (live)fetchWidgetData() β†’ workflow startsSocket stream as nodes completeDashboards needing real-time updates
SYNC (on-completion)fetchWidgetData() with executionMode=SYNCHTTP response after workflow finishesOne-shot data fetch, no live updates
subscribewidget_workflow_connect with mode="subscribe"Socket stream on existing instanceIDRe-attach to an already-running workflow
replaywidget_workflow_connect with mode="replay"Single emit with final contextInspect a completed instance

Connection flows​

ASYNC mode β€” live execution​

This is the primary mode for live dashboard widgets.

SYNC mode β€” on-completion​

No socket connection is established. The API call blocks until the workflow completes and returns fully processed data in the HTTP response.

Widget registration timing​

Critical

In execute mode, widgetWorkflowBridge.registerWidget() is called before emitting widget_workflow_connected, inside onWidgetWorkflowConnect. This prevents NODE_COMPLETED events from the very first node from being missed while the widget is still setting up its listener.


Backend bridge: widgetWorkflowBridge​

Widget registry​

The bridge maintains two in-memory Maps:

MapKeyValuePurpose
widgetConnectionswidgetID{ instanceID, socketId, widgetType, widgetConfig, workflowConfig, ... }Full connection state per widget
instanceWidgetsinstanceIDSet<widgetID>Fast lookup β€” all widgets watching a given instance
Scaling

The registry is in-process memory. For horizontal scaling across multiple pods, this must be replaced with a shared store (e.g. Redis Pub/Sub) so emits reach widgets connected to any pod.

Context processing pipeline​

The pipeline runs per widget, per node completion. Each widget may have a different widgetConfig, so context is processed separately for each registered widget.

Template resolution detail​

widgetConfig is an opaque JSON blob that may contain {{ctx.*}} expressions anywhere in its structure β€” nested objects, arrays, string values.

// widgetConfig before resolution
{ "data": { "values": "{{ctx.queryResult}}" }, "mark": "bar" }

// After resolveTemplate with preserveSingleExpressionType: true
// ctx.queryResult is an array β€” type is preserved, not serialised to string
{ "data": { "values": [{ "x": 1, "y": 2 }, ...] }, "mark": "bar" }

preserveSingleExpressionType: true is critical for passing arrays and objects to Vega-Lite and similar widget types without unwanted stringification.

Room architecture​

Each widget joins two socket rooms on registration:

The dual-room design means a widget receives both:

  • Typed processed data (via its private room) β€” chart-ready output already shaped by widgets-logic.
  • Raw node execution logs (via the instance room) β€” used for the per-node logs array in the developer tooling panel.

widgetWorkflowBridge API​

MethodParametersDescription
registerWidget()widgetID, instanceID, socket, metadataStores connection; joins socket rooms widget:<id> and instanceID
unregisterWidget()widgetID, socket?Removes from both Maps; leaves socket rooms
processContextForWidget()context, { widgetType, widgetConfig, workflowConfig }Template resolution + widgets-logic delegation. Returns processedData or null.
emitContextUpdate()instanceID, updateProcesses and emits widget_context_update to all registered widgets for this instance
emitWorkflowStatus()instanceID, status, finalContext?Emits widget_workflow_status with final processedData to all registered widgets
emitError()widgetID, errorEmits widget_workflow_error to a specific widget's private room
getWidgetsForInstance()instanceIDReturns string[] of widgetIDs watching this instance
getWidgetConnection()widgetIDReturns full connection metadata object or null
getStats()β€”Returns connection counts and summary array (diagnostics)
cleanupStaleConnections()maxAgeMs?Removes connections older than maxAgeMs (default 1 hr)

Socket event reference​

Client β†’ server​

EventHandlerKey payload fields
widget_workflow_connectonWidgetWorkflowConnectwidgetID, workflowID, mode, instanceID?, inputParams, widgetType, widgetConfig, workflowConfig, tenantID
widget_workflow_disconnectonWidgetWorkflowDisconnectwidgetID
widget_send_inputonWidgetSendInputwidgetID, instanceID?, inputType, data
widget_refreshonWidgetRefreshwidgetID, inputParams?, tenantID
workflow_run_joinworkflowSocketController.onWorkflowRunJoinrunId (instanceID)

Server β†’ client​

EventRoomKey payload fields
widget_workflow_connectedsocket directlywidgetID, instanceID, mode, initialContext, workflowMeta
widget_context_updatewidget:<widgetID>widgetID, instanceID, update: { processedData, contextSnapshot, nodeID, ... }
widget_workflow_statuswidget:<widgetID>widgetID, instanceID, status, finalContext, processedData
widget_workflow_errorwidget:<widgetID>widgetID, error: { code, message, recoverable }
widget_workflow_disconnectedsocket directlywidgetID, success
workflow_node_updateinstanceID roominstanceID, nodeID, status, output, error
workflow_status_updateinstanceID roominstanceID, status, contextData

Connection modes in detail​

execute mode​

Starts a new workflow execution. Used when the widget drives its own workflow run.

  • Requires workflowID.
  • widgetWorkflowBridge.registerWidget() is called before emitting widget_workflow_connected.
  • Returns early β€” skips the common register + emit path at the bottom of onWidgetWorkflowConnect.

subscribe mode​

Attaches to an existing running workflow instance.

  • Requires instanceID.
  • initialContext is set from the instance's assembled context at subscribe time.
  • The widget receives all future context updates until the workflow completes.

replay mode​

Fetches the final context of a completed instance and emits it once. No live updates.

  • Requires instanceID.
  • If widgetType and widgetConfig are provided, processContextForWidget runs immediately and processedData is included in the widget_workflow_connected emit.
  • Returns early β€” the widget is not registered for live updates.

Frontend hook: useWidgetRun​

Parameters​

ParameterTypeDescription
tenantIDstringRequired. Scopes the workflow execution.
widgetIDstring | nullStable widget identifier. Null generates a preview ID.
widgetFetchedDataobject | nullInitial data from parent (SSR or cache). Hydrates on first render.
executionModeASYNC | SYNCControls whether live socket streaming is used.
workflowIDstring | nullTarget workflow to execute.
widgetTypestring | nullWidget type identifier passed to widgets-logic for data shaping.
widgetConfigobject | nullOpaque widget configuration blob containing {{ctx.*}} templates.
workflowConfigobject | nullWorkflow binding configuration.

Return values​

ReturnTypeDescription
dataobject | nullFinal resolved data β€” priority: wsProcessedData > localData > widgetFetchedData
processedDataobject | nullLatest chart-ready data from the socket stream. Updated per node completion.
contextobjectRaw workflow context snapshot (all node outputs merged).
isLoadingbooleanTrue only during the initial API fetch before any socket data arrives.
isRunningbooleanTrue while fetching OR workflowStatus is LOADING or PENDING.
isLivebooleanTrue when connectionState is CONNECTED.
workflowStatusstringLOADING | PENDING | COMPLETED | FAILED | ERROR
connectionStatestringdisconnected | connecting | connected | error
logsarrayOrdered { type, label, message, timestamp } entries for developer tooling.
runWidget(formValues, opts)functionTriggers API fetch. Accepts opts.inputParams.
clearLogs()functionEmpties the logs array.

Data priority resolution​

This means:

  • A widget pre-loaded with SSR data (widgetFetchedData) renders immediately β€” no loading state.
  • When the live workflow produces data, wsProcessedData takes over and the widget updates in place.
  • If the socket disconnects mid-run, the last received wsProcessedData remains β€” the widget does not revert to initial data.

shouldConnect logic​

The socket connection effect only fires when all four conditions are true:

ConditionWhy
instanceID is setNo point connecting before the workflow has started
executionMode is ASYNCSYNC mode receives data over HTTP, not sockets
socket is availableSocket context must be initialised
workflowStatus is not COMPLETED or FAILEDPrevents reconnecting to a finished workflow on re-render

Dual-channel subscription​

useWidgetRun subscribes to two separate socket channels simultaneously:

Both subscriptions are cleaned up in the effect's return function. On cleanup, socket.emit("widget_workflow_disconnect", { widgetID }) is sent so the bridge unregisters the widget and stops processing context updates for it.


Interactive widget input​

Widgets can send input back to a running workflow via widget_send_input. This allows widgets to drive workflow behaviour β€” form submissions, filter changes, action buttons.

Downstream workflow nodes can read ctx.__widgetInput to branch behaviour based on widget input. The version CAS in updateContext ensures the input write does not race with concurrent node completions.


Workflow service: widget path​

getRunStatusForWidget​

Used for SYNC mode and replay context pre-processing:


Comparison: useWidgetRun vs useWorkflowRun​

useWorkflowRun is the hook used by the workflow builder UI for developer testing. useWidgetRun is for end-user widget consumers.

FeatureuseWorkflowRunuseWidgetRun
AudienceWorkflow builder developersEnd-user widget consumers
Socket connectionCreates its own socket.io-client instanceUses shared socketContext socket
Data shapeRaw context + per-node outputsProcessed (chart-ready) via widgets-logic
Execution modestestRun (in-memory) or savedRunASYNC (live) or SYNC (on-completion)
Test run lifecycleCalls stopTestWorkflowAPI on stopN/A
Processed dataNot applicablewsProcessedData updated per node via bridge
Stop guardisStoppingRef prevents race on stopNot needed

isStoppingRef guard​

useWorkflowRun uses isStoppingRef to prevent a race condition on stop: if socket events arrive after the user clicks Stop but before the socket disconnects, the guard causes those handlers to return immediately without updating state. This prevents flickering or false-positive completion events.


Error handling​

Widget connection errors​

If onWidgetWorkflowConnect throws, the controller emits widget_workflow_error with:

{
"widgetID": "...",
"error": {
"code": "CONNECTION_FAILED",
"message": "...",
"recoverable": false
}
}

Workflow failure during streaming​

When the engine marks an instance FAILED, widgetWorkflowBridge.emitWorkflowStatus(instanceID, "FAILED", finalContext) is called. useWidgetRun's handleWidgetStatus sets workflowStatus to "FAILED" and shouldConnect becomes false, stopping further reconnection attempts.

processContextForWidget errors​

Template resolution or widgets-logic processing may fail (e.g. invalid template expression, missing context key). processContextForWidget wraps the entire pipeline in try/catch and returns null on any error. The emit continues with processedData: null β€” the widget receives a context update but renders with no data change, preserving the last valid state.


Scaling considerations​

Widget registry​

widgetConnections and instanceWidgets are in-process Maps. With multiple pods:

  • A widget connected to Pod A may not receive events emitted by the orchestrator on Pod B, because the bridge on Pod B has no knowledge of the widget registered on Pod A.
  • The correct fix is to move the widget registry to Redis and use Redis Pub/Sub or Socket.IO's Redis adapter to broadcast events across pods.

Socket.IO adapter​

The orchestrator emits directly to instanceID rooms via socketIO.to(instanceID).emit(...). In a multi-pod setup, room membership is not shared across pods by default. The Socket.IO Redis adapter makes rooms available to all pods, ensuring events reach the correct client regardless of which pod it is connected to.


Developer guide​

Adding a new widget type​

  1. Implement the widget rendering component in the frontend.
  2. Define the widgetConfig schema β€” the set of fields the widget needs, with {{ctx.*}} templates for dynamic values.
  3. Add the new widget type handler to @jet-admin/widgets-logic in processWorkflowDataForWidget. The backend bridge is widget-type-agnostic β€” it passes widgetType and the resolved widgetConfig to this function and returns whatever it produces.
  4. No backend changes are needed unless the widget requires a new context processing step.

Adding a new connection mode​

  1. Add a case to the switch in onWidgetWorkflowConnect.
  2. Call widgetWorkflowBridge.registerWidget() if live updates are required.
  3. Emit widget_workflow_connected with the appropriate initialContext.

Debugging widget data flow​

  • widgetWorkflowBridge.getStats() returns all active connections with metadata. Expose this on a debug endpoint to see what widgets are registered.
  • The logs array in useWidgetRun contains timestamped entries for every connection event, node update, and error. Surface this in a dev panel to trace what the widget received.
  • Set a breakpoint in processContextForWidget to inspect resolvedWidgetConfig before it reaches widgets-logic.
  • Call socket.emit("widget_workflow_connect", { mode: "replay", instanceID }) in the browser console to re-fetch the final context of any completed instance.

File reference​

FileRole
widgetWorkflowBridge.jsWidget registry; context processing pipeline; socket room emission
widget.socket.controller.jsSocket event handlers for all widget_workflow_* events
workflow.service.js (getRunStatusForWidget)SYNC mode and replay β€” template resolution + processWorkflowDataForWidget
useWidgetRun.jsxFrontend hook β€” API execution, socket lifecycle, data priority resolution
useWorkflowRun.jsxWorkflow builder hook β€” test/saved run management, per-node status tracking
@jet-admin/widgets-logicExternal package β€” processWorkflowDataForWidget; sole owner of widget-type-specific data shapes