Action
Action protocol schemas
Action Parameter Schema
Defines inputs required before executing an action.
Two declaration modes:
-
Field-backed (preferred) — reference an existing object field; the runtime resolves the field's label (i18n), type, validation rules, options, placeholder, help text, and widget mapping from object metadata. Cross-object references use
objectOverride.params: [ { field: 'email' }, // same object { field: 'role', objectOverride: 'sys_member' }, // different object ] -
Inline (legacy / bespoke) — declare
name,label,typeetc. inline when no matching object field exists. Inline values may also be used alongsidefieldto override individual properties. Alookup/master_detailparam declared this way MUST name its target object viareference— there is no field to inherit it from:params: [ { name: 'inspector', label: 'Inspector', type: 'lookup', reference: 'sys_user' }, ]
name is required unless field is provided (in which case it defaults
to the field name and is used as the request-body key).
Source: packages/spec/src/ui/action.zod.ts
TypeScript Usage
import { ActionSchema, ActionAiSchema, ActionLocationSchema, ActionParamSchema, ActionType, InlineActionSchema } from '@objectstack/spec/ui';
import type { Action, ActionAi, ActionLocation, ActionParam, ActionType, InlineAction } from '@objectstack/spec/ui';
// Validate data
const result = ActionSchema.parse(data);Action
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Machine name (lowercase snake_case) |
| label | string | Record<string, string> | ✅ | Display label |
| description | string | Record<string, string> | optional | Explanatory line shown under the title in the action's param dialog. Carries the confirm question for an action that collects params (one dialog, not two — #7278). Not the LLM-facing ai.description. |
| objectName | string | optional | Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack(). |
| icon | string | optional | Icon name |
| locations | Enum<'list_toolbar' | 'list_item' | 'record_header' | 'record_more' | 'record_related' | 'record_section'>[] | optional | Locations where this action is visible |
| component | Enum<'action:button' | 'action:icon' | 'action:menu' | 'action:group'> | optional | Visual component override |
| type | Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'> | optional | Action functionality type |
| target | string | optional | URL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation. |
| openIn | Enum<'self' | 'new-tab'> | optional | For type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only). |
| body | { language: 'expression'; source: string } | { language: 'js'; source: string; capabilities?: Enum<'api.read' | 'api.write' | 'api.transaction' | 'crypto.uuid' | 'log'>[]; timeoutMs?: integer; … } | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is script. |
| execute | never | optional | [REMOVED] execute was removed in @objectstack/spec 17 (#3855) — use target. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run os migrate meta --from 16 to rewrite existing sources automatically. |
| params | { name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[] | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra). |
| variant | Enum<'primary' | 'secondary' | 'danger' | 'ghost' | 'link'> | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
| order | number | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without order keep their registration order. |
| confirmText | string | Record<string, string> | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty params is refused (#7428) — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |
| successMessage | string | Record<string, string> | optional | Success message to show after execution |
| errorMessage | string | Record<string, string> | optional | Error message to show when the action fails (overrides the raw error). |
| refreshAfter | boolean | optional | Refresh view after execution |
| undoable | boolean | optional | Offer an Undo affordance after this single-record update action succeeds. |
| resultDialog | { title?: string | Record<string, string>; description?: string | Record<string, string>; acknowledge?: string | Record<string, string>; format?: Enum<'qrcode' | 'code-list' | 'secret' | 'text' | 'json'>; … } | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). |
| visible | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | optional | Visibility predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
| requiresFeature | Enum<'twoFactor' | 'organization' | 'multiOrgEnabled' | 'degradedTenancy' | 'oidcProvider' | 'sso' | 'ssoEnforced' | 'deviceAuthorization' | 'admin' | 'phoneNumber' | 'phoneNumberOtp'> | optional | Public auth feature flag gating this action; lowered into visible at parse time. |
| disabled | boolean | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | optional | Disabled predicate — true/false literal, CEL string, or {dialect, source} envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
| requiredPermissions | string[] | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a type: api action pointed at a custom endpoint must re-check it there. |
| shortcut | never | optional | [REMOVED] action.shortcut was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. Run os migrate meta --from 16 to rewrite existing sources automatically. |
| bulkEnabled | never | optional | [REMOVED] action.bulkEnabled was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's bulkActions / bulkActionDefs, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's bulkActions instead. Run os migrate meta --from 16 to rewrite existing sources automatically. |
| ai | { exposed?: boolean; description?: string; category?: Enum<'data' | 'action' | 'flow' | 'integration' | 'vector_search' | 'analytics' | 'utility'>; paramHints?: Record<string, object>; … } | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
| recordIdParam | string | optional | Body key to inject the row id into when running from a list_item context. |
| recordIdField | string | optional | Row field whose value seeds recordIdParam. Defaults to "id". |
| bodyShape | 'flat' | { wrap: string } | optional | Body wrapping: flat (default) or { wrap: key } to nest user-collected params under a key. |
| method | Enum<'POST' | 'PATCH' | 'PUT' | 'DELETE'> | optional | HTTP method for type:"api" actions. Defaults to POST. |
| bodyExtra | Record<string, any> | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes. |
| mode | Enum<'create' | 'edit' | 'delete' | 'custom'> | optional | Semantic mode of the action. |
| opensInNewTab | boolean | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| newTabUrl | string | optional | Direct new-tab URL template ({recordId} placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| aria | { ariaLabel?: string | Record<string, string>; ariaDescribedBy?: string; role?: string } | optional | ARIA accessibility attributes |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
ActionAi
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| exposed | boolean | ✅ | Expose this action to AI agents. Requires description when true. |
| description | string | optional | LLM-facing description (≥40 chars). Required when exposed. |
| category | Enum<'data' | 'action' | 'flow' | 'integration' | 'vector_search' | 'analytics' | 'utility'> | optional | Tool category override (defaults to "action"). |
| paramHints | Record<string, { description?: string; enum?: (string | number)[]; examples?: any[] }> | optional | Per-parameter AI hints keyed by param name. |
| outputSchema | Record<string, any> | optional | JSON Schema for the action return value. |
| requiresConfirmation | boolean | optional | Override HITL confirmation for AI invocations. |
ActionLocation
Allowed Values
list_toolbarlist_itemrecord_headerrecord_morerecord_relatedrecord_section
ActionParam
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| field | string | optional | Snake case identifier (lowercase with underscores only) |
| objectOverride | string | optional | Snake case identifier (lowercase with underscores only) |
| label | string | Record<string, string> | optional | Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time |
| type | Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | 'markdown' | 'html' | 'richtext' | 'number' | 'currency' | 'percent' | 'date' | … +35 more> | optional | |
| required | boolean | optional | |
| options | { label: string | Record<string, string>; value: string; visibleWhen?: string | object }[] | optional | |
| placeholder | string | optional | |
| helpText | string | optional | |
| defaultValue | any | optional | |
| multiple | boolean | optional | Allow multiple values (array value shape); mirrors FieldSchema.multiple. |
| accept | string[] | optional | Accepted upload types (MIME types / extensions) for file/image params. |
| maxSize | integer | optional | Max upload size in bytes for file/image params. |
| reference | string | optional | Reference target object for inline lookup/master_detail params; mirrors FieldSchema.reference. |
| defaultFromRow | boolean | optional | |
| visible | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | optional | Param visibility predicate (CEL); omits the param when false. |
| requiresFeature | Enum<'twoFactor' | 'organization' | 'multiOrgEnabled' | 'degradedTenancy' | 'oidcProvider' | 'sso' | 'ssoEnforced' | 'deviceAuthorization' | 'admin' | 'phoneNumber' | 'phoneNumberOtp'> | optional | Public auth feature flag gating this param; lowered into visible at parse time. |
Allowed Values: ActionParam.type
texttextareaemailurlphonepasswordsecretmarkdownhtmlrichtextnumbercurrencypercentdatedatetimetimebooleantoggleselectmultiselectradiocheckboxeslookupmaster_detailtreeuserimagefileavatarvideoaudioformulasummaryautonumbercompositerepeaterrecordlocationaddresscodejsoncolorratingslidersignatureqrcodeprogresstagsvector
ActionType
Allowed Values
scripturlmodalflowapiform
InlineAction
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | Enum<'script' | 'url' | 'modal' | 'flow' | 'api' | 'form'> | optional | Action functionality type |
| name | string | optional | Machine name (lowercase snake_case) |
| label | string | Record<string, string> | optional | Display label |
| target | string | optional | URL, Script Name, Flow ID, or API Endpoint. Supports ${param.X} and ${ctx.X} interpolation. |
| openIn | Enum<'self' | 'new-tab'> | optional | For type:'url' — where to open target. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of params (which is user-input-collection only). |
| method | Enum<'POST' | 'PATCH' | 'PUT' | 'DELETE'> | optional | HTTP method for type:"api" actions. Defaults to POST. |
| params | { name?: string; field?: string; objectOverride?: string; label?: string | Record<string, string>; … }[] | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in bodyExtra). |
| bodyExtra | Record<string, any> | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). {{page.<var>}} tokens are resolved by the runtime. This — not params — is where a payload goes. |
| confirmText | string | Record<string, string> | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty params is refused (#7428) — that opens a second dialog for one decision; put the question on description instead. Correct on a param-LESS action, where the confirm is the only dialog there is. |
| successMessage | string | Record<string, string> | optional | Success message to show after execution |
| errorMessage | string | Record<string, string> | optional | Error message to show when the action fails (overrides the raw error). |
| refreshAfter | boolean | optional | Refresh view after execution |
| opensInNewTab | boolean | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |