Procedures
An agent that sets status with update_record skips everything the form does: the
onChange that stamps a date, the onSave checks, the workflows that start when the
record is confirmed. A procedure changes the record the way a user would — through the
form.
A procedure is a named run of a form’s own steps — set a field, add a line, click a
button, save — stored on a skill. An agent linked to that skill runs it with the run_procedure tool,
giving only the procedure’s name, the record’s id and the inputs the procedure declares.
The steps are yours; the model never supplies them. Each step runs the form’s chains on
the server, as the browser would (see Headless Forms).
When to use
| The change | Use |
|---|---|
A field no form rule touches — a note, a tag |
|
Anything a form controls — a status, a confirmation, a computed total, a button’s work |
A procedure |
Logic that lives in an action and needs no form |
An ACTION tool |
Write a procedure
The running example is a sale order confirmed from chat. Confirming is what the form’s
Confirmed switch does: its onChange sets the confirm date and the status, and the
workflows watching confirmed start.
-
Read the form. Ask an admin agent to run
inspect_processon the model (see Check procedures against the forms). It lists the action-views that open the form, its buttons and their conditions, and every chain with the questions it can ask. -
Write
procedures.yamlnext to the process skill’sSKILL.md:confirm_order: title: Confirm the order (1) description: >- Recalculates the totals, then confirms the order as the form's Confirmed switch does. action: sale.orders (2) when: "!confirmed && customer" (3) inputs: confirmDate: { type: date } (4) steps: - click: btnTotalAmount (5) - set: { confirmDate: "eval: inputs.confirmDate ?: confirmDate" } - set: { confirmed: true } (6) - save: true1 What the approval card asks: "Confirm the order?". 2 The action-view whose form it runs in — the one users open orders with. 3 The guard, on the record as loaded. A false guard refuses the run before the user is asked; nothing of the form runs for that check. The run reads it again after the form’s load hooks. 4 The only values the model gives. Read them as inputs.<name>, never bare: an input may share a field’s name.5 A button, by name: its onClickchain runs.6 The switch: its onChangeruns after the value is set. -
Link it. Give the agent the skill and the
run_proceduretool, andupdateon the procedure’s model in itspermissions—createfor one that starts a new record. -
Load it. Restart, or use Reload built-in defaults on the Import / Export screen. A file that does not read is refused at load. A procedure that does not match the form loads with a warning in the log, naming the procedure and the step, and a run refuses it until it is fixed.
| Tell the agent when to run each procedure in the skill’s instructions. The model sees the procedures — name, title, guard and inputs — only once it has loaded the skill. |
The format
| Key | Meaning |
|---|---|
|
Required. What the approval card asks, and how the agent refers to it. |
|
What it does, for the agent. |
|
Required. An action-view: it gives the model, the form and the context the form expects. |
|
|
|
|
|
A guard, as a server expression on the record’s fields. |
|
|
|
What a user does, in order. See Steps. |
|
Answers to the form’s questions where no one can answer them. See Answer questions without a user. |
Expressions are the server’s, as in action-record: the
record’s fields by name, the action-view’s context, and the inputs as inputs.<name>.
An enum field equals no string: status == 'DRAFT' is false on a draft. Compare it to its
type — status == OrderStatus.DRAFT — as inspect_process lists an enum’s states.
Nothing is saved without a save step, or a save in a chain the steps run.
Steps
| Step | Does |
|---|---|
|
Writes each value, a literal or |
|
A new line: its defaults, its |
|
The button’s prompt, then its |
|
A hook, or a chain a field or button declares, run again. |
|
A line out of its collection; the collection’s |
|
Leaves a line now: the collection’s |
|
Leaves the open lines, then |
A line is named by its path: items[#42] by id, items[-1] for the first line the
procedure added (items[-2] the second…), items[0] by position. A line’s field is written
after it, items[-1].qty, and a line of a line is items[-1].taxes[-2]. A custom field is
attrs.name or its bare name. A value for a line is evaluated on that line, its record as
_parent.
A step on a line runs in the line’s context, as the browser runs it. Moving on to another
record, or saving, leaves the line: its collection’s onChange runs, once. For example, to
add a line and let the order recompute its totals:
add_line:
title: Add a product to the order
action: sale.orders
when: "!confirmed"
inputs:
product: { type: reference, model: com.axelor.sale.db.Product, required: true }
steps:
- add: { items: { product: "eval: inputs.product", quantity: 1 } }
- click: btnTotalAmount # leaves items[-1] first: items' onChange runs
- save: true
A line the model chooses is named by an input: items[@line] is the line of items whose
id the input line gives. The input is required, and is an integer id or a reference to the
line’s model; an id of a line the record does not hold is refused when the step runs. For
example, to change the quantity of one line of an order:
set_quantity:
title: Change the quantity of an order line
action: sale.orders
when: "!confirmed"
inputs:
line: { type: reference, model: com.axelor.sale.db.OrderLine, required: true }
quantity: { type: integer, required: true }
steps:
- set: { "items[@line].quantity": "eval: inputs.quantity" }
- save: true
The full rules — where a line’s hooks come from, when a line is left — are those of headless forms.
From client expressions to a procedure
showIf, readonlyIf, requiredIf, x-bind, a static readonly or hidden, and what an
action-attrs sets on the way run in the browser, not on the server, so a procedure goes
past them. inspect_process lists them for each form and its lines under Limits, and a
procedure with no when is refused when one of its steps is on anything listed there. Write
their meaning in:
-
A button’s
showIforreadonlyIf, or a field’sreadonlyIf→ part ofwhen. So is a statichiddenorreadonlyon a button or field the steps use, and anaction-attrsthat locks one. -
An
x-bindthe steps depend on → an explicitset. -
A
requiredIf→ a required input, or a clause inwhen. -
An
onSelectdomain → awhen, or an input the caller picks within it.
What the user sees
| Moment | In the chat |
|---|---|
Before the run |
An approval card: the procedure’s title as the question ("Confirm the order?"), the record it acts on, Approve and Decline. A run the procedure would refuse — a false guard, an input of the wrong type, a record the user may not change — is refused without a card. |
The form asks a question |
A confirmation card with the form’s own words and buttons — the alert or prompt the browser would have shown. Only the user who started the run can answer; the agent never does. The run waits up to a day, and an answer counts once, for the question it was given for. If someone changes the record meanwhile, the run’s save fails and nothing is saved over their change. |
After the run |
The agent reports what the result says: done or why it stopped, whether the record was
saved, what changed, the messages. A save before a stop stays saved; what changed after it
is told apart, as not saved. A field no step set that the form or the save emptied is
listed with the value it had, so the agent tells the user. What the form’s own actions saved themselves stays too — an
invoice made on confirmation, the record saved by an |
Starting from the open form
When the user has the record open in a form beside the chat, the run starts from what the form shows, unsaved edits included — they are saved with the procedure’s own changes. The agent then refreshes the form. If the form is older than the saved record, the run starts from the saved record instead and says so.
Answer questions without a user
A chain can stop to ask: an action-validate alert, a button’s prompt. In a chat, the
user answers on the card. Where the answer is always the same, write it down:
interactions:
"action-sale-order-validate: No Sale Order Items*": refuse
The key is the action’s name, : `, and its message as written in the action — exactly, or
its start followed by `*. The action’s name alone matches all its questions. accept answers Yes; refuse answers No
and ends the run. A question the procedure does not answer goes to the user in a chat, and
stops the run where there is no one to ask.
Check procedures against the forms
inspect_process is a read-only tool for AI administrators. Given a model, it digests
how its forms work from their definitions: the menus and action-views that open them, the
fields and buttons with their client expressions, the hooks, and every chain written out
with the alerts, errors and Java steps it can reach.
Each form comes with its Limits: what the browser would do that a run does not, for the form and for the lines of each collection.
Given a skill instead, it checks the skill’s procedures against the forms as they are now:
what would stop them — a renamed button, a removed field, a step on something the form
limits with no when — and the questions their interactions leave unanswered. Run it after
an upgrade.
Procedures are also checked whenever they are saved on the skill’s form or by the
Agent Builder's save_skill, where a mismatch refuses the save.
At seed load and bundle import a mismatch is only logged, so that a bundle exported before a
form changed still imports.
Safety
-
The user’s rights, nothing more. Each step is a request made as the user, with the same permissions and the same server-side checks as the browser’s.
-
What a procedure sets, the user must be able to write. Every field a
setstep or anaddstep’s seed names —items[-1].title, oritems[+].titlefor the line anaddseeds — must be one the user’s field rules let them write, or the run is refused before anything of the form runs. The form’s ownonChangeandonSavewrite what they write, as in the browser. What the run reports it changed leaves out, under$hidden, what the user may not read. -
Linking the skill is the grant. An agent runs only the procedures of the skills linked to it, on models its permissions, and the user’s, allow it to
update—createfor a procedure that starts a new record. -
The model gives no steps. Only a procedure name, a record id and the declared inputs, each checked against its type.
-
Every run is approved.
run_procedureasks the user first; tell the agent not to ask again withask_to_continue. -
Not from workflows. A workflow’s
<agent>node never getsrun_procedure: it may modify data, and there is no one to approve it.
Client expressions are not a security boundary — a REST save ignores them too. A
procedure enforces exactly what its when and set say, and what the server enforces.
|