User-Defined Actions
Reusable logic written in ARO itself — a feature set that any other feature set can call by name, and get a value back from.
A feature set that anything can call
Give a feature set the business activity Action and it becomes
callable application-wide as Application.<Name>. That is
the whole mechanism. Use it instead of writing a plugin, or hopping through
the event bus, when what you want is reusable logic with a return value.
(DoubleValue: Action takes <number>) {
Extract the <n> from the <input: number>.
Compute the <doubled> from <n> * 2.
Return an <OK: status> with { doubled: <doubled> }.
}
Call it from anywhere, with the same shape a plugin action uses:
(* `takes <number>` is sugar for one positional argument *)
Application.DoubleValue the <d> from 5.
Extract the <doubled> from the <d: doubled>. (* 10 *)
(* The object form always works *)
Application.DoubleValue the <second> with { number: 7 }.
Extract the <second-doubled> from the <second: doubled>. (* 14 *)
Arguments
takes <name> declares a single positional argument, which
arrives as input.<name>. Without it, callers pass an object
literal through with and the action pulls fields off
<input>:
(SumAndDouble: Action) {
Extract the <a> from the <input: a>.
Extract the <b> from the <input: b>.
Compute the <sum> from <a> + <b>.
(* Actions compose — this one calls the one above *)
Application.DoubleValue the <inner> from <sum>.
Extract the <answer> from the <inner: doubled>.
Return an <OK: status> with { value: <answer> }.
}
At the call site the composed action looks like any other:
Application.SumAndDouble the <res> with { a: 3, b: 4 }.
Extract the <value> from the <res: value>. (* 14 *)
What the callee can see
A callee sees its own bindings and application-level published symbols — never the caller's locals. Lookup cost therefore does not grow with call depth. Field values keep their shape across the boundary: return a list and the caller gets a list it can measure and iterate, not a rendering of one.
Recursion
Recursion has no depth limit. What it costs depends on the shape you write.
A call in tail position — the final statement is an unguarded
Return … with <r>. forwarding the call's result untouched —
reuses the frame it is already in, so depth costs nothing at all:
(SumTo: Action) {
Extract the <n> from the <input: n>.
Extract the <acc> from the <input: acc>.
Return an <OK: status> with { total: <acc> } when <n> <= 0.
Compute the <next> from <n> - 1.
Compute the <running> from <acc> + <n>.
(* Tail call: the result is handed straight back, so this frame is reused *)
Application.SumTo the <r> with { n: <next>, acc: <running> }.
Return an <OK: status> with <r>.
}
Any other recursion keeps its frames on the heap, so its depth is bounded by
memory rather than by a stack. Factorial is the standard example
— the frame still has to multiply the callee's result, so each level stays
live until the one below it returns:
(Factorial: Action takes <n>) {
Extract the <v> from the <input: n>.
Return an <OK: status> with { value: 1 } when <v> <= 1.
Compute the <next> from <v> - 1.
Application.Factorial the <inner> from <next>.
Extract the <sub> from the <inner: value>.
Compute the <value> from <v> * <sub>.
Return an <OK: status> with { value: <value> }.
}
Guard rails
| What | Behaviour |
|---|---|
aro check | Warns when every path through an action reaches a call before a Return — direct or mutual cycles alike. |
ARO_MAX_CALL_DEPTH | A runaway recursion stops at 50 000 live frames with an error naming the call chain, instead of an OOM kill. 0 disables the check. |
| Unknown action | Calling an Application.<Name> that no feature set defines is a check-time error, not a runtime surprise. |
Learn More
The full specification is
ARO-0081:
User-Defined Actions. Examples/UserDefinedActions and
Examples/RecursiveActions are runnable. For actions written
in Swift, Rust, C or Python instead, see
Writing Extensions.