# Result shapes

## Invoke response (REST and SDKs)

`POST /api/v2/actions/{ref}/invoke` with `{ input, … }` and an `Idempotency-Key` header (the SDKs always send one). HTTP `200` when the call ran inline (including refusals and `unknown`), `202` when it is parked for approval.

```json
{
  "success": true,
  "data": {
    "id": "inv_…",
    "actionName": "send-email",
    "status": "succeeded",
    "pendingApproval": false,
    "approvalId": null,
    "responseStatus": 200,
    "body": "…redacted, truncated provider response…",
    "error": null,
    "effectId": "eff_…",
    "grade": "confirmed",
    "endUserId": null,
    "decision": null,
    "retryOf": null
  }
}
```

- `status`: `pending_approval`, `running`, `succeeded`, `failed`, `unknown`, `denied`, `expired`. Treat any status you do not recognise as not successful.
- `grade` (governed actions): `confirmed`, `acknowledged`, `unknown`, `failed`, `refused`, `denied`, `pending`.
- `decision` (when a policy, a person or expiry decided): `{ source, code, effect, explanation, policy }`. Codes include `POLICY_DENIED`, `POLICY_APPROVED`, `HUMAN_APPROVED`, `HUMAN_DENIED`, `APPROVAL_EXPIRED`, `END_USER_ERASED`.
- A refusal at dispatch: `status: "failed"`, `grade: "refused"`, `error: "Dispatch refused (impact_limit_exceeded): …"`.
- `body` is a string: parse it yourself if the provider returns JSON.

Python: `client.actions.invoke(name, input, idempotency_key=…).data` is a dict with the same keys. `client.approvals.wait_for_decision(approval_id, timeout_seconds=300)`.

## Reading later

| Read | SDK | REST |
| --- | --- | --- |
| One invocation | `actions.invocation(id)` | `GET /api/v2/actions/invocations/{id}` |
| A receipt | `actions.effect(effectId)` | `GET /api/v2/actions/effects/{id}` |
| An approval | `approvals.get(id)` | `GET /api/v2/approvals/{id}` (add `?waitSeconds=` to long-poll) |
| Calls in `unknown` | `actions.invocations({ status: 'unknown' })` | `GET /api/v2/actions/invocations?status=unknown` |

The receipt (`EffectDetail`) carries `stage`, `outcome`, `partialEffect`, `grade`, `verification`, `providerReference` (for example a commit SHA) and the end user.

## @anlyonhq/ai-sdk tool result

```json
{
  "status": "pending_approval",
  "message": "…a sentence for the model…",
  "invocationId": "inv_…",
  "approvalId": "apr_…",
  "effectId": null,
  "grade": null,
  "endUserId": "user_42",
  "requestId": "…",
  "code": null,
  "limits": null,
  "body": null,
  "bodyTruncated": false
}
```

- `code`: `impact_limit_exceeded`, `policy_denied`, `approval_denied`, `approval_expired`, or null.
- `limits`: for `impact_limit_exceeded`, `[{ name, requested, available }]`.
- `body` is cut at 8,000 characters (`maxBodyChars` changes it).
- Connect prompt: `{ status: "connect_required" | "reconnect_required", integration, displayName, connectUrl, endUserId, message }`. `connectUrl` is null when the environment has no default redirect URL.
- `anlyon_check_request({ invocationId })` returns the same fields plus `action` and `approvalStatus`.
- A bad key or missing scope still throws.

## MCP tool results

- A gated action returns text naming the `apr_…` id and telling the agent to call `wait_for_approval`.
- `wait_for_approval({ approvalId, timeoutSeconds? })` returns the approval; `status` stays `pending` when nobody decided within the wait.
- End-user connect prompts are returned as a normal result (not `isError`), with the same fields in `structuredContent`.
- Governed results carry `grade`; `unknown` is reported as unknown, with an instruction not to retry.
