---
name: anlyon-governed-actions
description: Defines Anlyon actions an AI agent can invoke by name, and governs them - vaulted credentials bound to hosts, declared impact counted against impact limits, read-backs that grade receipts confirmed, approval policies, and per-user limits. Use when writing or reviewing an actions.create / actions.declare call, an action's urlTemplate, headers, bodyTemplate or inputSchema, a {{secret:NAME}} or {{connection:x}} placeholder, an approval policy, an impact limit, an OpenAPI import, or a pack install for Anlyon.
license: MIT
metadata:
  author: Anlyon
  version: "1.0.0"
  docs: https://docs.anlyon.com/skills/anlyon-governed-actions
---

# Defining governed actions

An action is one HTTPS request Anlyon makes for the agent. A **governed** action also declares what it changes: each call then creates an effect that is reserved against impact limits, bound to its approval, and graded on its receipt.

## Rules

- Define actions, secrets, policies and limits with an **operator key** (`ANLYON_OPERATOR_KEY`) in a setup script or deploy job, never from the agent's process or the key the agent runs with.
- Credentials enter from environment variables or the console, never as literals. Never print or log a secret value. No API returns one.
- Govern every write: give it `impact` (and `verify` where the provider can show the change). A plain action gets no limit, no bound approval and no grade.
- Do not loosen a gate to make a call pass. Changing `requiresApproval`, or changing or removing a governed action's declarations, needs `actions:govern`; ask the user.

## Pick a route

| The API is… | Do this |
| --- | --- |
| An integration Anlyon offers (GitHub, Stripe, Notion, Slack…) | The user connects the account and turns on its operations in the console (**Integrations**). Each operation becomes a governed action. Nothing to write. |
| Described by an OpenAPI document | `actions.importFromOpenApi(spec, …)`: one action per operation, writes governed. See [references/openapi-and-packs.md](references/openapi-and-packs.md). |
| Anything else over HTTPS | `actions.create` below. |

## Define one

```typescript
import { Client } from '@anlyonhq/sdk';
const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });

await ops.secrets.put('SENDER_KEY', {
  value: process.env.SENDER_KEY!,
  allowedHosts: ['api.example.com'], // the credential can never be sent anywhere else
  allowedPlacements: ['header'],
});

await ops.actions.create({
  name: 'send-email',                                     // ^[a-zA-Z0-9_-]+$, max 100
  description: 'Send one email to up to 10 recipients.',  // the model and the approver read this
  method: 'POST',
  urlTemplate: 'https://api.example.com/v1/messages',
  headers: { Authorization: 'Bearer {{secret:SENDER_KEY}}' },
  bodyTemplate: { to: '{{input.to}}', subject: '{{input.subject}}', text: '{{input.text}}' },
  inputSchema: {
    type: 'object',
    properties: {
      to: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 10 },
      subject: { type: 'string', maxLength: 200 },
      text: { type: 'string' },
    },
    required: ['to', 'subject', 'text'],
    additionalProperties: false,
  },
  impact: { dimension: 'emails', amount: 'count(input.to)', bound: 'exact' },
  verify: {
    url: 'https://api.example.com/v1/messages/{{response.id}}',
    status: 200,
    match: [{ path: 'subject', equals: '{{input.subject}}' }],
  },
});
```

Python takes the same fields in snake_case (`url_template`, `input_schema`, `requires_approval`, `allowed_hosts`).

Key points (full field rules in [references/action-fields.md](references/action-fields.md)):
- **Placeholders:** `{{input.field}}`, `{{input.field:path}}` (multi-segment path), `{{secret:NAME}}`, and `{{connection:<integration>}}` (the end user's account when the call names one, otherwise the workspace's).
- **Governed rules:** `https`, host fixed in `urlTemplate` (never from input), `verify.url` on the same host.
- **`impact`:** `dimension` is `money` (with `unit` a currency, amounts in minor units), `resource_mutations`, or a unit you name (`emails`, `rows`). `amount` is a string: an integer literal (`'1'`), `'input.<path>'` or `'count(input.<path>)'`, optionally `* <integer>`. `bound` is `exact` or `upper_bound`. An amount read from an optional input the caller left out is unbounded, and any applicable limit refuses it (`impact_unbounded`): make that input required.
- **`verify`:** a `GET` read after the write. A match grades `confirmed`; without one a `2xx` grades `acknowledged`. Match a value this request set, so a pre-existing resource cannot match.
- **No read-back possible?** Use `governed: true` instead, and say so in the description.
- To add declarations to an existing action: `ops.actions.declare('send-email', { impact, verify })`. `null` removes one.

## Gate it with an approval

- **Always ask a person:** `requiresApproval: true` on the action.
- **Ask sometimes, or route to someone:** an approval policy (needs `policies:write`).

```typescript
await ops.approvalPolicies.create({
  name: 'Large sends need a person',
  matchKind: 'action',
  matchActionName: 'send-email',
  condition: 'size(input.to) > 3',  // CEL over input, agent.id, user.id, tags, action.name
  effect: 'require_approval',       // or auto_approve, auto_deny
  decidedBy: 'team',                // 'end_user': the user the call was made for decides, in your app
});
```

Policies run in ascending `priority`; the earliest match wins. An `auto_approve` clears an action's own `requiresApproval` gate when it names that action, and not otherwise. On approval Anlyon sends the exact request snapshot that was reviewed. Details: [references/policies-and-limits.md](references/policies-and-limits.md).

## Cap it with an impact limit

```typescript
await ops.impactLimits.create({ name: 'Emails per day', dimension: 'emails', period: 'day', amount: 500 });
await ops.impactLimits.create({ name: 'Refunds per day', dimension: 'money', currency: 'usd', period: 'day', amount: 50_000 });
await ops.impactLimits.create({ name: 'Emails per user', dimension: 'emails', period: 'day', amount: 20, actionName: 'send-email', scope: 'end_user' });
```

Needs `impact-limits:write`. Every key and session draws from the same limit. A call that does not fit is refused whole before anything is sent (`status: failed`, `grade: refused`, reason `impact_limit_exceeded`).

## Check your work

1. `ops.actions.get('send-email')` shows `governed: true` and the declarations.
2. Invoke once with the agent key in a staging environment; expect a `grade` of `confirmed` (with `verify`) or `acknowledged`.
3. `ops.impactLimits.list()` shows `consumed` / `available` moving.
4. Deleting an action archives it; the name stays taken (`409 CONFLICT` on re-create). A new action name needs its own gate or policy.
