Appearance
Actions
Actions are Atelier's execution plane: the bridge between what exists (your entities) and what is experienced (your screens). Where entities answer "what is true," actions answer "what can happen — to whom, under what conditions, and with what consequences."
You never write code to add a behavior. You declare an action and its parts, and Atelier compiles that declaration, at request time, into a uniform pipeline that validates inputs, checks preconditions, applies changes, records an audit trail, and triggers any follow-on effects. Adding a new behavior means adding a declaration — never editing an engine.
What an action is made of
An action is one declaration plus a small set of composable parts:
| Part | Purpose |
|---|---|
| Parameters | The inputs the action collects — the fields of its form, with types, constraints, and where their values may come from. |
| Preconditions | The criteria that must hold for the action to run: a field comparison, a computed check, or a reachability rule. If they fail, the action is refused with a clear message. |
| Edits | Declarative changes the action applies to the target entity once its preconditions pass. |
| Creates | Related records the action brings into being as part of the same operation. |
| Side effects | Events the action emits — most commonly a notification — when it succeeds. |
| Placements | Where the action appears: each placement projects the action onto a surface as a button or a form. |
Because placements are separate from the action itself, any action can appear on any compatible surface, and the catalog of actions grows simply by declaring more of them.
Inputs and value sources
Every input — and every value an action writes — can be a literal or a reference. References let an action pull from the submitted parameters, the target entity, the current user, the current time, a freshly created record, or a named function. This is what lets a single declaration express rich behavior without bespoke code: "set the resolver to the current user," "stamp the closed-at time to now," "copy the parent's organization onto the new record."
From a simple change to a durable workflow
Actions span a spectrum. A simple action — acknowledge, assign, close — runs synchronously and returns immediately. When an action is marked as a workflow, it is handed to Atelier's durable-execution backend instead: it can run for a long time, retry safely, pause for human approval, and call back to apply its results. The same declaration model covers both; you choose the execution mode. See Workflows & durable execution.
How it's declared
Two authoring surfaces, one model:
- The visual editor. A guided editor organizes the work into tabs — Purpose, Inputs, Rules, Effects, Placements, and Activity — so an operator can compose an action, prefill inputs from an entity's schema, add preconditions, wire up notifications, and place the action on a surface, all with a live preview that matches what ships.
- The declarative sheet. The same action is expressible in a Vertical Sheet under an
actions:block, with nestedparameters,creates,edits,submission_criteria,side_effects, andplacements. See Vertical Sheets.
There is no third, hidden source of truth: the declaration is the runtime contract.
How it's provisioned
Action types are part of the shared catalog. The complete, worked set of actions lives on the template and is copied — and rewired to stand alone — when you fork the template onto a new tenant. Afterward, each tenant edits its own actions with the same editor, with no special-case code. See Provisioning & tenancy.
Extension points
- A new action — declare it in a sheet's
actions:block or create it in the editor. - A new value source — extend the reference resolver and it becomes selectable in the editor.
- A new function — register a named function and reference it as a value or a precondition.
- A new precondition operator — add it to the operator set; it becomes available to every action.
- A new surface — register a surface and actions can be placed on it; no enums to edit.
- A new notification — declare the action's side effect and subscribe a rule to it. See Notifications.
The write contract
Two promises govern every write an action makes — worth knowing whenever you call the API directly rather than through the generated forms.
Partial updates: what omission means
The built-in update action is PATCH-shaped. When you submit a partial body, presence is decided by membership — whether the key is in your payload — never by whether its value happens to be truthy:
- Omit a field → it is left untouched. A parameter you don't send never writes anything.
- Send an explicit
null→ that is a deliberate clear, and the field is cleared. - Send a falsy value —
false,0,"", an empty list — → it is written verbatim. Supplied is not the same as omitted.
Create-shaped actions deliberately work differently: omitting a field there means "let the declared defaults and platform stamps fill it in." The two contracts are specified per lane, not homogenized — an update must never invent values, and a create must never refuse to.
When a write fails
A failed write always tells you why, in the right status class, without ever leaking platform internals:
- A rejection you caused — a validation error, a missing target, a conflict, a malformed payload — comes back with that same status (
400,404,409,422) and the data engine's own reason, in a typed envelope: the failed operation, the entity type, the upstream status, and a sanitized detail. - A failure inside the platform comes back as a
502with the detail redacted — upstream internals never cross into a response. - Deleting a row that doesn't exist (or was already deleted) is a
404— not a precondition violation, and not a silent success. A repeat delete tells you plainly that the row is gone. - Bulk actions report per row. Each failed row carries its own status and sanitized reason, so one bad row never obscures — or takes down — the rest of the batch.
What you can rely on
- The declaration is the contract. Actions are compiled from their declarations on demand — there is no stale cache to fall out of sync.
- The editor and the runtime agree. Whether an action is offered to a user is decided by the same preconditions the runtime enforces when it runs, so a visible action is a runnable one.
- Every run is audited. Each action records an activity entry — who did what, and what changed — which powers the Activity view.
- Effects never compromise the action. Notifications, events, and audit are best-effort and never cause an otherwise-valid action to fail.
- Omission never destroys data. A field you don't send is a field the platform doesn't touch — see the write contract.
Example: an action in a sheet
A staff-facing Validate action on a proposal. It collects no input, refuses unless the proposal is in received state, writes status: validated, emits a notification, and appears as a button on the admin detail page.
yaml
actions:
- key: validate_proposal
label: Validate
target_model: proposal
execution_mode: engine
status: published
is_active: true
permissions_policy:
view: ["admin", "staff"]
apply: ["admin"]
edit: ["admin"]
submission_criteria:
- key: must-be-received
criteria_type: field
config: { field: status, operator: eq, value: received }
message: Only received proposals can be validated.
order: 1
is_active: true
edits:
- { key: set-validated, field: status, value: validated }
side_effects:
- { key: proposal_validated, type: notification, event_type: proposal_validated, event: proposal_validated, order: 1, is_active: true }
placements:
- { key: validate-on-detail, surface: admin_detail, target_model: proposal }Values can be literals or references — $parameters.x, $entity.x, $user, $now, or $function.name — so an action expresses rich behavior without code.