| title | Action Protocol |
|---|---|
| description | Buttons, triggers, navigation, and user interaction definitions |
import { MousePointer, Zap, ExternalLink, Workflow, GitBranch, Play, Component, Layout } from 'lucide-react';
The Action Protocol specifies how user interactions (button clicks, menu selections, gestures) trigger business logic, navigation, or external integrations. Actions are the "verbs" of ObjectUI—they make things happen.
Instead of writing onClick handlers in JavaScript, you declare what should happen in metadata:
# ❌ Imperative (React)
<Button onClick={() => {
if (validate()) {
api.post('/customers', data);
router.push('/customers');
}
}}>Save</Button>
# ✅ Declarative (ObjectUI)
action:
type: standard_save
label: Save
onSuccess:
- navigate: /customersBenefits:
- No code: Business analysts configure actions
- Type-safe: Actions validated against schema
- Testable: Actions are data, can be unit tested
- Auditable: See what actions users can perform
ObjectUI defines several categories of actions.
Built-in CRUD operations that require no configuration:
actions:
- type: standard_new
label: New Customer
object: customer
- type: standard_edit
label: Edit
recordId: '{recordId}'
- type: standard_delete
label: Delete
recordId: '{recordId}'
confirm: true # Show confirmation dialog
- type: standard_save
label: Save
validate: true # Run validation before saving
- type: standard_cancel
label: Cancel
confirmIfDirty: true # Warn if unsaved changesStandard Actions Available:
standard_new- Create new recordstandard_edit- Edit existing recordstandard_delete- Delete recordstandard_save- Save current formstandard_cancel- Cancel and returnstandard_clone- Clone recordstandard_refresh- Reload datastandard_export- Export to CSV/Excelstandard_print- Print view
Navigate to different pages or external URLs:
actions:
- type: navigate
label: View Dashboard
target: /dashboards/sales_overview
- type: navigate
label: View Customer
target: /customers/{customer_id} # Dynamic parameter
- type: navigate
label: External Link
target: https://example.com
openInNewTab: true
- type: navigate_back
label: Go Back
fallback: /customers # If no history, go hereLaunch Screen Flows (multi-step wizards):
actions:
- type: flow
label: Approve Request
flow: approval_wizard
input:
recordId: '{recordId}'
approver: '{currentUserId}'
onComplete:
- action: refresh
- action: toast
message: Request approved successfullyCall custom REST endpoints:
actions:
- type: api
label: Sync with ERP
method: POST
endpoint: /api/integrations/erp/sync
params:
customerId: '{recordId}'
headers:
X-Custom-Header: value
onSuccess:
- action: toast
message: Sync completed successfully
variant: success
onError:
- action: toast
message: Sync failed: {error}
variant: errorTrigger server-side workflows:
actions:
- type: workflow
label: Send Reminder
workflow: send_customer_reminder
input:
customerId: '{recordId}'
reminderType: payment_due
async: true # Don't wait for completionOpen modals (dialogs) with embedded content:
actions:
- type: modal
label: Quick Edit
title: Edit Customer
content:
type: form
object: customer
recordId: '{recordId}'
fields: [name, email, phone]
size: medium
actions:
- type: standard_save
label: Save
closeOnSuccess: true
- type: standard_cancel
label: CancelOperate on multiple selected records:
# In ListView
actions:
- type: bulk_update
label: Update Status
availableWhen: rowsSelected
fields:
- name: status
type: select
options: [active, inactive, pending]
onSuccess:
- action: refresh
- type: bulk_delete
label: Delete Selected
availableWhen: rowsSelected
confirm:
title: Delete {count} records?
message: This action cannot be undone
confirmLabel: Delete
cancelLabel: CancelExecute custom JavaScript (renderer-side):
actions:
- type: custom
label: Calculate Discount
handler: calculateDiscount # Function registered in renderer
params:
quantity: '{quantity}'
unitPrice: '{unit_price}'interface Action {
// Identification
type: ActionType;
name?: string; // Unique name
label: string; // Button text
// Display
icon?: string; // Lucide icon
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'small' | 'medium' | 'large';
// Visibility & State
visible?: VisibilityRule; // When to show
disabled?: DisabledRule; // When to disable
availableWhen?: Condition; // Conditional availability
// Confirmation
confirm?: boolean | ConfirmDialog;
// Execution
onBefore?: Action[]; // Run before main action
onSuccess?: Action[]; // Run on success
onError?: Action[]; // Run on error
onFinally?: Action[]; // Run always (cleanup)
}Control when actions appear:
actions:
# Show only if status is 'draft'
- type: standard_edit
label: Edit
visible:
field: status
value: draft
# Show only if user has permission
- type: standard_delete
label: Delete
visible:
permission: delete_customers
# Show based on multiple conditions
- type: workflow
label: Approve
visible:
and:
- { field: status, value: pending }
- { permission: approve_requests }
- { field: amount, operator: '<', value: 10000 }Disable actions based on conditions:
actions:
# Disable if form is invalid
- type: standard_save
label: Save
disabled:
formInvalid: true
# Disable if record has children
- type: standard_delete
label: Delete
disabled:
field: has_open_orders
value: true
disabledTooltip: Cannot delete customer with open orders
# Disable based on field value
- type: workflow
label: Ship Order
disabled:
or:
- { field: status, operator: '!=', value: paid }
- { field: inventory_available, value: false }Show confirmation before destructive actions.
action:
type: standard_delete
label: Delete Customer
confirm: true # Default confirmationRenders:
┌─────────────────────────────────┐
│ Confirm Delete │
├─────────────────────────────────┤
│ Are you sure you want to │
│ delete this customer? │
│ │
│ [Cancel] [Delete] │
└─────────────────────────────────┘
action:
type: bulk_delete
label: Delete Selected
confirm:
title: Delete {count} customers?
message: |
This will permanently delete {count} customers
and all related records. This action cannot
be undone.
icon: alert-triangle
variant: danger
confirmLabel: Yes, Delete
cancelLabel: Cancel
requireTyping: DELETE # User must type "DELETE" to confirmActions can reference form data and context variables.
action:
type: api
endpoint: /api/customers/{customer_id}/send-email
params:
# Field values
email: '{email}'
name: '{name}'
# System variables
currentUser: '{$userId}'
timestamp: '{$now}'
# Computed values
fullName: '{first_name} {last_name}'
# Nested fields
billingCity: '{billing_address.city}'// Form field values
{fieldName} // e.g., {customer_name}
// System variables
{$userId} // Current user ID
{$userName} // Current user name
{$userEmail} // Current user email
{$now} // Current timestamp
{$recordId} // Current record ID
{$objectName} // Current object name
// Context variables (ListView)
{$selectedRows} // Selected row IDs
{$selectedCount} // Count of selected rows
{$filteredCount} // Count after filters
// Context variables (FormView)
{$isNew} // True if creating new record
{$isEditing} // True if editing existing
{$isDirty} // True if form has unsaved changes
{$isValid} // True if form passes validationExecute multiple actions in sequence or parallel.
action:
type: standard_save
label: Save & Send Email
onSuccess:
- action: api
type: api
endpoint: /api/send-welcome-email
params:
customerId: '{recordId}'
- action: navigate
type: navigate
target: /customers/{recordId}
- action: toast
type: toast
message: Customer saved and email sent
variant: successaction:
type: standard_save
label: Save
onSuccess:
parallel: true # Execute in parallel
actions:
- type: api
endpoint: /api/analytics/track
params:
event: customer_created
- type: api
endpoint: /api/crm/sync
params:
recordId: '{recordId}'
- type: workflow
workflow: send_notifications
input:
recordId: '{recordId}'action:
type: standard_save
label: Save
onSuccess:
- when: { field: send_email, value: true }
action:
type: api
endpoint: /api/send-email
- when: { field: amount, operator: '>', value: 10000 }
action:
type: workflow
workflow: high_value_alertActions can appear in different UI locations.
Buttons in form header or footer:
form:
object: customer
actions:
placement: footer # or 'header' | 'both'
align: right # or 'left' | 'center' | 'space-between'
actions:
- type: standard_save
label: Save
variant: primary
- type: standard_cancel
label: Cancel
variant: secondaryActions in list view rows:
listView:
object: customer
rowActions:
- type: standard_edit
label: Edit
icon: edit
- type: standard_delete
label: Delete
icon: trash
confirm: true
- type: separator # Visual separator
- type: api
label: Send Email
icon: mail
endpoint: /api/customers/{_id}/send-emailGroup actions in a dropdown:
actions:
- type: menu
label: More Actions
icon: more-vertical
items:
- type: standard_clone
label: Clone Customer
- type: api
label: Export to PDF
endpoint: /api/customers/{_id}/export-pdf
- type: separator
- type: workflow
label: Merge Duplicate
workflow: merge_customersActions that operate on selected rows:
listView:
object: customer
bulkActions:
- type: bulk_update
label: Change Status
icon: edit
fields:
- name: status
type: select
options: [active, inactive]
- type: bulk_delete
label: Delete
icon: trash
confirm: trueFloating action buttons:
page:
object: customer
quickActions:
- type: api
label: Call Customer
icon: phone
endpoint: /api/telephony/call
params:
phone: '{phone}'
- type: navigate
label: View Map
icon: map
target: https://maps.google.com?q={address}
openInNewTab: true# Navigate to object list
action:
type: navigate
target: /customers
# Navigate to specific record
action:
type: navigate
target: /customers/{customer_id}
# Navigate to related record
action:
type: navigate
target: /accounts/{account_id}
params:
recordId: '{account_id}'
# Navigate to dashboard
action:
type: navigate
target: /dashboards/sales_overview# Open external URL
action:
type: navigate
label: View Website
target: https://{website}
openInNewTab: true
# Deep link to external app
action:
type: navigate
label: Open in Slack
target: slack://channel?id={slack_channel_id}
# Email link
action:
type: navigate
label: Send Email
target: mailto:{email}?subject=Hello&body=Message# Navigate with state
action:
type: navigate
target: /orders/new
state:
customerId: '{customer_id}'
prefillData:
shipping_address: '{address}'
# Navigate and replace history
action:
type: navigate
target: /customers
replace: true # Don't add to history stackTrigger external webhooks:
action:
type: webhook
label: Notify Slack
url: https://hooks.slack.com/services/XXX/YYY/ZZZ
method: POST
headers:
Content-Type: application/json
body:
text: "New customer: {name}"
channel: "#sales"Initiate OAuth flows:
action:
type: oauth
label: Connect Google Calendar
provider: google
scopes:
- https://www.googleapis.com/auth/calendar
onSuccess:
- action: toast
message: Google Calendar connectedUpload files to external storage:
action:
type: file_upload
label: Upload Attachment
accept: .pdf,.docx,.xlsx
maxSize: 10485760 # 10MB
storage: s3
bucket: customer-documents
path: customers/{customer_id}/
onSuccess:
- action: update_field
field: attachment_url
value: '{uploadedUrl}'action:
type: update_field
field: total_price
value: '{quantity} * {unit_price}'
# Multiple fields
action:
type: update_fields
fields:
status: completed
completed_at: '{$now}'
completed_by: '{$userId}'action:
type: clear_fields
fields: [shipping_address, shipping_method]
# Clear with condition
action:
type: clear_fields
when: { field: shipping_required, value: false }
fields: [shipping_address]action:
type: show_fields
fields: [billing_address, payment_method]
action:
type: hide_fields
fields: [internal_notes, api_key]action:
type: toggle_section
section: advanced_settingsaction:
type: toast
message: Customer saved successfully
variant: success # or 'error' | 'warning' | 'info'
duration: 3000 # milliseconds
position: top-right
# With action button
action:
type: toast
message: Draft saved
variant: info
action:
label: View
onClick:
type: navigate
target: /customers/{recordId}action:
type: alert
title: Error
message: Failed to save customer
variant: error
buttons:
- label: Retry
action:
type: standard_save
- label: Cancelaction:
type: api
label: Generate Report
endpoint: /api/reports/generate
showProgress: true
progressMessage: Generating report...
onProgress:
- action: update_progress
percent: '{progress}'actions:
- type: workflow
label: Approve
icon: check-circle
variant: primary
workflow: approval_flow
input:
recordId: '{recordId}'
decision: approved
comments: '{approval_comments}'
confirm:
title: Approve this request?
message: This will notify the requestor
onSuccess:
- action: toast
message: Request approved
variant: success
- action: navigate
target: /requests
- type: workflow
label: Reject
icon: x-circle
variant: danger
workflow: approval_flow
input:
recordId: '{recordId}'
decision: rejected
comments: '{rejection_reason}'
confirm:
title: Reject this request?
message: Please provide a reason in the comments
requireField: rejection_reason
onSuccess:
- action: toast
message: Request rejected
variant: warning
- action: navigate
target: /requestsaction:
type: modal
label: Send Email
icon: mail
title: Send Email to Customer
content:
type: form
fields:
- name: template
type: select
label: Email Template
options:
- { value: welcome, label: Welcome Email }
- { value: invoice, label: Invoice Email }
- { value: reminder, label: Payment Reminder }
- name: subject
type: text
label: Subject
value: '{template.subject}'
- name: body
type: richtext
label: Message
value: '{template.body}'
actions:
- type: api
label: Send
variant: primary
endpoint: /api/emails/send
method: POST
params:
to: '{email}'
subject: '{subject}'
body: '{body}'
customerId: '{recordId}'
onSuccess:
- action: toast
message: Email sent successfully
- action: close_modal
- type: standard_cancel
label: Cancelaction:
type: flow
label: Migrate Data
icon: database
flow: data_migration_wizard
steps:
- name: select_source
title: Select Source
fields:
- source_type
- connection_string
- name: field_mapping
title: Map Fields
component: field_mapper
props:
sourceFields: '{sourceFields}'
targetObject: customer
- name: validation
title: Validate Data
component: migration_validator
props:
mapping: '{fieldMapping}'
- name: execute
title: Execute Migration
component: migration_progress
onExecute:
type: api
endpoint: /api/migrations/execute
method: POST
params:
mapping: '{fieldMapping}'
async: true
onComplete:
- action: navigate
target: /migrations/{migrationId}action:
type: modal
label: Export
icon: download
title: Export Customers
content:
type: form
fields:
- name: format
type: select
label: Format
options: [CSV, Excel, PDF]
default: CSV
- name: includeFields
type: multiselect
label: Fields to Include
options: [name, email, phone, address, status]
- name: filterBy
type: select
label: Filter
options:
- { value: all, label: All Customers }
- { value: active, label: Active Only }
- { value: selected, label: Selected Rows Only }
actions:
- type: api
label: Export
variant: primary
endpoint: /api/customers/export
method: POST
params:
format: '{format}'
fields: '{includeFields}'
filter: '{filterBy}'
selectedIds: '{$selectedRows}'
onSuccess:
- action: download
url: '{exportUrl}'
- action: toast
message: Export started. Download will begin shortly.
- action: close_modalinterface Action {
// Core
type: ActionType;
name?: string;
label: string;
// Display
icon?: string;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'link';
size?: 'small' | 'medium' | 'large';
fullWidth?: boolean;
// State
visible?: VisibilityRule;
disabled?: DisabledRule;
loading?: boolean;
// Execution
confirm?: boolean | ConfirmConfig;
async?: boolean;
debounce?: number;
// Callbacks
onBefore?: Action[];
onSuccess?: Action[];
onError?: Action[];
onFinally?: Action[];
// Type-specific props
[key: string]: any;
}
type ActionType =
| 'standard_new' | 'standard_edit' | 'standard_delete'
| 'standard_save' | 'standard_cancel' | 'standard_clone'
| 'navigate' | 'navigate_back'
| 'flow' | 'api' | 'workflow'
| 'modal' | 'toast' | 'alert'
| 'bulk_update' | 'bulk_delete'
| 'update_field' | 'clear_fields' | 'show_fields' | 'hide_fields'
| 'webhook' | 'oauth' | 'file_upload'
| 'custom';- FormView Actions - Form-specific actions
- ListView Actions - List view actions
- Flow Builder - Build multi-step flows
- Workflow Engine - Server-side automation