---
title: "Receipts and grades"
description: "Every call Anlyon made on your agent's behalf, what the destination replied, which action version ran, and the grade that says how far the outcome was verified."
---

Because Anlyon makes the production call, it can say what happened to it. An **invocation** is that record: one intended operation, its outcome, and everything needed to attribute it.

A [governed action](/execution/actions) adds an **effect** to the invocation, and every effect carries a **grade**. The grade says how far the outcome was verified. An action with no declaration and no adapter creates no effect and returns no grade. Its `succeeded` status records an HTTP result, not independent verification of the business outcome.

## The seven grades

| Grade | Meaning |
| --- | --- |
| `confirmed` | The outcome is `succeeded`, and a read-back matched or an adapter read the provider's own record of the operation. |
| `acknowledged` | The provider accepted the request with a `2xx`. No read-back has matched. An operator's manual `succeeded` also grades here. |
| `unknown` | No usable response. Anlyon does not send it again. It stays unknown until a read-back or an operator settles it. The allowance stays held. |
| `failed` | The provider rejected the request, or the request provably never left Anlyon. |
| `refused` | Anlyon refused at dispatch. Causes include an exhausted or unbounded limit, an expired preview or approval, a changed action version and a digest mismatch. |
| `denied` | The approval was denied. |
| `pending` | No receipt yet. The effect is waiting for approval, is being dispatched, or the provider accepted it and has not finished it. |

`confirmed`, `acknowledged`, `unknown`, `failed`, `refused` and `denied` are the six receipt grades. `pending` exists so the API returns a grade on every effect, including one that has not run.

### How the grade relates to `stage`, `outcome` and `verification`

