Skip to content

Actions

An action is server code that a screen, a grid, a form, a command, the JSON API or an agent can call. #[action] declares who may call it; the framework checks that before the code runs.

Declaring an action

#[action(permission = "stock.product.read")]
pub async fn search_products(q: DataQuery, store: ProductStore) -> Result<Page<Product>> {
    Ok(q.apply(store.all()?))
}

#[action(permission = "stock.product.write", audit)]
pub async fn save_product(product: Valid<Product>, store: ProductStore) -> Result<Product> {
    store.save(product.into_inner())
}
  • The first argument is the input. It comes from the client, so the framework reads it as untrusted JSON.
  • Every other argument is injected: Ctx or a registered Service.
  • The return type is Result<T>.

Access

Every action declares exactly one of:

OptionMeaning
permission = "feature.resource.verb"The identity needs this permission
publicAnyone, including users who have not signed in

An action without access does not compile. Permission names are lower case and dotted, such as sales.order.write.

Other options:

OptionMeaning
auditWrites an audit record for every call
reauthThe user must have signed in recently (App::reauth_within)

Errors

ErrorStatusUse
Error::NotFound404The record does not exist or the user may not know it exists
Error::Forbidden403Access denied
Error::Denied(t!("..."))403Denied with a reason the user can read
Error::Conflict409The row version changed since the form loaded
Error::Validation(errors)422Field errors, shown next to the fields
Error::Internal("detail")500The detail goes to the server log only

A field error from code:

let mut errors = ValidationErrors::default();
errors.add(Product::code, t!("stock.code_taken", code = product.code));
return Err(errors.into());

Lists

Lists take a DataQuery and return Page<M>. A list action never returns every row; the grid asks for pages, and the server caps the page size whatever the client sends.

The context

Ctx is the request: ctx.identity(), ctx.locale(), ctx.time_zone(), ctx.now(), ctx.today(), ctx.tenant(), ctx.license(), ctx.config(), ctx.theme() and ctx.t(&text). Use ctx.now() and ctx.today() instead of the system clock, so tests can fix the time with App::fixed_now.

Where actions are used

  • DataGrid::of(action) loads rows; .edit_with(action), .row_action(...) and .bulk_action(...) change them.
  • Form::submit(label, action) saves a form.
  • ActionButton::new(label, action, input) runs an action from a button.
  • Command::action(...) runs one from the command palette or a shortcut.
  • Feature::api(action) publishes it as JSON (see API and OpenAPI).
  • Feature::search(action) adds a server search to the command palette.

Every path runs the same checks: CSRF for browser calls, the feature's guards, the action's access, then the interceptors.