Trigger actions on the backend from user interactions in your widgets.
Actions are attached directly to interactive elements (buttons, inputs, checkboxes) and allow the widget to trigger server-side logic or client-side callbacks without the user submitting a message.
Action Properties
1interface WidgetAction {2 type: string; // Action identifier (e.g., "submit", "delete", "toggle")3 payload?: Record<string, any>; // Optional data to include with the action4 handler?: "server" | "client"; // Handler location (default: "server")5 loadingBehavior?: "auto" | "self" | "container" | "none"; // Loading state behavior6}loadingBehavior Options
| Value | Description |
|---|---|
auto | Automatic loading state (default) |
self | Show loading spinner on the clicked element |
container | Show loading state on the entire widget |
none | No loading indicator |
Attaching Actions to Elements
Actions are attached directly to elements using onClickAction, onChangeAction, or onCheckedChangeAction:
Button Click Actions
1children := []models.WidgetNode{2 {3 Type: models.WidgetNodeTypeButton,4 Label: "Submit",5 Variant: "default",6 OnClickAction: &models.WidgetAction{7 Type: "submit",8 Payload: map[string]interface{}{"source": "form"},9 LoadingBehavior: "self",10 },11 },12 {13 Type: models.WidgetNodeTypeButton,14 Label: "Cancel",15 Variant: "outline",16 OnClickAction: &models.WidgetAction{17 Type: "cancel",18 },19 },20}Input Change Actions
1{2 Type: models.WidgetNodeTypeInput,3 Name: "email",4 Placeholder: "Enter email",5 OnChangeAction: &models.WidgetAction{6 Type: "email_changed",7 Handler: "client",8 },9}Checkbox Toggle Actions
1{2 Type: models.WidgetNodeTypeCheckbox,3 Name: "task_completed",4 Label: "Mark as complete",5 DefaultChecked: false,6 OnCheckedChangeAction: &models.WidgetAction{7 Type: "toggle_task",8 Payload: map[string]interface{}{"task_id": "123"},9 },10}Complete Example
1widget := &models.Widget{2 Type: models.WidgetTypeUI,3 Title: "Delete Project?",4 Children: []models.WidgetNode{5 {6 Type: models.WidgetNodeTypeCol,7 Gap: 3,8 Children: []models.WidgetNode{9 {Type: models.WidgetNodeTypeText, Value: "This action cannot be undone. All data will be permanently deleted."},10 {11 Type: models.WidgetNodeTypeRow,12 Gap: 2,13 Justify: "end",14 Children: []models.WidgetNode{15 {16 Type: models.WidgetNodeTypeButton,17 Label: "Cancel",18 Variant: "ghost",19 OnClickAction: &models.WidgetAction{Type: "cancel"},20 },21 {22 Type: models.WidgetNodeTypeButton,23 Label: "Delete",24 Variant: "destructive",25 OnClickAction: &models.WidgetAction{26 Type: "confirm_delete",27 Payload: map[string]interface{}{"project_id": "123"},28 LoadingBehavior: "self",29 },30 },31 },32 },33 },34 },35 },36}Handling Actions
Frontend
Capture widget events with the onAction callback in your widget renderer:
1<WidgetRenderer2 widget={widget}3 onAction={async (action, formData) => {4 if (action.handler === 'client') {5 // Handle client-side action6 handleClientAction(action, formData);7 return;8 }910 // Send to server11 await fetch('/api/widget-action', {12 method: 'POST',13 headers: { 'Content-Type': 'application/json' },14 body: JSON.stringify({ action, formData }),15 });16 }}17/>Backend
Handle widget actions in your agent:
1func (e *Executor) HandleWidgetAction(ctx context.Context, action models.WidgetAction, formData map[string]interface{}) error {2 switch action.Type {3 case "confirm_delete":4 projectID := action.Payload["project_id"].(string)5 return e.deleteProject(ctx, projectID)6 case "cancel":7 return e.cancelAction(ctx)8 case "toggle_task":9 taskID := action.Payload["task_id"].(string)10 return e.toggleTask(ctx, taskID)11 default:12 return fmt.Errorf("unknown action type: %s", action.Type)13 }14}Common Action Types
| Type | Description |
|---|---|
confirm | User confirmed the action |
cancel | User cancelled/dismissed the widget |
submit | Form submission |
delete | Delete operation |
toggle | Toggle a boolean value |
Form Data
When a widget contains form inputs (input, select, checkbox), the form data is automatically collected and passed to the action handler:
1widget := &models.Widget{2 Type: models.WidgetTypeUI,3 Title: "Create User",4 Children: []models.WidgetNode{5 {6 Type: models.WidgetNodeTypeCol,7 Gap: 3,8 Children: []models.WidgetNode{9 {Type: models.WidgetNodeTypeLabel, Value: "Email", FieldName: "email"},10 {Type: models.WidgetNodeTypeInput, Name: "email", Placeholder: "Enter email"},11 {Type: models.WidgetNodeTypeLabel, Value: "Role", FieldName: "role"},12 {13 Type: models.WidgetNodeTypeSelect,14 Name: "role",15 Options: []models.WidgetSelectOption{16 {Label: "Admin", Value: "admin"},17 {Label: "User", Value: "user"},18 },19 },20 {21 Type: models.WidgetNodeTypeRow,22 Gap: 2,23 Justify: "end",24 Children: []models.WidgetNode{25 {26 Type: models.WidgetNodeTypeButton,27 Label: "Create",28 Variant: "default",29 OnClickAction: &models.WidgetAction{30 Type: "create_user",31 LoadingBehavior: "self",32 },33 },34 },35 },36 },37 },38 },39}When the "Create" button is clicked, formData will contain:
1{2 "email": "[email protected]",3 "role": "admin"4}Best Practices
1. Use Descriptive Action Types
1// Good2OnClickAction: &models.WidgetAction{Type: "approve_request"}34// Avoid5OnClickAction: &models.WidgetAction{Type: "action1"}2. Include Context in Payload
1OnClickAction: &models.WidgetAction{2 Type: "approve_request",3 Payload: map[string]interface{}{4 "request_id": request.ID,5 "approval_level": "manager",6 },7}3. Use Loading States for Async Operations
1OnClickAction: &models.WidgetAction{2 Type: "submit_form",3 LoadingBehavior: "self", // Shows spinner on button during submission4}4. Validate Action Data on Backend
1func (e *Executor) HandleAction(action models.WidgetAction, formData map[string]interface{}) error {2 // Validate required fields3 requestID, ok := action.Payload["request_id"].(string)4 if !ok || requestID == "" {5 return fmt.Errorf("request_id is required")6 }78 // ... handle action9}