An effect has a **stage** (where it is in Anlyon's pipeline), an **outcome** (what happened in the world) and a **verification** (who established the outcome). The grade is derived from them by one rule in the database. No code path writes it.

The rule is read top to bottom, and the earliest match wins:

| Order | Condition | Grade | `verification` |
| --- | --- | --- | --- |
| 1 | `stage` is `rejected` and the rejection reason is `approval_denied` | `denied` | `not_dispatched` |
| 2 | `stage` is `rejected` for any other reason | `refused` | `not_dispatched` |
| 3 | `outcome` is `succeeded`, established by the provider, and a read-back or the provider's own record verified it | `confirmed` | `provider` |
| 4 | `outcome` is `succeeded` in any other case | `acknowledged` | `provider` or `manual` |
| 5 | `outcome` is `failed` | `failed` | `provider`, `manual`, or `not_dispatched` when the request never left Anlyon |
| 6 | `outcome` is `unknown` | `unknown` | `unverified` |
| 7 | Anything else: `outcome` is `not_dispatched` or `pending` | `pending` | `not_dispatched` or `unverified` |

A `rejected` effect always has `outcome: not_dispatched`. Nothing was sent.

Two rows deserve a second look. An `acknowledged` effect can show `verification: provider`. The provider's `2xx` established the outcome, and nothing read it back. And an approval that expired before anyone decided grades `refused`, not `denied`. Its rejection reason is `approval_expired`.

### Reading the grade

The invoke response and a single invocation read carry the `grade` of the effect the invocation created. It is absent on list reads and when the invocation has no effect. List effects by grade to find the receipts that need attention.

```typescript TypeScript
const { data } = await anlyon.actions.invoke('send-email', { messageId: 'm-1', subject: 'Welcome', to: ['a@example.com'] });
console.log(data!.status, data!.grade);

const { data: unread } = await ops.actions.effects({ grade: 'acknowledged' });
const { data: open } = await ops.actions.effects({ grade: 'unknown' });
```

```python Python
result = anlyon.actions.invoke("send-email", {"messageId": "m-1", "subject": "Welcome", "to": ["a@example.com"]})
print(result.data["status"], result.data["grade"])

unread = ops.actions.effects(grade="acknowledged").data
open_effects = ops.actions.effects(grade="unknown").data
```

A declared action whose read-back matched:


```json
{
  "stage": "settled",
  "outcome": "succeeded",
  "grade": "confirmed",
  "verification": "provider",
  "adapter": { "type": "http.declared", "version": "1" }
}
```

The invocation's `status` and the effect's `grade` answer different questions. An effect refused on an exhausted limit has invocation `status: failed` and `grade: refused`. A lost response has `status: unknown` and `grade: unknown`. See [Governed effects](/execution/governed-effects) for evidence, reconciliation and resolution of an effect, and [When the outcome is unknown](/execution/when-the-outcome-is-unknown).

The rest of this page describes the invocation record, which every action has.

## Reading an invocation

```typescript TypeScript
const { data } = await anlyon.actions.invocation('inv_01JABCDEF');

console.log(data!.status);          // succeeded | failed | unknown | ...
console.log(data!.responseStatus);  // what the destination replied, or null
console.log(data!.durationMs);
console.log(data!.approvalId);      // set when the call was gated
```

```python Python
result = anlyon.actions.invocation("inv_01JABCDEF")

print(result.data["status"])
print(result.data["responseStatus"])
```

List them with `actions.invocations()`, or narrow to one action with `actions.invocationsFor(name)`. The dashboard shows the same records under **Actions → Invocations**.

## The statuses are not interchangeable

This is the part worth reading carefully, because two of these look similar and mean opposite things.

| Status | What it means | What to do |
| --- | --- | --- |
| `pending_approval` | Parked. A human has not decided yet. | Wait, or poll the approval. |
| `running` | Dispatching now. | Wait. |
| `succeeded` | The destination replied with a `2xx`. If the response body could not be read after the status arrived, the invocation is still `succeeded` and `body` says the body was unavailable. | Read `responseStatus` and `body`. |
| `failed` | **The call did not happen.** It was refused before dispatch, failed before anything was sent, or the destination refused it with a `4xx` (a `402`, for example, is `failed`). | Safe to retry once you have fixed the cause. |
| `unknown` | **The request may have reached the destination and Anlyon cannot confirm the outcome.** A timeout, a socket that died after the request was written, or a `5xx` from the destination: a server error does not say the change was undone, so Anlyon treats a `5xx` as indeterminate. `responseStatus` is kept when one arrived. | Reconcile with the destination before doing anything else. Do not retry blind. |
| `denied` | A human declined. | Nothing ran. |
| `expired` | Nobody decided in time. | Nothing ran. |

`failed` and `unknown` exist as separate statuses because the safe response to each is different. Splitting them is the whole reason an operator reading `failed` can retry, and the reason an operator reading `unknown` should go and look at Stripe first.

An `unknown` invocation carries a message saying exactly that: the destination may have received this request, Anlyon cannot confirm the outcome, reconcile with the provider before retrying.

## Resolving an unknown outcome

Once you have checked the destination, record what happened. Nothing is sent to the destination:

```typescript TypeScript
const { data: queue } = await operator.actions.invocations({ status: 'unknown' });

await operator.actions.resolveInvocation('inv_01JABCDEF', {
  outcome: 'failed',
  evidence: 'Stripe shows no charge for order 4417.',
  externalReference: 'order_4417',
});
```

```python Python
queue = operator.actions.invocations(status="unknown")

operator.actions.resolve_invocation(
    "inv_01JABCDEF",
    outcome="failed",
    evidence="Stripe shows no charge for order 4417.",
    external_reference="order_4417",
)
```

- **Who can resolve.** A workspace admin in the console, or a key holding `actions:resolve`, which is never granted by default. The key that made the call can never resolve it, so an agent cannot declare its own uncertain call failed and try again.
- **Evidence is required** and kept on the invocation with who resolved it and when. The invocation shows it as `resolution`.
- **Billing follows the answer.** Resolved `succeeded` is billed. Resolved `failed` releases the reserved usage and refunds the key's budget reservation.
- **One resolution wins.** An invocation that is no longer `unknown` returns `409`, including when two operators resolve it at the same moment.

## Retrying on purpose

Resolving does not free the original's idempotency key. Running the action again is a new decision, made visible:

```typescript
await operator.actions.invoke('refund-order', input, {
  retryOf: 'inv_01JABCDEF',
  idempotencyKey: 'refund:4417:retry-1',
});
```

A retry needs its own idempotency key and names the invocation it retries. It is allowed only when the original is `failed` (on its own or resolved as failed) and of the same action. An `unknown` original must be resolved first, and a `succeeded` one cannot be retried this way. Each invocation can be retried once: if the retry fails, retry the retry. The link shows on both invocations as `retryOf` and `retriedBy`.

## What is recorded, and what is scrubbed

| Recorded | Not recorded |
| --- | --- |
| Action name and the action **version** that executed | Any resolved secret value |
| Response status, truncated response body, duration | The interpolated URL or headers |
| The approval id and the decision with its policy name and version, when gated. Who decided is on the approval | |
| The run and span the invocation belongs to | |

Log lines carry the action's **template**, never the interpolated request. Anything the upstream sends back is scrubbed for secret values before it is persisted or logged, and the response body is truncated, so a chatty API that echoes your request cannot smuggle a credential into your own audit trail.

## Attribution

Pass a `runId` and the invocation joins that run's trace alongside the model calls and approval waits that led to it. That is what lets you answer the question that actually gets asked after an incident: which agent issued this refund, on which version of the action, and who approved it.

Runs and spans are OpenTelemetry-compatible and exportable, so the record outlives any one vendor, including this one. They are never metered on any plan.

See [Analytics](/advanced/analytics) for aggregate reporting, and [Approvals and policies](/trust-control/approvals) for the decision half of the record.

**What the record is**

These are durable records of every decision, with export and retention you configure.

## How it behaves

- **A receipt is a database record with provider evidence.**
- **`confirmed` on a declared action means your own verify rule matched.** A resource that already existed can match, so write a rule that distinguishes this write.
- **Confirmed is Resend's record of the send, not inbox delivery.**
- **`acknowledged` means the provider answered `2xx` and nothing read the result back.** A declared action with no `verify` rule grades `acknowledged`.
- **A manual `succeeded` grades `acknowledged`.** It keeps `verification: manual` and is never shown as provider verification.
- **A lost response that lookup cannot find stays `unknown`.** The effect keeps its allowance until evidence or an operator settles it.
- **A verify URL that reads the provider response needs that response.** After a lost response that effect stays `unknown` until an operator resolves it.
- **A request is sent once.** When the outcome cannot be confirmed, the invocation is recorded `unknown` and you reconcile before retrying.
- **A grade belongs to a governed action.** For an action with no declaration and no adapter, `succeeded` records the destination's `2xx`.

## Where to go next

**[Idempotency and retries](/execution/idempotency)**

Retrying safely after a `failed`, and what to do about an `unknown`.

**[Approvals and policies](/trust-control/approvals)**

Who decided, under which policy.

**[Immutable versions and rollback](/trust-control/promotion)**

Pinning the definition an invocation runs.

**[Analytics](/advanced/analytics)**

Aggregate reliability and usage.
