Actions
Named operations the platform exposes as REST endpoints, Console buttons, flow steps, and AI tools — from one declaration.
Actions
An Action is a named operation on an object. Declare it once and it appears as:
- a REST endpoint at
/api/v1/actions/<object>/<action> - a button in Console's record detail
- a flow step (
type: 'action') for automation - an AI tool (
action_<name>) for Agents and the AI Builder
You don't repeat yourself across four surfaces. One declaration; four ways to call it.
Declare an action
// src/actions/approve_invoice.action.ts
import { Action } from '@objectstack/spec';
export const approveInvoice = Action.create({
name: 'approve_invoice', // lowercase snake_case (machine id)
label: 'Approve Invoice',
objectName: 'invoice', // attaches to the invoice object
icon: 'check',
variant: 'primary',
locations: ['record_header'], // where the button shows
confirmText: 'Approve this invoice?',
successMessage: 'Invoice approved',
refreshAfter: true,
// collect input before running
params: [
{ name: 'note', label: 'Approval note', type: 'textarea' },
],
// only show the button when the record is still pending
visible: 'record.status == "pending"',
// what it does — a sandboxed script body
type: 'script',
body: {
language: 'js',
source: `
await ctx.data.update('invoice', input.id, {
status: 'approved',
approved_by: ctx.user.id,
approved_at: now(),
approval_note: input.note,
});
`,
},
});After os dev recompiles:
POST /api/v1/actions/invoice/approve_invoiceworks- The Invoice record page in Console shows an Approve Invoice button
- A flow can include
{ type: 'action', action: 'approve_invoice', inputs: { note: '…' } } - The AI assistant can call
action_approve_invoiceif its skills allow
Action types
The type field decides what the action does:
type | What runs | Use for |
|---|---|---|
script | A body — an L1 formula expression or sandboxed L2 JavaScript | Most cases — server-side logic, auditable + AI-callable |
api | An HTTP call to a target endpoint (method, bodyExtra) | Reusing data-API or platform endpoints |
flow | Runs the flow named in target | Multi-step business processes |
url | Navigates to the target URL | Deep links, redirect-style actions |
modal | Opens the page/modal named in target | Custom dialogs |
form | Opens the FormView named in target | Guided data entry |
// api type — reuse a data-API endpoint
Action.create({
name: 'archive_order',
objectName: 'order',
label: 'Archive',
locations: ['list_item'],
type: 'api',
method: 'PATCH',
target: '/api/v1/data/order/{id}',
bodyExtra: { archived: true },
});Types other than script require a target. Whichever type, the action
is the same first-class citizen on every surface.
Calling an action
REST
# the record id can go in the body, or in the path
curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice/inv_123 \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"note": "LGTM"}'Params are sent flat in the request body. The record id can be supplied
either as a trailing path segment (.../approve_invoice/:recordId) or in
the body. The response is your script body's return value (or the call
result for api-type actions).
Console
By default, Console shows actions as buttons on the record detail page,
filtered by the action's visible predicate. Override placement
in your view config:
defineView({
name: 'invoice_detail',
object: 'invoice',
actions: ['approve_invoice', 'reject_invoice', 'send_to_customer'],
});From a flow
{
type: 'action',
action: 'approve_invoice',
inputs: { note: 'Auto-approved by SLA flow' },
record: '{!trigger.record.id}',
}From an AI Agent
If approve_invoice is in any skill the agent has, the LLM can call
it. Inputs come from the conversation; permissions are enforced as if
the user invoked it directly.
"Approve invoice INV-2042 with note 'verified by phone.'"
Permissions
Actions run with the calling user's permissions. The platform checks:
- Object permissions — the user's permission sets must grant the object-level access the action needs (e.g. update).
- Field permissions — for any field the action writes, the user must have write access (FLS).
- UI gating — the
visibleanddisabledpredicates (CEL, evaluated againstrecord,os.user, and params) control whether the button renders or is greyed out in Console.
Failed permission checks return 403 with a PERMISSION_DENIED error.
Built-in actions
Every object gets these for free:
| Action | What it does |
|---|---|
create | Insert a record |
update | Update a record |
delete | Delete (or soft-delete) a record |
restore | Undo a soft-delete |
clone | Deep-copy a record |
share | Direct share with a user / role |
Don't redeclare these — they follow the object's lifecycle & capability flags.
Auditing
Platform events land in sys_audit_log, an immutable trail with fields
including:
user_id— the originating useraction— the action nameobject_nameandrecord_id— what was touchedold_value/new_value— the changeip_address/user_agent— request origincreated_at— when it happened
This is your first stop for "who pushed the button?" questions.
Generating actions with the AI Builder
"Create an action
escalate_ticketonsupport_ticketthat sets priority to urgent and assigns it to the on-call engineer."
The AI Builder generates the action metadata and queues the change for approval. After approval, the action is callable from REST, Console, flows, and — recursively — the AI itself.
Where to go next
- Flows — compose multiple actions into business logic
- Agents — expose actions as AI tools
- API Access — call actions from external systems
- Permissions — gate who can call what