---
title: "Actions (ActionDecl)"
description: "Generated from the doc schema (CatalogDocSchema, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle…"
order: 1
id: "model/action"
---

# Actions (ActionDecl)

Generated from the doc schema (`CatalogDocSchema`, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle decodes. A field’s Example is its value in one of those examples.

## ActionDecl {#actiondecl}

An action (runtime).

The `actions` slot of the doc holds these, keyed by id. In a bundle each is one file, `actions/<name>.json`, named by its `name`, which the file then leaves out.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `id` | [ActionId](shapes.md#actionid) | yes (propose assigns it; a new declaration leaves it out) | — | — | An action id (`ac_…`); renames keep it. |
| `name` | [Ident](shapes.md#ident) | yes | — | `"add_note"` | The action name. |
| `label` | [Text](shapes.md#text) | yes | — | `"Add note"` | The words shown for it. |
| `subject` | [TypeId](shapes.md#typeid) | no | — | `"ty_3f8v6h2n0c"` | The type of the row it runs on; absent = it runs on no row. |
| `inputs` | [InputDecl](action.md#inputdecl)[] | yes | — | (see the example) | The inputs, in field order. |
| `lets` | [LetDecl](action.md#letdecl)[] | no | — | `[{"name":"line_count","value":"count(lines)"}]` | Named values, evaluated in order before the preconditions. |
| `preconditions` | [PreconditionDecl](action.md#preconditiondecl)[] | yes | — | (see the example) | The conditions the call needs. |
| `effects` | [EffectDecl](effect.md#effectdecl)[] | yes | — | (see the example) | What the call does, in order, in one transaction. |
| `outputs` | Record<[Ident](shapes.md#ident), [Expr](shapes.md#expr)> | no | — | `{"lines":"line_count"}` | The CX value of each output the call returns. |
| `approval` | [ApprovalRule](action.md#approvalrule) | no | — | (see the example) | The approval a call needs before it runs. |
| `idempotency` | `"key"` \| `"none"` | no | `"key"` | `"key"` | key: a repeated idempotency key replays the first answer (default); none: no key is accepted. |
| `confirm` | [ActionDecl.confirm](action.md#actiondecl.confirm) | no | — | (see the example) | The call runs only when the caller confirms this message. |
| `writeRights` | `"invoker"` \| `"action"` | no | `"invoker"` | `"invoker"` | Whose rights the writes need: the invoker’s (default) or the action’s own, a declared widening. |
| `limits` | [ActionDecl.limits](action.md#actiondecl.limits) | no | — | `{"maxRowsWritten":100}` | The action’s own limits, under the platform’s. |
| `presentation` | [PresentationOverrides](view.md#presentationoverrides) | no | — | (see the example) | The presentation keys the action overrides (pages). |

`actions/add_note.json`

```json
{
  "label": "Add note",
  "subject": "ty_3f8v6h2n0c",
  "inputs": [
    {
      "name": "note",
      "writes": { "type": "ty_3f8v6h2n0c", "prop": "pr_4p6r8t0v2x" }
    }
  ],
  "preconditions": [],
  "effects": [
    {
      "op": "update",
      "target": "this",
      "set": { "pr_4p6r8t0v2x": "input.note" }
    }
  ],
  "presentation": {
    "inputs": {
      "note": {
        "label": "Note for the kitchen",
        "help": "The kitchen reads it when it packs the order."
      }
    }
  }
}
```

`actions/place_order.json`

```json
{
  "label": "Place order",
  "subject": "ty_3f8v6h2n0c",
  "inputs": [
    {
      "name": "delivery",
      "type": { "t": "date" },
      "label": "Delivery date",
      "required": true
    }
  ],
  "lets": [{ "name": "line_count", "value": "count(lines)" }],
  "preconditions": [
    {
      "id": "has_lines",
      "holds": "line_count > 0",
      "severity": "refuse",
      "message": { "text": "Add a line before you place the order." },
      "offer": true
    }
  ],
  "effects": [
    {
      "op": "transition",
      "target": "this",
      "lifecycle": "lc_2k4m6p8r0t",
      "transition": "tn_4g6j8m0p2r"
    }
  ],
  "outputs": { "lines": "line_count" },
  "approval": {
    "when": "total > 10000",
    "approvers": { "role": "admin" },
    "selfApproval": "forbidden"
  },
  "idempotency": "key",
  "confirm": {
    "message": {
      "text": "Place the order for {total}?",
      "params": { "total": "total" }
    }
  },
  "writeRights": "invoker",
  "limits": { "maxRowsWritten": 100 }
}
```

## ActionDecl.confirm {#actiondecl.confirm}

The call runs only when the caller confirms this message.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `message` | [MsgRef](shapes.md#msgref) | yes | — | (see the example) | What the caller confirms. |

## ActionDecl.limits {#actiondecl.limits}

The action’s own limits, under the platform’s.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `maxRowsWritten` | integer (greater than 0) | no | — | `100` | The most rows one call writes; at most the op ceiling. |

## InputDecl {#inputdecl}

An input of the action.

One of:

- [InputDecl.writes](action.md#inputdecl.writes)
- [InputDecl.type](action.md#inputdecl.type)
- [InputDecl.parts](action.md#inputdecl.parts)
- [InputDecl.ref](action.md#inputdecl.ref)
- [InputDecl.items](action.md#inputdecl.items)

## InputDecl.writes {#inputdecl.writes}

An input that writes a property.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"note"` | The input name. |
| `writes` | [PropRef](shapes.md#propref) | yes | — | `{"type":"ty_3f8v6h2n0c","prop":"pr_4p6r8t0v2x"}` | The property it writes; its type, label and sensitivity come from there. |
| `required` | boolean | no | — | `false` | Whether the caller must give it; absent = as the written property. |
| `default` | [Expr](shapes.md#expr) | no | — | `"''"` | The value when the caller gives none. |
| `showWhen` | [Expr](shapes.md#expr) | no | — | `"input.channel != 'web'"` | The input is shown (and accepted) only while this CX condition holds. |
| `requiredWhen` | [Expr](shapes.md#expr) | no | — | `"input.channel == 'phone'"` | The input is required while this CX condition holds. |
| `options` | [Expr](shapes.md#expr) | no | — | `"active"` | A ref input: the CX condition a target row must hold to be offered. |

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{
  "name": "customer",
  "writes": { "type": "ty_3f8v6h2n0c", "prop": "pr_5n1v3x7c9d" },
  "required": false,
  "showWhen": "input.channel != 'web'",
  "requiredWhen": "input.channel == 'phone'",
  "options": "active"
}
```

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{
  "name": "note",
  "writes": { "type": "ty_3f8v6h2n0c", "prop": "pr_4p6r8t0v2x" },
  "default": "''"
}
```

## InputDecl.type {#inputdecl.type}

An input with its own type.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"delivery"` | The input name. |
| `type` | [ValueType](value-type.md#valuetype) | yes | — | `{"t":"date"}` | What the input holds. |
| `label` | [Text](shapes.md#text) | yes | — | `"Delivery date"` | The words shown for it. |
| `required` | boolean | no | `false` | `true` | Whether the caller must give it; default false. |
| `help` | [Text](shapes.md#text) | no | — | `"Shown to the customer."` | The help text shown with the field. |
| `default` | [Expr](shapes.md#expr) | no | — | `"''"` | The value when the caller gives none. |
| `showWhen` | [Expr](shapes.md#expr) | no | — | `"input.kind == 'other'"` | The input is shown (and accepted) only while this CX condition holds. |
| `requiredWhen` | [Expr](shapes.md#expr) | no | — | `"input.kind == 'other'"` | The input is required while this CX condition holds. |
| `sensitive` | `"personal"` \| `"secret"` | no | — | `"personal"` | Personal input is masked for readers without the right; a secret is never shown (authz C4). |

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{
  "name": "reason",
  "type": { "t": "text", "format": "long" },
  "label": "Reason",
  "help": "Shown to the customer.",
  "required": false,
  "default": "''",
  "showWhen": "input.kind == 'other'",
  "requiredWhen": "input.kind == 'other'",
  "sensitive": "personal"
}
```

## InputDecl.parts {#inputdecl.parts}

The part rows of an owner type.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"lines"` | The input name. |
| `parts` | [TypeId](shapes.md#typeid) | yes | — | `"ty_9d4r7t1x5b"` | The part type: rows owned by the subject (a Lines grid). |
| `max` | integer (greater than 0) | yes | — | `50` | The most part rows one call may send. |

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{ "name": "lines", "parts": "ty_9d4r7t1x5b", "max": 50 }
```

## InputDecl.ref {#inputdecl.ref}

An input that names a row of a type, which the action reads (a selector); CX reads it as a ref<T>.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"bill_to"` | The input name. |
| `ref` | [TypeId](shapes.md#typeid) | yes | — | `"ty_7k2m9q4z1a"` | The type of the row it names: the caller picks one row of this type (a selector the action reads). |
| `label` | [Text](shapes.md#text) | yes | — | `"Bill to"` | The words shown for it. |
| `required` | boolean | no | `false` | `false` | Whether the caller must give it; default false. |
| `help` | [Text](shapes.md#text) | no | — | `"The customer the invoice goes to; the order keeps its own."` | The help text shown with the field. |
| `default` | [Expr](shapes.md#expr) | no | — | `"customer"` | The value when the caller gives none. |
| `showWhen` | [Expr](shapes.md#expr) | no | — | `"input.channel != 'web'"` | The input is shown (and accepted) only while this CX condition holds. |
| `requiredWhen` | [Expr](shapes.md#expr) | no | — | `"input.channel == 'phone'"` | The input is required while this CX condition holds. |
| `options` | [Expr](shapes.md#expr) | no | — | `"active"` | A ref input: the CX condition a target row must hold to be offered. |

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{
  "name": "bill_to",
  "ref": "ty_7k2m9q4z1a",
  "label": "Bill to",
  "help": "The customer the invoice goes to; the order keeps its own.",
  "required": false,
  "default": "customer",
  "showWhen": "input.channel != 'web'",
  "requiredWhen": "input.channel == 'phone'",
  "options": "active"
}
```

## InputDecl.items {#inputdecl.items}

A list of records whose fields are not the properties of one part type; CX reads it as a collection<record> (`input.lines`).

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"returns"` | The input name. |
| `items` | [InputFieldDecl](action.md#inputfielddecl)[] (at least one) | yes | — | (see the example) | The fields of each record, in column order. |
| `label` | [Text](shapes.md#text) | yes | — | `"Returned lines"` | The words shown for it. |
| `required` | boolean | no | `false` | `true` | Whether the caller must give it; default false. |
| `max` | integer (greater than 0) | yes | — | `50` | The most records one call may send. |

An item of `inputs` in an action file (`actions/<name>.json`).

```json
{
  "name": "returns",
  "items": [
    {
      "name": "line",
      "ref": "ty_9d4r7t1x5b",
      "label": "Line",
      "required": true
    },
    {
      "name": "qty",
      "type": { "t": "integer" },
      "label": "Returned",
      "required": true
    }
  ],
  "label": "Returned lines",
  "required": true,
  "max": 50
}
```

## InputFieldDecl {#inputfielddecl}

A field of each record of an input list.

One of:

- [InputFieldDecl.type](action.md#inputfielddecl.type)
- [InputFieldDecl.ref](action.md#inputfielddecl.ref)

## InputFieldDecl.type {#inputfielddecl.type}

A field with its own type.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"qty"` | The field name; CX reads it as `l.<name>` of each record. |
| `type` | [ValueType](value-type.md#valuetype) | yes | — | `{"t":"integer"}` | What the input holds. |
| `label` | [Text](shapes.md#text) | yes | — | `"Returned"` | The words shown for it. |
| `required` | boolean | no | `false` | `true` | Whether the caller must give it; default false. |

An item of `items` in an input list of records (an item of `inputs` in an action file).

```json
{
  "name": "reason",
  "type": { "t": "enum", "values": [{ "code": "damaged", "label": "Damaged" }] },
  "label": "Reason",
  "required": false
}
```

## InputFieldDecl.ref {#inputfielddecl.ref}

A field that names a row of a type.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"line"` | The field name; CX reads it as `l.<name>` of each record. |
| `ref` | [TypeId](shapes.md#typeid) | yes | — | `"ty_9d4r7t1x5b"` | The type of the row it names: the caller picks one row of this type (a selector the action reads). |
| `label` | [Text](shapes.md#text) | yes | — | `"Line"` | The words shown for it. |
| `required` | boolean | no | `false` | `true` | Whether the caller must give it; default false. |

An item of `items` in an input list of records (an item of `inputs` in an action file).

```json
{ "name": "line", "ref": "ty_9d4r7t1x5b", "label": "Line", "required": true }
```

## LetDecl {#letdecl}

A named value of the action.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"line_count"` | The name everything after it reads. |
| `value` | [Expr](shapes.md#expr) | yes | — | `"count(lines)"` | The CX value. |
| `rights` | [Elevated](action.md#elevated) | no | — | (see the example) | A widened read: the value is one aggregate over rows the invoker may not read. |

An item of `lets` in an action file.

```json
{ "name": "line_count", "value": "count(lines)" }
```

## PreconditionDecl {#preconditiondecl}

A precondition of the action.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `id` | [Ident](shapes.md#ident) | yes | — | `"has_lines"` | The precondition name: refusals and on-fail triggers name it. |
| `holds` | [Expr](shapes.md#expr) | yes | — | `"line_count > 0"` | The CX condition that must hold. |
| `severity` | `"refuse"` \| `"warn"` | yes | — | `"refuse"` | refuse stops the call; warn needs the caller to confirm. |
| `message` | [MsgRef](shapes.md#msgref) | yes | — | `{"text":"Add a line before you place the order."}` | What the refusal says. |
| `offer` | boolean | yes | — | `true` | Whether it also decides if the action is offered on a row; then it may not read input. |
| `reveals` | `true` | no | — | `true` | The refusal may carry the values it compared (disclosable ones only). |

An item of `preconditions` in an action file.

```json
{
  "id": "within_limit",
  "holds": "total <= customer.credit_limit",
  "severity": "warn",
  "message": {
    "text": "The total passes the credit limit of {limit}.",
    "params": { "limit": "customer.credit_limit" }
  },
  "offer": false,
  "reveals": true
}
```

## ItemPreconditionDecl {#itempreconditiondecl}

A condition each item of the forEach needs before the body runs for it.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `id` | [Ident](shapes.md#ident) | yes | — | `"positive_amount"` | The precondition name: refusals and on-fail triggers name it. |
| `holds` | [Expr](shapes.md#expr) | yes | — | `"item.amount > 0"` | The CX condition that must hold. |
| `severity` | `"refuse"` | yes | — | `"refuse"` | It stops the call (the only value: it is checked while the effects run). |
| `message` | [MsgRef](shapes.md#msgref) | yes | — | (see the example) | What the refusal says. |
| `reveals` | `true` | no | — | `true` | The refusal may carry the values it compared (disclosable ones only). |

An item of `preconditions` in a forEach effect of an action file.

```json
{
  "id": "positive_amount",
  "holds": "item.amount > 0",
  "severity": "refuse",
  "message": {
    "text": "Each line needs an amount above zero, not {amount}.",
    "params": { "amount": "item.amount" }
  },
  "reveals": true
}
```

## ApprovalRule {#approvalrule}

The approval a call needs before it runs.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `when` | [Expr](shapes.md#expr) | no | — | `"total > 10000"` | The call needs approval only while this CX condition holds; absent = always. |
| `approvers` | [Subject](policy.md#subject) | yes | — | `{"role":"admin"}` | Who may approve: a role, a group or a member. |
| `selfApproval` | `"forbidden"` | yes | — | `"forbidden"` | The requester may never approve their own call (the only value, R14). |
| `expiresAfter` | integer (greater than 0) | no | — | `604800000` | How long a pending request waits, in milliseconds (C18); absent = 30 days. |

The `approval` of an action file.

```json
{
  "when": "total > 10000",
  "approvers": { "role": "admin" },
  "selfApproval": "forbidden",
  "expiresAfter": 604800000
}
```

## Elevated {#elevated}

A widened read: one aggregate over rows the invoker may not read (authz Elevated).

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `discloses` | [Elevated.discloses](action.md#elevated.discloses) | yes | — | `"count"` | The one aggregate the value may be: exists, count, or a sum, min or max of a property. |
| `reason` | [MsgRef](shapes.md#msgref) | yes | — | `{"text":"Shows how many open orders exist."}` | Why the read is widened, shown in policies.effective. |

The `rights` of an action’s let.

```json
{
  "discloses": "count",
  "reason": { "text": "Shows how many open orders exist." }
}
```

The `rights` of an action’s let.

```json
{
  "discloses": { "sum": { "type": "ty_3f8v6h2n0c", "prop": "pr_8b2f4h6j0k" } },
  "reason": { "text": "Shows the customer’s open balance." }
}
```

The `rights` of an action’s let.

```json
{
  "discloses": { "min": { "type": "ty_6c8e0g2j4m", "prop": "pr_3a5c7e9g1j" } },
  "reason": { "text": "Shows when the room is next taken." }
}
```

The `rights` of an action’s let.

```json
{
  "discloses": { "max": { "type": "ty_6c8e0g2j4m", "prop": "pr_9t1v3x5z7b" } },
  "reason": { "text": "Shows when the room is free again." }
}
```

## Elevated.discloses {#elevated.discloses}

The one aggregate the value may be: exists, count, or a sum, min or max of a property.

One of:

- [Elevated.discloses.sum](action.md#elevated.discloses.sum)
- [Elevated.discloses.min](action.md#elevated.discloses.min)
- [Elevated.discloses.max](action.md#elevated.discloses.max)
- `"exists"` | `"count"`

## Elevated.discloses.sum {#elevated.discloses.sum}

The sum of a property.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `sum` | [PropRef](shapes.md#propref) | yes | — | `{"type":"ty_3f8v6h2n0c","prop":"pr_8b2f4h6j0k"}` | The property it sums. |

## Elevated.discloses.min {#elevated.discloses.min}

The least value of a property.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `min` | [PropRef](shapes.md#propref) | yes | — | `{"type":"ty_6c8e0g2j4m","prop":"pr_3a5c7e9g1j"}` | The property it takes the least of. |

## Elevated.discloses.max {#elevated.discloses.max}

The greatest value of a property.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `max` | [PropRef](shapes.md#propref) | yes | — | `{"type":"ty_6c8e0g2j4m","prop":"pr_9t1v3x5z7b"}` | The property it takes the greatest of. |
