Actions and writes

The 11 action kinds and which five are in production use, how write and an api action dispatch to your app's own capability, and the request-body dialog that makes a parameterised capability reachable.

Two mechanisms send work to your own code: write replaces where a surface saves, and an api action puts a button on a screen that calls a route.

The action shape

An action is { label, kind } plus whatever the kind needs. It is the same shape everywhere: a list view's actions, row_actions and bulk_actions, a detail view's actions and section actions, an empty state's action, and a banner's action.

cockpit.json
{
  "label": { "en": "Receive goods", "de": "Ware vereinnahmen" },
  "icon": "add-circle",
  "kind": "api",
  "method": "POST",
  "endpoint": "/inventories/receive",
  "variant": "primary",
  "confirm": {
    "title": { "en": "Receive goods" },
    "message": { "en": "Books a delivery into stock and writes an inbound row in the movements ledger." }
  },
  "fields": [
    { "name": "sku", "type": "text", "label": "SKU" },
    { "name": "quantity", "type": "number", "label": { "en": "Quantity received" },
      "required": true, "validation": { "min": 0.001 } },
    { "name": "location_code", "type": "text", "label": { "en": "Location code" },
      "description": { "en": "Leave empty to use the tenant's default location." } }
  ],
  "submitLabel": { "en": "Book receipt" },
  "success_message": { "en": "Receipt booked." }
}

That is a real shipped example, from the inventories app's stock list.

The 11 kinds — and which are proven

text
navigate    api     link      create      delete
edit-dialog bulk-edit         bulk-adjust
inline-create         assign            refresh
KindDoesStatus
apiCalls your app's route (endpoint, method)In production use
navigateMoves to a Cockpit route (to)In production use
createOpens the entity's create formIn production use
deleteDeletes the record, with a guardIn production use
linkOpens an external URL (url)Supported, unexercised
edit-dialogEdits the record in a dialogSupported, unexercised
bulk-editEdits every selected rowIn production use (promotions)
bulk-adjustApplies a percent or absolute adjustment to a column across selected rowsSupported, unexercised
inline-createAdds a row without leaving the listSupported, unexercised
assignOpens an assignment pickerSupported, unexercised
refreshRe-fetches the viewSupported, unexercised
"Supported, unexercised" means exactly that. The kind is in the schema and the renderer accepts it, but no shipped commerce app declares one (as of 2026-10), so there is no production example and no behaviour we can describe with confidence. Try them and verify what you get; prefer the five proven kinds where either would do.

Common fields on any action:

KeyDoes
iconA Solar icon name — see Navigation.
variantprimary, secondary, danger or ghost.
confirmA confirmation dialog with title and message.
success_messageThe toast after it lands.
refreshWhich views to re-fetch afterwards.
placementheader or footer.
enabled_when + disabled_reasonConditional, with the reason said out loud.
noteA sentence beside the button saying what the action does NOT do. The app owns this claim; the renderer must never invent one.
resultHow the response body is used: toast, dialog, navigate or download.
prefillValues seeded into the dialog.
guardOn delete: the same blocker configuration a detail view's delete takes.
adjustOn bulk-adjust: what to move and by how much.

note earns its place. "Receiving goods does not create a purchase order" is exactly the kind of thing an operator needs and a generic renderer cannot know.

write — where a surface saves

By default the renderer writes the entity directly, and that is the right default for an admin surface: a position column, a label, a boolean flag gain nothing from a round trip through your function that would write the same column back.

cockpit.json
"write": { "method": "POST", "endpoint": "/suppliers" }
FieldRequiredMeaning
endpointYesYour app's own namespaced route. :param is substituted from the route params and the record id.
methodNoDefaults to what the surface is doing — POST for a create, PATCH for an edit, DELETE for a delete. Say PUT if you model an update as a full replacement.

Declare it for what the database cannot do on its own: a contact that must also become a platform user, an organization mirrored to a team, a stock movement that has to leave a ledger entry behind it.

write is accepted on a form view, a detail view's edit, and — per verb — on a detail view's child list. A verb left out of a child list's write stays a plain row write, so a collection can route its creates through the app and keep editing a position column as the cheap write it is.

How an endpoint is resolved

Both write.endpoint and an api action's endpoint resolve against your app's own capability routes under /v1:

text
endpoint:  /inventories/receive
              │
              ▼
POST https://api.revenexx.com/v1/inventories/receive

The request carries the operator's Authorization: Bearer <jwt>, plus X-Revenexx-Tenant and X-Revenexx-Market. The Cockpit calls the same public route a partner integration would — there is no privileged internal path.

The capability has to exist. An endpoint with no matching declaration in manifest.capabilities.json gives the operator a 404 after they click. Declare the route, and remember that a capability route uses {braces} while a manifest endpoint uses :param:
manifest.capabilities.json
{
  "type": "define",
  "capability": "inventories.receive",
  "version": "1.0.0",
  "route": { "method": "POST", "path": "/inventories/receive" },
  "response": { "title": "StockReceived", "type": "object" }
}
See Capabilities.

fields — the request-body dialog

An api action dispatches with an empty body by default. To collect arguments first, declare fields: the same field shapes a form view uses, rendered as a dialog, with the collected values becoming the JSON request body.

cockpit.json
{
  "label": { "en": "Adjust stock" },
  "kind": "api",
  "method": "POST",
  "endpoint": "/inventories/adjust",
  "fields": [
    { "name": "quantity", "label": { "en": "Change by" }, "type": "number", "required": true },
    { "name": "reason", "label": { "en": "Reason" }, "type": "select", "required": true,
      "vocabulary": "inventories.movement-reasons" }
  ],
  "submitLabel": { "en": "Apply" },
  "result": "dialog"
}

This is what makes a capability that takes arguments reachable from the Cockpit at all. Without it, only the parameterless half of your app's state machine can be driven from the UI — which is exactly the gap that left /v1/inventories/adjust unreachable while an editable on_hand field wrote straight through.

fields is only meaningful for kind: "api". Omitting it dispatches immediately with an empty body, which is what every action that predates the key does — so adding it never changes an existing one.

submitLabel and submit_label are both accepted; the snake-case spelling is tolerated for symmetry with formView.submit_label.

result: "dialog" and dry runs

cockpit.json
{
  "label": { "en": "Preview rule" },
  "kind": "api",
  "method": "POST",
  "endpoint": "/products/categories/:id/rules/preview",
  "result": "dialog"
}

result decides what happens to the response body. dialog shows what came back, which is the whole point of a dry run — a preview, a readiness report, a usage count. download is for an export; navigate follows an id in the response; toast is the default.

Several commerce routes are built for exactly this shape: rule previews, market readiness, tax-class usage.

Bulk action dispatch — two semantics

KindDispatch
apiCalled once per selected row, with :id substituted per row.
navigateEntered once, receiving the whole selection as ?ids=<comma-separated>.

An api bulk action over 500 rows is 500 requests. If that is wrong for your route, publish a batch capability and use a navigate action into a form that posts it.

Choosing between the two

SituationUse
Edit a label, a flag, a positionNothing. The row write is correct.
A save with a side effect the database cannot producewrite on the form or edit
An operation that is not a save at all — receive, ship, recompute, refreshAn api action
An operation that needs argumentsAn api action with fields
A read whose answer is the point — a preview, a diagnosisAn api action with result: "dialog"
Move the operator somewherenavigate

Where to go next

  • Capabilities — declaring the route an endpoint reaches.
  • Forms — the field shapes a dialog reuses.
  • List views — where toolbar, row and bulk actions live.
  • Detail views — edit, delete and per-verb child writes.
Was this page helpful?