Skip to content

Semantic actions

An action is a business operation with an id, a description and a JSON Schema for its parameters. One declaration serves the AI (function calling), macros, ui.json commands and buttons, and undo:

  • An id is namespace/name, both identifiers (no hyphen: use my_plugin/run); any other id, or a schema with an unknown JSON Schema keyword, is refused at registration. The contract comes from @scfloworg/actions.

  • execute(id, params, context?) validates the parameters against their JSON Schema (draft 2020-12, default applied — an error with the code invalid_parameters lists every problem), resolves the context (viewId = active tab, selection = current selection, docId = its document; overrides win) and runs run(params, context) in one transaction: one undo step per document, everything rolled back if run throws. Called from inside a running handler, it joins that transaction.

  • kind: "read" actions run outside any transaction and need no approval.

  • executeBatch([{ action, params, as? }]) runs a macro in one transaction (one undo step); results come back in order, as plain JSON. A step can use an earlier step’s result: as: "taskA" names it, and "${taskA}" or "${taskA.id}" (any path) is replaced by that value before the step is validated, keeping its type when the placeholder is the whole string. A reference to an unknown or later name, or any refused step, stops the batch and rolls every step back. Actions should therefore return what later steps need (e.g. { id }).

    await ctx.actions.executeBatch([
    { action: "notes_tsx/create_task", params: { name: "A" }, as: "a" },
    { action: "notes_tsx/create_task", params: { name: "B" }, as: "b" },
    { action: "notes_tsx/link_tasks", params: { source: "${a.id}", target: "${b.id}" } },
    ]);
  • getAiTools() returns { name, description, parameters, kind, actionId }, name being the id with / . : - replaced by _. subscribe() follows additions and removals; a disabled plugin’s actions disappear.

  • run writes state like a handler does (it runs inside the kernel’s actions/execute handler).

Declare them in ui.json and bind run in code:

"actions": [{
"id": "scflow/create_actor", "title": "action.createActor",
"description": "Create an actor in the active model.",
"parameters": { "type": "object", "properties": {
"name": { "type": "string" },
"domain": { "type": "string", "enum": ["physical", "logical"] }
}, "required": ["name"], "additionalProperties": false },
"run": "createActor"
}],
"commands": [{
"id": "new_actor", "title": "command.newActor",
"event": "action:scflow/create_actor", "payload": { "domain": "physical" }
}]
ctx.ui.define({ actions: { createActor: (params, context) => { /* ... */ } } });
// or in code: ctx.actions.register({ id, title, description, parameters, run })

Anywhere an event name is accepted — commands, palette selectEvent / addEvent, dropEvent, table createEvent, graph createEdgeEvent, map createEvent/moveEvent, form events… — you may write "action:<id>"; no handler is needed. A command’s payload gives its default parameters ("event": "action:<id>", "payload": { … }). When a required parameter is still missing (or the payload has "$prompt": true) the shell opens a dialog built from the schema. Notes shows the pattern: its task table createEvent, its graph createEdgeEvent and its “New task…” command all target actions.

Payload unpacking. For each property the action’s schema declares, the first defined value wins, in this order:

  1. payload[key] — flat payloads (map clicks { lat, lng }, graph links { source, target }, commands);
  2. payload.values[key] — value dictionaries (table creation row, forms);
  3. payload.data[key] — item data (palette cards, transfers).

Undeclared keys are dropped, nothing is renamed or guessed. A docId in the payload or its context becomes the context’s docId. When a gesture’s payload does not fit an action (cell commits { rowId, key, value }, batch deletes { rowIds }, cascades…), keep a handler and call ctx.actions.execute from it.

Every run, whatever its channel (command, AI tool, macro, code), is reported on the bus as actions/ran { actionId, kind, params, status, error?, durationMs, batch? } (informational, never opens a transaction); refused runs (unknown id, invalid parameters) are reported too. The event inspector shows them with a badge. actions/execute carries { label, actionId, params } (or steps for a macro).

AI assistants opt in with useActions: true (or a list of action ids) in their options or ui.json entry; action kinds wait for the user’s approval. They also receive an actions_batch tool { summary, steps: [{ action, params, as? }] } limited to their allowed actions: the model proposes several chained changes at once, the user sees one card (the model’s summary in the user’s language, then each step as its action title and name: value chips, references shown as → taskA.id) and approves them all with one click, undone in one step. Action titles are shown as written, so they need no translation keys.

Action IDs and function names are distinct: scflow/create_item is an action ID; scflow_create_item is its provider-safe function name. Individual calls use the function name; actions_batch.steps[].action uses the exact ID from its schema enum. Tool descriptions include this mapping. The AI adapter also accepts an unambiguous offered function name in a batch (or an offered action ID in a single call), without guessing by replacing underscores. It resolves all batch names before running any step, rejects unknown or ambiguous names, and never expands the assistant’s useActions allowlist. Approval remains mandatory for mutations. Read actions retain their immediate execution behavior.

Documentation under CC BY 4.0 · © SCFlow contributors ·Back to supplychainflow.org