# [Action] — what an app offers to be done, by name

> Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a caller needs derived from the function itself: its label, its parameters and their kinds (an enum's members are the choices), the record it acts on, what it reads and writes, and — as JSON Schemas — what it takes and what it answers. Only a marked function can be run this way, and it always runs under someone's ordinary security: the caller's, or the person whose consent it carries. What it returns comes back to whoever ran it, as JSON of its own type.

<!-- id: function-action · area: function · stability: preview · html: https://osysharp.com/reference/function/action/ -->

## Summary        {#summary}
`[Action]` on a top-level function says "this can be done to a record, by name". Everything else is derived:

```osy syntax
/// Mark how soon an order must go.
[Action("Set the rush")]
public string SetRush(Order o, [Label("How soon")] Rush rush) { … }

Actions.Catalog()                                   // every action the app offers, described
Actions.Run(Actions.Of(SetRush).Name, [new ActionValue { Parameter = "o", Value = id }, …])
```

Kits that do things on an app's behalf — the Automations kit's rules, the ReplyComposer kit's macros — are built on this one
surface, so an app says once what can be done, and every one of them offers exactly that.

## Signature      {#signature}
```osy syntax
[Action]                    // label derived from the name: SetRush → "Set rush"
[Action("Set the rush")]    // or said
[Action(Query)]             // it only READS — proved by the compiler; with a label: [Action("Find orders", Query)]
[Sends]  [Spends]           // on ANY function: it sends something to somebody · it spends money or an allowance
public <ReturnType> Name(Record subject,                       // the first parameter: the record it acts on (optional)
                         [Label("How soon")] Rush rush,          // a parameter's label (derived when not said)
                         [Choices(ShelfChoices)] Shelf shelf)    // a record (or a string) chosen from what F offers
public List<ActionChoice> ShelfChoices();                       // F: takes nothing, answers { Key, Label } rows

Actions.Catalog()                         → List<ActionInfo>     // Name, Label, Description, Subject, Parameters, Effects,
                                                                 //   Query, ParametersSchema, ResultSchema
Actions.For(record)                       → List<ActionInfo>     // the actions on this record's type (or a base) the caller may run
Actions.Find(string name)                 → ActionInfo?
Actions.Of(F)                             → ActionInfo           // the marked function F, checked where it is written
Actions.Choices(string action, string parameter) → List<ActionChoice>   // run as the caller
Actions.Check(string action, List<ActionValue> values) → string   // "" when they would run, else why not — nothing runs
Actions.Run(string action, List<ActionValue> values, string cause = "")  → ActionResult   // as the caller
Actions.Consent(string action, List<ActionValue> values)       → string // the caller's sealed consent
Actions.RunConsented(string consent, List<ActionValue> values, string cause = "") → ActionResult   // as its author
Actions.ValuesFromJson(string action, string json) → List<ActionValue> // one JSON object of arguments, as the values Run takes
Actions.Cause                             → string               // inside an action: the cause it was run with
Actions.NameOf(Func<T, R> step)           → string               // a step written as the action CALLED: which action
Actions.ValuesOf(Func<T, R> step)         → List<ActionValue>    // …and what it gives each other parameter, as text

public Spec Run<R>([ActionStep] Func<T, R> step)                 // a kit's parameter that takes a STEP — checked where
public Spec Run([ActionStep] Action<T> step)                     //   every caller writes one
```

`ActionResult` is `{ Outcome, Said, Value, Problem, RanAs }` — `Outcome` one of `Done`, `Invalid`, `Denied`, `Gone`,
`Failed`; `Value` what the function returned, as JSON ([below](#value)); `Said` the same when it is a string — the
sentence a caller shows.

## Description    {#description}

### What is derived, and from what   {#derived}
- **The label** — `[Action("…")]`, else the function's name in words. **The description** — its `///` comment.
- **The subject** — the first parameter when it is an entity: the record the action is done TO. A caller passes its id.
- **Each other parameter** — its name, `[Label]` (else derived), whether it is optional (a default value), and its
  KIND: text, number, decimal, yes/no, date, date and time, time of day, duration, an id, an enum (its members are the
  choices, in order, by their `[Label]`), a record — which must say where its choices come from with `[Choices(F)]`,
  since "any row of that table" is not a choice a person can make — or a **structure** (a class, a
  `Dictionary<string, T>`) or a **sequence** (a list, an array, a set), given as JSON ([below](#structured)). A string
  may take `[Choices]` too.
- **Its schemas** — each parameter's JSON Schema (`ActionParameter.Schema`), all of them as one object
  (`ActionInfo.ParametersSchema`), and what the action answers (`ActionInfo.ResultSchema`). They are what
  [Json.SchemaOf<T>() — the JSON Schema of a type](https://osysharp.com/reference/json/schema-of/) answers for those types, so nothing describes an action that the action could disagree with.
- **The effects** — what the function reads, creates, changes and deletes, whether it reaches outside the app
  (`External`), and whether it sends something to somebody (`Sends`) or spends (`Spends`) — from the compiler's own
  analysis of its body and everything it calls (`ActionInfo.Effects`). A caller can say "this rule changes tickets and
  sends mail" without anyone writing it down.

### A class or a list as a parameter   {#structured}
An action may take a class, a list, an array, a set or a `Dictionary<string, T>` — the shapes a tool call or another
system sends. Its value is **JSON**, read exactly as `JsonSerializer.Deserialize<T>` reads that type (so a member is
named by its `[ExternalName]` where it has one, and an enum by its member's name), and its
`ActionParameter.Schema` is `Json.SchemaOf<T>()` (with a row inside it described as its id — below). A value that is not what the schema says is `Invalid`, with the
reason in `Problem`, and nothing runs.

A **row** is named by its id wherever it stands — as a parameter of its own, as a member of a class, as an element
of a list (`List<Ticket> tickets`, `class Move { public Ticket Ticket; public Queue To; }`). The schema describes it
as that id (`"type": "string", "format": "uuid"`), and each row is looked up, as whoever the action runs as, when the
action runs. An id that names no row its caller may read is `Gone` — "No Ticket with id … : it does not exist, or
whoever this runs as cannot read it" — and the action does not run at all. Anything that is not an id is `Invalid`.

### What it answers   {#value}
`ActionResult.Value` is what the action returned, as JSON — what `JsonSerializer.Serialize` writes for it, with every
enum by its name — so the caller holds the value itself:

```osy syntax
var result = Actions.Run("Price", values);
if (result.Outcome == ActionOutcome.Done) {
  var quote = JsonSerializer.Deserialize<Quote>(result.Value);   // the Quote the action built
}
```

A number is `25`, a string `"It is done."`, an enum `"High"`, a list `[…]`, a class `{…}`, a row its id and its own
values. It is `""` when the action answers nothing (`void`) or did not run. `ActionInfo.ResultSchema` describes it
before anything is run.

### From one JSON object of arguments   {#from-json}
Something that holds an action's arguments as ONE JSON object — a model's tool call, written to
`ParametersSchema` — turns it into the values a run takes with `Actions.ValuesFromJson`:

```osy syntax
var result = Actions.Run(name, Actions.ValuesFromJson(name, argumentsJson));
```

Each member becomes its parameter's value: text for a single value (`"High"`, `12`, `true`, a row's id), JSON for a
structure or a sequence. A `null` member is a value left out. It only reads the catalogue; the run is what checks the
values, and refuses a member that names no parameter. The same values go to `Actions.Consent`, so a call a person
approves is sealed exactly as it was proposed.

### Checking values before they are kept   {#check}
An editor that sets up a step to run LATER — an automation's step, a macro's — asks `Actions.Check` before it keeps
the values, and gets `""` or the first problem in a person's words. It reads every value exactly as `Actions.Run`
will — a date, a number, a duration, the JSON of a structure or a list — and says a required one left out. Nothing
runs and no row is read: the record the action is done to is whatever runs it, so it is never asked for, and a row
named by id is checked to be an id. The Ui kit's `ActionArgumentField(p, choices, value)` is the input each
`ActionParameterKind` takes, for the same editors.

```osy title="a step's values read before they are kept" test app=function-action-check
entity Visit {
  DateOnly? On;
  int Hours;
}

/// Promise a visit on a day, for so many hours.
[Action("Promise a visit")]
public void Promise(Visit v, DateOnly on, int hours = 1) {
  v.On = on;
  v.Hours = hours;
  UnitOfWork.Commit();
}

/// What an editor says before it keeps the step — "" when it would run. For "next tuesday":
/// "On is a date, written 2026-09-04 — “next tuesday” is not."
public string BeforeKeeping(string day) {
  var name = Actions.Of(Promise).Name;
  return Actions.Check(name, [new ActionValue { Parameter = "on", Value = day }]);
}
```

It is a server read: a page calls it through a function of its own, as `BeforeKeeping` is here.

### An action that only reads   {#query}
`[Action(Query)]` declares that running the action changes nothing — it finds something out. The compiler **proves**
it, over everything the action calls, and refuses one that does more:

```text
`[Action(Query)]` on 'Peek': a query only reads, and this one creates Sighting. A query may read any entity and
compute from what it read. It may not create, change or delete a row, raise an event, start a workflow, or reach
outside the platform (an outbound call, `[Sends]`, `[Spends]`) — itself or through anything it calls. Remove `Query`,
or move that work out of it.
```

`ActionInfo.Query` is `true` for one. Whatever runs actions on somebody's behalf can run a query without asking —
it is how a search or a lookup is offered to an agent — and must ask, or be allowed, for everything else.

### Saying what it sends and spends   {#sends-spends}
The compiler sees that a function makes an outbound call; it cannot tell a lookup from a mail or a charge. The function
that knows says so, once:

```osy syntax
[Sends]  void MailReceipt(Order o) { … }        // it sends something to somebody
[Spends] Receipt Charge(Order o) { … }          // it spends money or a metered allowance
```

Every action that reaches one — however many calls away — reports `Effects.Sends` or `Effects.Spends`, and
`Effects.External` with it. The attributes go on any function, most usefully the one a kit wraps its provider in, so
an app's actions inherit them without saying anything. A declaration only ever **adds** an effect: there is no way to
declare an action safer than the compiler found it.

### What may be done to this record?   {#for-a-record}
`Actions.For(record)` is every action whose SUBJECT is the record's own type or a type it derives from — from the app
and every package, so a kit's action on a hub's record (`Close(WorkItem w)`) is offered on a ticket — that the caller may
run: an action marked `[Authorize(P)]` is offered only to a caller `P` admits. It is what a record's action bar and a
command menu list; running one is `Actions.Run(a.Name, …)` with the record as its subject. On the server, like the rest.

```osy title="what a job offers its reader" test app=function-action-for
[Principal] entity User { [Required, MaxLength(200)] string Email; security { allow read when IsAuthenticated; } }
[Role] enum AppRole { Member, Boss }
[RoleGrant] entity RoleGrant { [Required] User Grantee; [Required] AppRole Level; security { allow read when IsAuthenticated; } }
policy IsBoss => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Boss);

entity Work {
  [MaxLength(100)] string Title = "";
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}
entity Job : Work { }

[Action("Close")] void CloseWork(Work w) { w.Title = w.Title + " (closed)"; }
[Action("Start")] void StartJob(Job j) { j.Title = j.Title + " (started)"; }
[Action("Archive")] [Authorize(IsBoss)] void ArchiveJob(Job j) { j.Title = "archived"; }

/// "Close, Start" for anybody; a boss is offered Archive too.
string Offered(Guid id) {
  var j = Job.Single(x => x.Id == id);
  return string.Join(", ", Actions.For(j).Select(a => a.Label).OrderBy(l => l));
}
```

### Only a marked function runs by name   {#only-marked}
`Actions.Run("Wipe", …)` on a function that is not `[Action]` answers `Gone`, exactly as for a name that does not
exist — the two are not told apart. A class method, a test, an `[AuthMethod]` and a client function cannot be marked.

Being an action does **not** put a verb in your REST API. To offer one to callers outside the app, list it in
`app.Apis` like any function, `new Endpoint(CloseJob) { Method = HttpMethod.Post, Path = "/jobs/{id}/close" }`, and
the path's `{id}` binds its record. See [[api-rest#action-endpoints]].

### What is the caller told when the action throws?   {#refusals}
What the exception's **type** says. A refusal you raise on purpose reaches the caller with your words; anything else
is a failure, and its message — which may be a driver's or a stack's — stays in the app's log.

| the action throws | `Outcome` | `Problem` |
|---|---|---|
| `ValidationException`, `ConflictException`, or a class deriving from one | `Invalid` | your message |
| `NotFoundException`, or a class deriving from it | `Gone` | your message |
| `NotAuthorized`, or a class deriving from it; a security rule's refusal | `Denied` | the message |
| `Exception`, or a class deriving **directly** from it; a runtime fault | `Failed` | *It failed for a reason the app did not foresee — the app's log has the details.* |

So a refusal of your own that a caller should hear — a person, a rule, a model running the action as a tool — derives
from the type that says what kind of refusal it is ([[function-throw#declare-your-own]]):

```osy title="a refusal of your own that is told, and one that is not" run app=function-action-refusals
/// Told: it is a validation refusal with a name of its own.
public class LimitRefused : ValidationException { }
/// Not told: nothing says what kind of failure this is.
public class Jammed : Exception { }

/// Books a crane for a lift of this many tonnes.
[Action]
string BookCrane(int tonnes) {
  if (tonnes > 40) { throw new LimitRefused("No crane here lifts more than 40 tonnes."); }
  if (tonnes == 13) { throw new Jammed("winch driver: register 0x3F stuck"); }
  return "Booked.";
}

string Book(int tonnes) {
  var result = Actions.Run("BookCrane", [new ActionValue { Parameter = "tonnes", Value = tonnes.ToString() }]);
  return result.Outcome.ToString() + ": " + (result.Outcome == ActionOutcome.Done ? result.Said : result.Problem);
}

[Test]
void a_refusal_deriving_from_a_told_type_reaches_the_caller_in_its_own_words() {
  Assert.Equal("Done: Booked.", Book(10));
  Assert.Equal("Invalid: No crane here lifts more than 40 tonnes.", Book(50));
}

[Test]
void an_exception_that_says_nothing_about_its_kind_is_a_failure_and_its_words_stay_in_the_log() {
  var told = Book(13);
  Assert.True(told.StartsWith("Failed: "));
  Assert.False(told.Contains("0x3F"));
  // …while code that catches it by its own type still reads everything.
  var caught = "";
  try { BookCrane(13); } catch (Jammed e) { caught = e.Message; }
  Assert.Equal("winch driver: register 0x3F stuck", caught);
}
```

`osy lint` says so before a caller finds out: `correctness-action-throws-a-refusal-its-caller-is-not-told` reports a
class deriving straight from `Exception` that an `[Action]` — or a function a REST `Endpoint` publishes — can throw,
in its own body or in anything it calls, however deep: a function, a class method, and the code of a package you use.
It names the class and the function that throws it, at the line of the action that leads there. `Jammed` in the
example is reported; `LimitRefused` is not, and neither is a throw a `catch` on the way takes. For a class of your
own, derive it from one of the four types above; for a package's, which is not yours to change, catch it where you
call the package and throw what your caller should hear — `catch (WinchJammed) { throw new ConflictException("That
crane is jammed."); }`. Where a failure is meant to stay unsaid,
`[SuppressWarning("correctness-action-throws-a-refusal-its-caller-is-not-told")]` on the function records it.

### It runs under someone's security, always   {#security}
`Actions.Run` runs the action as the CALLER, under the caller's own rules: a person who could not set the rush by hand
cannot set it through `Actions.Run` either — the result is `Denied`, with the refusal in `Problem`. Code with no person
behind it (a workflow body, a timer) runs it as the app.

To run an action later as somebody else — a rule a person saved, running when something happens at 3 a.m. — the person
**consents** at the time they choose it: `Actions.Consent(action, values)` seals the action, the values they chose and
who they are, under the app's key. `Actions.RunConsented(consent, rest)` runs it as them, under their rules as they are
THEN: a person who has since lost the role, or whose account is switched off, gets `Denied`. A consent cannot be edited,
names one action, and can only be made by the person it names.

### A step written as the action called   {#step}
A kit that takes a STEP from the app — an automation rule's "then" — takes it as the action called on its record,
`t => TagTicket(t, "urgent")`, rather than a name and a list of strings. The call is compiled where the app writes it,
so a misspelt parameter (*'TagTicket' has no parameter 'tga'. Did you mean 'tag'?*) or a value of the wrong type is an
error there. `Actions.NameOf(step)` answers which action it is and `Actions.ValuesOf(step)` what it gives each other
parameter, in the text an `ActionValue` stores — exactly what `Actions.Run` takes. The values are fixed when the step is
written, so the step's SHAPE matters too: it calls an `[Action]`, on the step's own record, and no value reads the record.

Mark the kit's parameter `[ActionStep]` and the compiler checks that shape at every call, where the step is written:

```text
a step is the action called on the rule's record — `t => TagTicket(t, "urgent")`, and 'JobWords' is not an action — mark it `[Action]` to offer it.
a step is the action called on the rule's record — `t => TagTicket(t, "urgent")`: `label` reads the record, and a rule's values are fixed when it is written. Ask the record in a condition, or let the action read it itself.
```

`Actions.NameOf`/`ValuesOf` refuse the same, saying which, for a step that reaches them some other way (a function value
handed on, with no `[ActionStep]` in between).

### Saying what set it off   {#cause}
`cause:` is any string the caller wants an action — and everything it sets off — to carry. Inside the action,
`Actions.Cause` reads it. The Automations kit uses it to know a change was made by one of its own rules, and so to stop
a rule that would run again on what it just did.

## Examples       {#examples}

### A bakery's two actions, and one that is not offered   {#example-bakery}
```osy title="two actions, a choice from data, and a caller" test app=function-action
[Role] enum BakeryRole { Authenticator, Clerk, Boss }
[RoleGrant] entity RoleGrant { Baker Grantee; [Required] BakeryRole Level; security { allow read when IsAuthenticated; } }
policy IsClerk => RoleGrant.Any(g => g.Grantee == user && (g.Level == BakeryRole.Clerk || g.Level == BakeryRole.Boss));
policy IsBoss => RoleGrant.Any(g => g.Grantee == user && g.Level == BakeryRole.Boss);

[Principal] entity Baker {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

public enum Rush { [Label("Whenever")] Low, [Label("Today")] High }

entity Order {
  [Required, MaxLength(80)] string Item;
  Rush Rush = Rush.Low;
  [MaxLength(40)] string Shelf = "";
  security { allow read, create when IsClerk; allow update when IsBoss; }
}

entity Shelf {
  [Required, MaxLength(40)] string Name;
  security { allow read when IsClerk; }
}

/// Mark how soon an order must go.
[Action("Set the rush")]
public string SetRush(Order o, [Label("How soon")] Rush rush) {
  o.Rush = rush;
  UnitOfWork.Commit();
  return "Rush set to " + rush.Label + ".";
}

/// Put it where it is collected from.
[Action]
public void PutOnShelf(Order o, [Choices(ShelfChoices)] Shelf shelf) {
  o.Shelf = shelf.Name;
  UnitOfWork.Commit();
}

public List<ActionChoice> ShelfChoices() {
  return Shelf.OrderBy(s => s.Name).ToList().Select(s => new ActionChoice { Key = s.Id.ToString(), Label = s.Name }).ToList();
}

/// Not offered — and so never run by name, whoever asks.
public void Wipe(Order o) { o.Delete(); UnitOfWork.Commit(); }

/// What the bakery offers, as a person reads it: "Set the rush (How soon)".
public List<string> Offered() {
  return Actions.Catalog().Select(a => a.Label + " (" + string.Join(", ", a.Parameters.Where(p => !p.Subject).Select(p => p.Label)) + ")").ToList();
}

/// Rush an order, as whoever asks — refused, by sentence, for somebody who may not.
public string RushIt(Guid order) {
  var result = Actions.Run(Actions.Of(SetRush).Name, [
    new ActionValue { Parameter = "o", Value = order.ToString() },
    new ActionValue { Parameter = "rush", Value = Rush.High.Name },
  ]);
  return result.Outcome == ActionOutcome.Done ? result.Said : result.Problem;
}
```

### A lookup and a priced answer, run from a tool call   {#example-typed}
```osy title="a query, a class in and a class out, and what it spends" test app=function-action-typed
public enum Rush { Low, High }

entity Part {
  [Required, MaxLength(80)] string Name;
  decimal Price;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

public class Wanted { public string Name = ""; public int Quantity; }

/// What an order would cost.
public class Quote {
  public decimal Total;
  public Rush Rush;
  public List<string> Missing = [];
}

/// The parts whose name contains a word. Only reads — so it may be run without asking.
[Action("Find parts", Query)]
public List<Part> FindParts(string word) {
  return Part.Where(p => p.Name.Contains(word)).OrderBy(p => p.Name).ToList();
}

/// Price a list of parts. A list of a class goes in; a class comes out.
[Action]
public Quote Price(List<Wanted> wanted, Rush rush) {
  var quote = new Quote { Rush = rush };
  foreach (var w in wanted) {
    var found = Part.Where(p => p.Name == w.Name).ToList();
    if (found.Count == 0) { quote.Missing.Add(w.Name); }
    else { quote.Total = quote.Total + found[0].Price * w.Quantity; }
  }
  return quote;
}

/// Whatever takes the money declares it; every action that reaches it reports `Effects.Spends`.
[Spends] void Charge(decimal amount) { }

[Action("Order the parts")]
public string OrderParts(List<Wanted> wanted) {
  var quote = Price(wanted, Rush.Low);
  Charge(quote.Total);
  return "Ordered for " + quote.Total + ".";
}

/// Run an action from one JSON object of arguments, as a tool call holds them, and hand back what it answered:
/// `Call("Price", "{\"wanted\":[{\"Name\":\"bolt\",\"Quantity\":3}],\"rush\":\"High\"}")`.
public string Call(string action, string argumentsJson) {
  var result = Actions.Run(action, Actions.ValuesFromJson(action, argumentsJson));
  return result.Outcome == ActionOutcome.Done ? result.Value : result.Problem;
}

/// The same, typed: the caller holds the Quote the action built.
public decimal TotalFor(string argumentsJson) {
  var result = Actions.Run("Price", Actions.ValuesFromJson("Price", argumentsJson));
  return JsonSerializer.Deserialize<Quote>(result.Value).Total;
}

/// What a model is shown for each action: its name, what it is for, the schema of its arguments — and whether it may
/// be run without asking.
public List<string> Tools() {
  return Actions.Catalog().Select(a =>
    a.Name + (a.Query ? " (reads only)" : a.Effects.Spends ? " (spends)" : "") + ": " + a.Description + " " + a.ParametersSchema).ToList();
}
```

## See also       {#see-also}
- [Json.SchemaOf<T>() — the JSON Schema of a type](https://osysharp.com/reference/json/schema-of/) — the schemas an action's parameters and answer are published as
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — reading `ActionResult.Value` as the type the action returned
- [The Automations kit — rules that do an app's routine work when something happens](https://osysharp.com/reference/ui/automations-kit/) — rules that run an app's actions when something happens, as the person who saved them
- [The ReplyComposer kit — where a person writes a reply, on any record](https://osysharp.com/reference/ui/composer-kit/) — macros whose steps are an app's actions
- [function authorization ([Authorize] on a function)](https://osysharp.com/reference/function/authorize/) — the gate for a function that touches no data
- [security { }](https://osysharp.com/reference/security/entity-security/) — the rules an action runs under
