# Operations

Source: https://permdock.com/docs/concepts/operations

defineOperations binds each action an application exposes to one permission leaf, with a data loader, a results permission and risk meta. Agent tools, automations, chat commands and approval prompts read the same frozen table by name.

`defineOperations(permissions, definitions)` names the actions an application exposes and binds each to one permission. Agent tools, automations, chat commands and approval prompts read the same table instead of each keeping its own.

```ts
import { defineOperations } from "permdock";

export const operations = defineOperations(permissions, {
  sendInvoice: {
    permission: permissions.billing.invoice.submit,
    data: (input: { id: string }) => db.invoice.find(input.id),
    meta: {
      risk: "high",
      effects: ["send", "external"],
      title: "Send invoice",
    },
  },
  listInvoices: {
    permission: permissions.billing.invoice.read,
    results: permissions.billing.invoice.read,
  },
});
```

It returns a frozen `OperationMap`. Each entry is a frozen `Operation` `{ name, permission, data?, results?, meta }`:

| Field | Meaning |
| --- | --- |
| `permission` | The leaf a caller must hold to run the operation |
| `data` | Loads the row the check runs on from the input; `OperationInput<typeof operations.sendInvoice>` is its parameter type |
| `results` | The leaf each result row is filtered by |
| `meta` | `{ risk?, effects?, title? }`; `{}` when absent |

`risk` is one of `low`, `medium`, `high`, `critical`, exported in that order as `OPERATION_RISKS` with the guard `isOperationRisk`. `effects` lists distinct values of `read`, `write`, `delete`, `send`, `spend`, `external`. A host reads `meta` to label, confirm or gate an operation; PermDock never decides on it.

`defineOperations` throws at definition time for:

* a `permission` or `results` outside `permissions` (matched by key);
* a `data` that is not a function;
* meta outside those lists;
* a name that is not 1 to 128 of `A-Z a-z 0-9 _ . -`, or that repeats another name case-insensitively.

## Where operations are read [#where-operations-are-read]

| Reader | What it takes from an operation |
| --- | --- |
| [Agents](/docs/concepts/agents) | `decideOperation(name, input, ctx)` loads the row through `data` and decides `permission`; `filterResults` applies `results` |
| [Automations](/docs/concepts/automations) | An automation lists the operations it runs; a run is narrowed to their permissions |
| [Chat](/docs/concepts/chat) | `protect(operation, handler)` decides a chat command as its author |
| [Delivery](/docs/concepts/delivery) | `approvalPrompt(request, { operation })` puts `meta.title` and `meta.risk` in the approval email or card |
| [Catalog](/docs/concepts/wire-formats#catalog) | `permdock collect` writes `{ name, permission, results?, meta }` with permission keys when `permdock.config.ts` names an `operations` module |

With `operations: "./src/operations.ts"` in `permdock.config.ts`, `permdock collect` writes the map to the catalog as `operations`. [Doctor](/docs/cli/doctor) PD067 reports an operation whose `permission` or `results` key the configured permissions module does not declare.

## Why [#why]

* **One table per application.** The same action reached from REST, MCP, an AI tool, an automation and a chat command otherwise gets a lookup table per entry point, and they drift apart.
* **An operation names a leaf.** Operations are not a second permission vocabulary. Grants, snapshots, RLS and the catalog stay keyed by permission, and an operation only says which leaf an action needs and how to load its row.
* **MCP tool-name characters.** An operation name becomes a tool name unchanged, so it uses the characters MCP 2025-11-25 recommends for tool names. Names that differ only in case are refused because chat commands and some tool hosts compare names case-insensitively.
* **Closed meta lists.** A host can only gate on `risk` and `effects` if every operation uses the same words, so an unknown value throws instead of passing through.
