---
title: "Bound approvals"
description: "An approval of a governed action is bound to a digest of the exact request. Anlyon recomputes the digest before dispatch and refuses on any difference."
---

An approval of a governed action is not an approval of "a refund" or "an email". It is an approval of one **preview**: the exact normalized request, the resolved target, the action and adapter versions, the preconditions observed at review, and an expiry. Anlyon hashes those together into a `bindingDigest` and stores it on the approval. Immediately before dispatch it recomputes the digest from the persisted effect. Any difference refuses the dispatch.

This applies to every governed action. That means an action with an adapter (`stripe.refund`, `github.file_update`, `resend.email_send`) and an action that carries a declaration (`impact`, `verify` or `governed: true`). The preview is always on. It is not a field you set, and it has no opt-out.

## What a preview shows

A preview is typed data, never provider HTML. Its `blocks` are:

- **fields**: the environment (name and kind), target account, resource, action version, adapter, what success means, when the evidence was observed, and when the preview expires.
- **text_diff**: before and after text, for file changes.
- **impact**: the quantities the operation consumes. `allowances` shows each applicable [limit](/execution/impact-limits) at review time.
- **limitations**: each guarantee labelled for what it is: `guarantee`, `freshness`, `unsupported` or `irreversible`.

```typescript TypeScript
const input = { path: 'data/status.txt', content: 'status: green\n', message: 'Mark green' };
const { data: preview } = await agent.actions.preview('update-fixture', input);

for (const block of preview!.blocks) console.log(block.type, block.title);

// Send the same input. preview.request is the normalized request, not the input.
await agent.actions.invoke('update-fixture', input, { previewId: preview!.id });
```

```python Python
preview = agent.actions.preview(
    "update-fixture",
    {"path": "data/status.txt", "content": "status: green\n", "message": "Mark green"},
).data

agent.actions.invoke("update-fixture", {"path": "data/status.txt", "content": "status: green\n", "message": "Mark green"},
                     preview_id=preview["id"])
```

A governed invocation gets a preview whether or not you create one beforehand. A gated invocation's approval shows it in the dashboard.

### The preview of a declared action

Anlyon has no adapter for the API behind a declared action, so the preview is the request itself. It is built from the action definition and the input. The provider is not read, and nothing is written.

The request block shows the method, the URL, each header, the body and the read-back. Secret references stay as `{{secret:NAME}}` placeholders, so the reviewed request and its digest never hold a credential.


```json
{
  "adapter": { "type": "http.declared" },
  "impact": [{ "dimension": "emails", "amount": 1, "bound": "exact" }],
  "request": { "headers": { "Authorization": "Bearer {{secret:MAILER_TOKEN}}" } }
}
```

## What each kind of action guarantees

All four execute exactly the reviewed request. They differ on stale-state protection, and the preview says so:

| | Exact request | Stale-state protection |
| --- | --- | --- |
| `github.file_update` | Yes | **Atomic.** The write sends the blob SHA shown at review, and GitHub refuses it (`409`) if the file changed since. The effect is `failed` with the stale version recorded as evidence, and the newer content is not overwritten. |
| `stripe.refund` | Yes | **Not atomic.** The adapter sends no compare-and-set on a payment intent. The account is re-checked immediately before dispatch, and the adapter relies on Stripe to refuse a refund above what remains refundable. The refundable amount shown is as of review. The preview labels this `freshness`. |
| `resend.email_send` | Yes | **None.** As of 2026-10-03, Resend has no conditional send. Nothing about the recipients or the domain is checked atomically with the write. |
| A declared action | Yes | **None.** Anlyon does not know the API's preconditions. The provider's state is not checked between review and dispatch. |

## What invalidates an approval

Three different mechanisms can stop a reviewed operation. They are not the same thing, and the receipt says which one acted.

**The request changed.** An invocation that names a `previewId` and sends a different destination, resource, amount or content is refused at admission with `409 PREVIEW_MISMATCH`. No effect is created and nothing is sent.

**Anlyon's own state changed.** Dispatch is refused, with nothing sent, and the receipt grades `refused` when:

- the action was edited after review (`action_changed`)
- the preview expired (`preview_expired`). An approval never outlives its preview. Invoking with an expired `previewId` is refused earlier, with `409 PREVIEW_EXPIRED`.
- the approval itself expired before anyone decided (`approval_expired`)
- a secret the action references is missing, or its host or placement binding no longer allows the request (`credential_unavailable`)
- the environment is halted, the requesting key was revoked, or a policy now denies
- the impact limits no longer have room, even if they did at review
- the stored preview, effect and approval no longer agree on the digest (`binding_mismatch`)

**The provider's state changed.** This is caught by the provider, where the provider has a conditional write. For GitHub, the write carries the reviewed blob SHA and GitHub refuses it. The receipt grades `failed`, not `refused`, because Anlyon did send the request.

Duplicate approval callbacks cannot run the operation twice. Consuming the approval and claiming dispatch are one conditional database write.

**Refreshing** a preview (`refreshPreview`) re-reads the provider into a *new* preview that supersedes the old one. Its binding is different, so an approval of the old preview never carries over.

See [When approval expires or changes](/execution/when-approval-expires-or-changes).

## How it behaves

- **The approval binds the exact request.** For `stripe.refund` the refundable amount shown is as of review, and the adapter re-checks the account before dispatch.
- **A change at GitHub is caught by GitHub.** The write carries the reviewed blob SHA, GitHub refuses a mismatch, and the receipt grades `failed`.
- **The preview of a declared action shows the request.** The provider is not read at review, so the preview describes the request and not the resource it will change.
- **The dispatch-time digest refusal guards stored state.** `binding_mismatch` fires when Anlyon's stored preview, effect and approval disagree. A changed request is refused earlier, at admission, with `PREVIEW_MISMATCH`.
- **An action with no declaration and no adapter is approved on its request snapshot.** See [Approvals and policies](/trust-control/approvals).
- **A credential is checked again at dispatch.** A secret revoked or rebound after review refuses the dispatch.
- **Any workspace member can decide an approval.**
- **A preview expires.** An approval given after the expiry does not run the operation.
