Actions and writes

The 11 action kinds and which four 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 rowSupported, unexercised
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, so there is no production example and no behaviour we can describe with confidence. Try them and verify what you get; prefer the four 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 viewsedit, delete and per-verb child writes.
Was this page helpful?