Portal Community

Interaction Event Model

Widgets communicate with the platform through interaction callbacks. When a user performs a meaningful action (clicks a button, submits a selection, drags a card), the widget calls callbacks.onInteraction(eventName, data).

// Inside a widget bundle:
function mount(container: HTMLElement, props: Record<string, unknown>, callbacks: WidgetCallbacks) {
  const approveBtn = container.querySelector('#approve-btn');
  approveBtn?.addEventListener('click', () => {
    const selectedItems = getSelectedItems();
    const totalApproved = selectedItems.reduce((sum, item) => sum + item.amount, 0);

    // Fire interaction — resumes the workflow
    callbacks.onInteraction('approve', {
      approvedItems: selectedItems,
      rejectedItems: getRejectedItems(),
      totalApproved,
      notes: getNotesFieldValue()
    });
  });
}

Resume API Call

The WidgetRenderer's onInteraction handler calls the HIL resume endpoint:

POST /api/executions/{executionId}/resume
{
  "action": "interaction",
  "interactionEvent": "approve",
  "interactionData": {
    "approvedItems": [{ "id": "item-1", "amount": 450.00 }],
    "rejectedItems": [],
    "totalApproved": 450.00,
    "notes": "All receipts verified"
  }
}

Node Output from Interaction

{
  "approvedItems": [{ "id": "item-1", "amount": 450.00 }],
  "rejectedItems": [],
  "totalApproved": 450.00,
  "notes": "All receipts verified",
  "_interactionEvent": "approve",
  "_interactedBy": "user-alice",
  "_interactedAt": "2026-05-25T14:30:00Z"
}

Port Routing by Interaction Event

Map different interaction events to different ports using the interactionPortMap config:

{
  "interactionPortMap": {
    "approve": "approved",
    "reject": "rejected",
    "requestAmendment": "amendment"
  }
}
Design principle: Widgets should only fire interactions when a user intentionally takes an action — not on every UI state change. Each interaction call resumes the workflow. Multiple accidental firings would cause unexpected behavior.