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

update_record

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.

  1. Read the form. Ask an admin agent to run inspect_process on 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.

  2. Write procedures.yaml next to the process skill’s SKILL.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: true
    1 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 onClick chain runs.
    6 The switch: its onChange runs after the value is set.
  3. Link it. Give the agent the skill and the run_procedure tool, and update on the procedure’s model in its permissions — create for one that starts a new record.

  4. 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

title

Required. What the approval card asks, and how the agent refers to it.

description

What it does, for the agent.

action

Required. An action-view: it gives the model, the form and the context the form expects.

view

form, the only view type for now. Omit it.

new

true for a procedure that creates a record: it starts from the form’s onNew and takes no id.

when

A guard, as a server expression on the record’s fields.

inputs

type: string, integer, decimal, boolean, date, datetime or reference (with model:, the full name); required: true when it must be given.

steps

What a user does, in order. See Steps.

interactions

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

set: { field: value }

Writes each value, a literal or "eval: <expr>", then runs its field’s onChange.

add: items, add: { items: { qty: 1 } }

A new line: its defaults, its onNew, then the values given, without their onChange.

click: btnName, click: { at: "items[-1]", button: btnName }

The button’s prompt, then its onClick — on the record, or on a line.

run: onLoad, run: { at: "items[-1]", chain: onNew }

A hook, or a chain a field or button declares, run again.

remove: "items[0]"

A line out of its collection; the collection’s onChange runs at once.

commit: "items[-1]"

Leaves a line now: the collection’s onChange. Rarely needed, since leaving a line does it.

save: true

Leaves the open lines, then onSave, saves, reads back, onLoad.

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 showIf or readonlyIf, or a field’s readonlyIf → part of when. So is a static hidden or readonly on a button or field the steps use, and an action-attrs that locks one.

  • An x-bind the steps depend on → an explicit set.

  • A requiredIf → a required input, or a clause in when.

  • An onSelect domain → a when, 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 onChange — and the result lists those records, so that the agent reads them before running again instead of making them twice.

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 set step or an add step’s seed names — items[-1].title, or items[+].title for the line an add seeds — 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 own onChange and onSave write 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 — create for 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_procedure asks the user first; tell the agent not to ask again with ask_to_continue.

  • Not from workflows. A workflow’s <agent> node never gets run_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.