ObjectOS
Build

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_invoice works
  • 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_invoice if its skills allow

Action types

The type field decides what the action does:

typeWhat runsUse for
scriptA body — an L1 formula expression or sandboxed L2 JavaScriptMost cases — server-side logic, auditable + AI-callable
apiAn HTTP call to a target endpoint (method, bodyExtra)Reusing data-API or platform endpoints
flowRuns the flow named in targetMulti-step business processes
urlNavigates to the target URLDeep links, redirect-style actions
modalOpens the page/modal named in targetCustom dialogs
formOpens the FormView named in targetGuided 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:

  1. Object permissions — the user's permission sets must grant the object-level access the action needs (e.g. update).
  2. Field permissions — for any field the action writes, the user must have write access (FLS).
  3. UI gating — the visible and disabled predicates (CEL, evaluated against record, 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:

ActionWhat it does
createInsert a record
updateUpdate a record
deleteDelete (or soft-delete) a record
restoreUndo a soft-delete
cloneDeep-copy a record
shareDirect 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 user
  • action — the action name
  • object_name and record_id — what was touched
  • old_value / new_value — the change
  • ip_address / user_agent — request origin
  • created_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_ticket on support_ticket that 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

On this page