# Anlyon > Shared limits and receipts for AI agent actions. Your agent names an action. Anlyon holds the credential, makes the call, and returns a receipt with a grade. # Shared limits and receipts for AI agent actions Source: https://docs.anlyon.com/ Anlyon enforces one shared allowance, approvals bound to the exact request, and provider-verified outcomes on the actions AI agents take through it, so adding agents or sessions never adds a way around the limit. Your agent names an action and passes input. Anlyon holds the credential, makes the call, and returns a receipt with a grade. **[First governed action in ten minutes](/quickstart)** Let an agent update a file on GitHub without holding your token. End with a receipt graded confirmed. **[Govern any HTTP API](/guides/any-http-api)** Declare what an action changes, and put it under a limit. ## What you get - **A shared limit.** Every agent, session and key in the environment draws from the same limit, reserved before dispatch. A new session or key does not get a new allowance. See [Impact limits](/execution/impact-limits). - **A bound approval.** The human approves the exact change, and that exact change is what runs. See [Bound approvals](/execution/previews). - **A receipt with a grade.** Confirmed means a read-back matched. Acknowledged means the provider accepted the request and nothing read it back. Unknown means no usable response. Anlyon does not send it again. It stays unknown until a read-back or an operator settles it. Failed, refused and denied mean it did not run. See [Receipts and grades](/execution/outcomes). ## Works on any API Put any HTTP API your agent calls under a shared limit, an exact approval and a graded receipt. Declare it as an action over HTTPS. Stripe refunds, GitHub file updates and Resend email sends come with read-back built in, and a verify rule adds it to anything else. | Action | What it covers | | --- | --- | | Stripe refunds | A refund of an explicit amount on a payment intent, counted in the action's currency. | | GitHub file updates | One existing text file, on a branch you name that is not the default branch. | | Resend email sends | A limit counted in emails, one per recipient. Confirmed is Resend's record of the send, not inbox delivery. | | Any HTTPS API | A [declared action](/guides/any-http-api). You state what one call changes and how to read the result back. | ## How it behaves - Anlyon governs the calls routed through an action. Keep the provider credential in the vault so the action is the path to the provider. - A limit and a grade apply to a governed action. A governed action has an adapter or a declaration. - A limit belongs to one environment. Each environment has its own. - A declared action grades `confirmed` when its read-back rule matches. With no read-back rule it grades `acknowledged`. - A receipt is a database record with provider evidence. ## Concepts **[Actions](/execution/actions)** Define a call once, then let your agent invoke it by name. **[Impact limits](/execution/impact-limits)** One allowance for every agent, session and key in an environment. **[Bound approvals](/execution/previews)** An approval tied to the exact request, target and version. **[Receipts and grades](/execution/outcomes)** What ran, what the provider recorded, and how sure the record is. **[Governed effects](/execution/governed-effects)** The record behind every governed action, from preview to outcome. **[Idempotency and retries](/execution/idempotency)** Replay an invocation without repeating a side effect. **[Secrets](/execution/secrets)** Store a credential outside the model and bind it to a host. **[API keys](/authentication)** Create environment-scoped keys and grant only the scopes a caller needs. ## When something goes wrong **[When the limit is hit](/execution/when-the-limit-is-hit)** What a refusal looks like, and what to do next. **[When approval expires or changes](/execution/when-approval-expires-or-changes)** What happens to a request after its review goes stale. **[When the outcome is unknown](/execution/when-the-outcome-is-unknown)** How an unknown receipt settles, and what you must not retry. ## Build and operate - Use the [TypeScript SDK](/guides/typescript-sdk), the [Python SDK](/guides/python-sdk) or the [Anlyon CLI](/cli). - Connect a coding agent with the [agent setup prompt](/agent-setup/prompt), or see [how agents can read these docs](/agent-setup/read-docs). - Separate staging from production with [Environments](/trust-control/environments), and route sensitive calls to a person with [Approvals](/trust-control/approvals). - Find every operation in the [API reference](/api-reference). --- # Billing & Pricing Source: https://docs.anlyon.com/account/billing The **current** terms are always the ones the API reports: `GET /api/v2/public/pricing-policy` (unauthenticated), the [pricing page](https://anlyon.com/pricing), and **Settings → Plan & Billing** in the console. They are read from the API's release setting, so they match what the API enforces. This page explains Early Beta Access, the terms in force while `mode` is `early_beta`. While Early Beta Access is active, Anlyon is free within hard limits, with no credit card, no subscription, no automatic upgrade, and no overage charges. **Paid plans arrive after the beta.** See the [beta policy](/beta) for the full terms. Nothing on this page converts, charges, or resets a workspace when the terms change. ## Early Beta Access Every workspace starts on the Free plan with these allowances: | Resource | Early Beta Access | | --- | --- | | Action executions | 10,000 / month | | Action definitions | 10 | | Seats, including pending invitations | 3 | | Environments | 3, of any kind | | Combined storage | 1 GiB | | Egress | 50 GiB / month | | Trace retention | 14 days | | Email alerts | 500 / month | | Anlyon Vigil reviews (opt-in) | 1,000 / month | | Anlyon Vigil approvals (opt-in) | 100 / day | Halt, isolation, and approvals are included. So are environment protection, controlled promotion between environments, and the email notification channel. A new workspace starts with one protected environment of kind `production`, and you can create `staging` and `development` kinds up to the total. There are no separate charges for traces, spans, or approval decisions. Other rate, retry, payload, and resource limits are unchanged from the standard Free plan: 30-second delivery timeout, 1 MiB payload, up to three retries, and 300 requests per minute per workspace, counted in one-minute windows. ### Resets versus persistent limits - **Reset monthly** at 00:00 UTC on the first of each month: action executions, egress and email alerts. - **Do not reset**: combined storage, seats (including pending invitations), environments, and action definitions. These are limits on what you hold. They free up when you remove things. ### When you reach a limit Requests stop at a hard limit and return the machine-readable error `QUOTA_EXCEEDED`. Nothing is charged, and nothing is upgraded automatically. | Limit | What to do | | --- | --- | | A monthly allowance (executions, egress) | Wait for the reset (the response and **Settings → Plan & Billing** show the date), or contact support. | | Storage or a resource cap (data, seats, environments, actions) | Remove unused items to free room. | | Anlyon Vigil reviews or approvals | Nothing to do: approvals still reach people, without a suggestion. See [Anlyon Vigil](/trust-control/vigil). | | Email alerts | Wait for the monthly reset. Alerts keep reaching the in-app inbox and the other channels you connected. | There is no guaranteed response time and no guaranteed limit increase. Inspecting and deleting your own data always works. ### Verified accounts and pauses Costly operations (memory processing, hosted action execution, and file or cache writes) require a verified workspace owner email. If we ever have to protect the service, new expensive work can be **paused** platform-wide and returns `EXPENSIVE_WORK_PAUSED` (HTTP 503). Signing in, viewing, and deleting your data keep working while paused. ## Usage and alerts Use the console's **Settings → Plan & Billing** and **Settings → Usage** pages, or: - `GET /api/v2/billing/plan`: current limits, subscription status, and `access`: the workspace's mode (`early_beta` or `standard`), whether paid plans can be bought (`paidPlansAvailable`), the next UTC reset (`usageResetsAt`), and `overageBilling: false` / `automaticCharges: false` during beta. - `GET /api/v2/billing/plans`: available plans, allowances, and prices. Paid plans report `purchasable: false` until paid plans open. - `GET /api/v2/billing/quota`: execution, message, storage, and egress allowances. - `GET /api/v2/billing/usage/current`: current usage. - `GET /api/v2/billing/budget`: workspace overage alert configuration. - `GET /api/v2/public/pricing-policy`: unauthenticated. The platform's current mode and published allowances, used by the public pricing page. The quota response adds `allowances` with `includedUnits`, `usedUnits`, `overageUnits`, and `projectedOverageCents`. During Early Beta Access every dimension is a hard cap, so `overageUnits` and `projectedOverageCents` are zero. Per-key budgets and safety controls are separate admission controls. They are not invoices, and they do not cap the money an approved action moves, model tokens, or all downstream delivery work. ## Planned paid plans The plans below are **planned** and arrive after the beta. The prices are not guaranteed and may change before paid plans open. This reference is kept so you can see what is intended. | | Free | Production | Growth | Enterprise | | --- | --- | --- | --- | --- | | Monthly fee | $0 | $49 (planned) | $199 (planned) | Custom | | Action executions | 5,000 | 500,000 | 2.5 million | Custom | | Durable messages | 10,000 | 1 million | 5 million | Custom | | Seats, including pending invitations | 2 | 5 | 20 | Custom | | Action definitions | 5 | Unlimited | Unlimited | Unlimited | | Environments, of any kind | 1 | 2 | 3 | Unlimited | | Protected production and controlled promotion | No | Yes | Yes | Yes | | Trace retention | 14 days | 90 days | 365 days | Custom | | Trace archive | No | Yes | Yes | Custom | | Email alerts | No | Yes | Yes | Yes | | Anlyon Vigil reviews per month | 250 | 25,000 | 150,000 | Custom | | Anlyon Vigil approvals per day | 25 | 1,000 | 5,000 | Custom | The Free column is the standard Free plan that applies once Early Beta Access ends. During Early Beta Access a workspace has the allowances at the top of this page, with environment protection, controlled promotion and email alerts included. Planned overage, for paid plans only: | Dimension | Production / Growth allowance | Overage | | --- | --- | --- | | Action executions | 500K / 2.5M per month | $10 per 100K | | Durable messages | 1M / 5M per month | $0.50 per 200K | | Stored memory, files, and trace archives | 1 GiB combined | $0.05 per additional GiB per month | | Egress | 50 GiB per month | $0.05 per additional GiB | A confirmed outbound response counts as an execution, including a non-2xx destination response. Halt-blocked requests, validation failures, denied or expired approvals, and other never-dispatched attempts do not count. Uncertain timeouts stay reserved but uncounted until reconciled. A replay does not add another execution. Anlyon Vigil reviews and approvals have no overage on any plan. They are hard caps: at the limit, approvals still reach people, without a suggestion. ### Change your plan Until paid plans open, the console shows no checkout, and the API refuses the request **before any payment system is contacted**: ```json { "success": false, "error": { "code": "PAID_PLANS_UNAVAILABLE", "message": "Paid plans arrive after the beta. Nothing was charged. Your workspace keeps its Early Beta allowances." } } ``` `POST /api/v2/billing/upgrade` (owner session) and `POST /api/v2/billing/create-setup-intent` return this with HTTP 403 for any workspace without an existing subscription. Going back to Free is never blocked. When paid plans open, choosing Production or Growth in the console collects payment details with Stripe Elements. A workspace receives paid access only after the subscription becomes active, and adding a card alone never upgrades a workspace. `POST /api/v2/billing/upgrade` takes `{ "plan": "production", "paymentMethodId": "pm_example" }` and may return a `clientSecret` requiring payment confirmation. Do not treat an incomplete subscription as paid access. Enterprise is contact-sales only. The earlier `paygo` identifier remains readable but cannot be purchased. Existing subscriptions are unaffected: invoices (`GET /api/v2/billing/invoices`), card updates, and cancellation (`POST /api/v2/billing/cancel`, effective at the billing-period boundary) stay available. See the [beta policy](/beta) for how paid plans will be introduced, and the [error-handling guide](/advanced/error-handling) for quota responses. ## Allowances for earlier namespaces The namespaces that predate governed actions keep their own allowances during Early Beta Access. They apply to existing workspaces that still use them. | Resource | Early Beta Access | | --- | --- | | Durable messages | 25,000 / month | | Memory storage | 50 MiB, within the 1 GiB combined storage | | Stored memories | 2,000 | | Internal memory-processing allowance | $0.25 / month | Durable messages and the memory-processing allowance reset at 00:00 UTC on the first of each month. Memory storage and stored memories do not reset. They free up when you remove memories. When memory processing reaches its allowance, wait for the reset. Existing data is kept and stays readable. ### Memory processing There is no public embedding-token or LLM-token charge, and no token meter. Memory and semantic-cache processing run against an **internal provider-cost allowance** ($0.25 per month during Early Beta Access). Larger inputs and model-assisted processing use more of it, so it is not a promise of unlimited operations. When it is reached, processing stops until the next UTC month. Stored memories are not deleted. Pending file uploads reserve storage capacity until confirmed or cleaned up. --- # Notifications Source: https://docs.anlyon.com/account/notifications Anlyon sends alerts about approvals waiting for a decision, usage against your allowances, billing and API keys. ## Channels - **In-app.** Shown in the console. - **Email.** Sent to the recipients you list. Early Beta Access includes it, up to 500 emails per workspace per UTC month. The standard Free plan that follows the beta sends no email alerts. - **Webhook.** A POST to an endpoint you configure. - **Slack and Discord.** Sent to a channel once you connect one. Configure channels and recipients in the console under **Settings → Notifications**. ## Notification types | Type | Sent when | | --- | --- | | `approval_requested` | An approval is waiting for a decision | | `quota_warning` | Usage of a monthly allowance crossed one of your alert thresholds | | `quota_exceeded` | A monthly allowance with a hard limit reached 100% | | `budget_alert` | Projected overage reached your alert target. The alert does not stop usage | | `payment_failed` | A payment did not go through | | `plan_upgraded`, `plan_downgraded` | The workspace plan changed | | `feature_enabled` | A feature was turned on for the workspace | | `api_key_expiring` | An API key is close to its expiry date | | `security_alert` | A security event needs attention | | `system_alert` | A system event needs attention | | `delivery_failure_spike` | Message deliveries are failing at an unusual rate | Usage alerts cover action executions and delivery messages. Each threshold alerts at most once per month for each of the two. When usage crosses several thresholds at once, only the highest one alerts. ## Managing notifications Listing and counting need `notifications:read`. Marking read and dismissing need `notifications:write`. Both scopes are in the default set. Read state belongs to the workspace, so a dismissed notification is gone for everyone. ```bash # List. Add ?unreadOnly=true or ?limit=50 (1 to 100). curl https://api.anlyon.com/api/v2/notifications \ -H "Authorization: Bearer $ANLYON_API_KEY" # Count the unread ones curl https://api.anlyon.com/api/v2/notifications/unread-count \ -H "Authorization: Bearer $ANLYON_API_KEY" # Mark one as read curl -X PATCH https://api.anlyon.com/api/v2/notifications/$NOTIFICATION_ID/read \ -H "Authorization: Bearer $ANLYON_API_KEY" # Mark all as read curl -X PATCH https://api.anlyon.com/api/v2/notifications/read-all \ -H "Authorization: Bearer $ANLYON_API_KEY" # Delete one curl -X DELETE https://api.anlyon.com/api/v2/notifications/$NOTIFICATION_ID \ -H "Authorization: Bearer $ANLYON_API_KEY" ``` ## Your own preferences Every member has their own preferences in each workspace. They hold the notification types you muted. A muted type is left out of your own list and unread count. It is still raised, still sent to email, Slack, Discord and the webhook, and still shown to every other member. Reading them needs `notifications:read`. Changing them needs `notifications:write`. No role is needed. Both calls need a user, so they work from the console and from an OAuth connection. An API key has no user and gets `403` with the code `USER_IDENTITY_REQUIRED`. ```bash # Read curl https://api.anlyon.com/api/v2/notifications/preferences/me \ -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN" # Replace the muted types. Send [] to mute nothing. curl -X PATCH https://api.anlyon.com/api/v2/notifications/preferences/me \ -H "Authorization: Bearer $OAUTH_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mutedTypes": ["quota_warning"] }' ``` ```json { "success": true, "data": { "mutedTypes": ["quota_warning"], "updatedAt": "2026-10-03T12:00:00.000Z" } } ``` `mutedTypes` takes up to 12 notification types. In the console this is **Settings > Notifications > My notifications**. ## Workspace preferences These decide where the workspace's alerts are sent and which types are raised, for every member. | What you do | Scope | Role, for a console session or an OAuth connection | | --- | --- | --- | | Read workspace preferences | `notifications:read` | Any member | | Update workspace preferences | `notifications:manage` | Admin or owner | | Send the test webhook | `notifications:manage` | Admin or owner | `notifications:manage` is a [privileged scope](/authentication#privileged-scopes). No key gets it by default. An OAuth connection gets it from the Notification channels group, which is off unless you turn it on. In the console, a member sees these settings and an admin changes them. OAuth tokens issued before 2026-10-03 do not carry `notifications:write` or `notifications:manage`. An existing OAuth client must re-consent before it can mark notifications read, dismiss them, or change notification settings. API keys created before that date that changed notification settings with `notifications:read` need the new scope. ### Read ```bash curl https://api.anlyon.com/api/v2/notifications/preferences \ -H "Authorization: Bearer $ANLYON_API_KEY" ``` ```json { "success": true, "data": { "emailEnabled": true, "webhookEnabled": false, "inAppEnabled": true, "slackEnabled": false, "discordEnabled": false, "emailRecipients": ["owner@example.com"], "webhookUrl": null, "hasWebhookSecret": false, "quotaAlertThresholds": [50, 75, 90, 100], "enabledTypes": ["approval_requested", "quota_warning", "quota_exceeded"] } } ``` The webhook secret is never returned. `hasWebhookSecret` says whether one is set. ### Update Send the fields you want to change. ```bash curl -X PATCH https://api.anlyon.com/api/v2/notifications/preferences \ -H "Authorization: Bearer $ANLYON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "emailEnabled": true, "emailRecipients": ["oncall@example.com"], "quotaAlertThresholds": [60, 80, 95, 100] }' ``` | Field | Meaning | | --- | --- | | `emailEnabled`, `webhookEnabled`, `inAppEnabled`, `slackEnabled`, `discordEnabled` | Turn a channel on or off | | `emailRecipients` | Up to 50 email addresses | | `webhookUrl`, `webhookSecret` | The webhook endpoint and the secret that signs its payloads. `null` clears either one | | `quotaAlertThresholds` | Up to 20 whole percentages from 0 to 100. The default is 50, 75, 90 and 100 | | `enabledTypes` | The notification types to send | ## Webhook notifications ### Configure the endpoint ```bash curl -X PATCH https://api.anlyon.com/api/v2/notifications/preferences \ -H "Authorization: Bearer $ANLYON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookEnabled": true, "webhookUrl": "https://your-app.com/webhooks/anlyon", "webhookSecret": "a-long-random-string" }' ``` Send a test payload to an endpoint before you save it: ```bash curl -X POST https://api.anlyon.com/api/v2/notifications/preferences/test-webhook \ -H "Authorization: Bearer $ANLYON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://your-app.com/webhooks/anlyon", "webhookSecret": "a-long-random-string" }' ``` ### Payload ```json { "event": "notification.quota_warning", "timestamp": "2026-10-01T15:00:00.000Z", "workspaceId": "2f8a1f72-7cf9-4fef-95e8-df9de5f2f5d0", "data": { "notificationId": "...", "type": "quota_warning", "priority": "medium", "title": "Action executions: 75% of monthly allowance used", "message": "7,500 of 10,000 used. ...", "metadata": { "dimension": "actionInvokes", "month": "2026-10", "threshold": 75, "percent": 75 } } } ``` The request carries `X-Anlyon-Event` and `X-Anlyon-Timestamp` headers. ### Signature When a webhook secret is set, the request also carries `X-Anlyon-Signature: sha256=`. The value is the HMAC-SHA256 of the raw request body, keyed with your webhook secret. With no secret set, the request is unsigned. This is a different format from the `Anlyon-Signature` header on signed deliveries. The SDK's `Receiver` class verifies `Anlyon-Signature` and does not verify this header. Verify it yourself: ```typescript import { createHmac, timingSafeEqual } from 'node:crypto'; function verifyNotification(rawBody: string, header: string | undefined, secret: string): boolean { if (!header) return false; const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`; const a = Buffer.from(expected); const b = Buffer.from(header); return a.length === b.length && timingSafeEqual(a, b); } ``` The signature covers the body and nothing else. It carries no timestamp and no environment, so it does not bound replay age. ## How it behaves - **An alert reports.** A `budget_alert` reports projected overage, and usage continues. - **A notification webhook is one POST.** Make your handler safe to call more than once, and do not rely on its arrival for anything that must happen. - **The notification signature is separate from the delivery signature.** It covers the body and names no environment. See [Webhook signature verification](/verification). ## Next steps **[Billing & Pricing](/account/billing)** Allowances and how limits behave **[Events and webhooks](/guides/webhooks)** Subscribe to approval and invocation events **[Approvals and policies](/trust-control/approvals)** What an `approval_requested` alert is asking for **[Workspaces](/account/workspaces)** Members and roles --- # Workspaces & Team Management Source: https://docs.anlyon.com/account/workspaces A workspace holds your environments, API keys, members and usage. Each workspace has its own data and its own members. The endpoints on this page are the ones the console uses. They take a signed-in console session, not an API key. Most teams manage members and keys in the console under **Settings**. ## Getting workspace information ### List your workspaces ```bash curl https://api.anlyon.com/api/v2/workspaces \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` ```json { "success": true, "data": [ { "workspace": { "id": "2f8a1f72-7cf9-4fef-95e8-df9de5f2f5d0", "name": "My Workspace", "slug": "my-workspace", "plan": "free", "createdAt": "2026-09-23T10:00:00Z" }, "role": "owner", "joinedAt": "2026-09-23T10:00:00Z" } ] } ``` ## Team management **List workspace members** ```bash curl https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/members \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` **Invite someone by email** An owner or admin sends an invitation. The person joins when they accept it. A pending invitation counts as a seat. ```bash curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/invitations \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "newmember@example.com", "role": "member" }' ``` `role` is `admin` or `member`. **Update a member's role** ```bash curl -X PATCH https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/members/$USER_ID \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role": "admin" }' ``` **Remove a member** ```bash curl -X DELETE https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/members/$USER_ID \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` ## Roles There are three roles: owner, admin and member. Each role can do what the one below it can. | Role | What it adds | | --- | --- | | **Member** | Read the workspace, its members and its API keys. Decide approvals in the console | | **Admin** | Change workspace settings. Invite, re-role and remove members. Create, regenerate and delete API keys. Create environments, and halt or resume one. Create and promote agents. Change approval policies | | **Owner** | Delete the workspace. Transfer ownership. Delete an empty environment. Change the plan, payment method and overage alert | Any workspace member can decide an approval. ## API key management ### List API keys ```bash curl https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/api-keys \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` ### Create an API key An admin or owner creates keys. Name the environment the key belongs to and the scopes it holds. See [Authentication & API Keys](/authentication). ```bash curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/api-keys \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Support agent, staging", "environmentId": "ENVIRONMENT_ID", "scopes": ["actions:invoke", "actions:read", "approvals:read"], "expiresAt": "2027-12-31T23:59:59Z" }' ``` **Warning** The full API key is shown once. Store it somewhere safe. ## Good practice - Give the admin role to the people who need to change keys, members or policies. - Review members and API keys on a schedule. - Set an expiry on keys, and delete the ones you no longer use. ## Next steps **[Authentication](/authentication)** API keys, environments and scopes **[Billing & Pricing](/account/billing)** Allowances and how limits behave **[Environments](/trust-control/environments)** Keep a staging key out of production data --- # Analytics API Source: https://docs.anlyon.com/advanced/analytics Use these endpoints to inspect agents, action invocations, approval decisions and usage from one analytics surface. ## Authentication All endpoints use API key auth, and the key needs `analytics:read`: ```bash curl https://api.anlyon.com/api/v2/analytics/trust-control \ -H "Authorization: Bearer anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ## Endpoints | Method | Endpoint | Notes | |---|---|---| | `GET` | `/api/v2/analytics/overview` | Summary figures, usage, and attention signals | | `GET` | `/api/v2/analytics/agents` | Agent run outcomes, latency, tokens, and cost | | `GET` | `/api/v2/analytics/trust-control` | Action invocation and approval decision analytics | | `GET` | `/api/v2/analytics/usage/current` | Current usage snapshot | | `GET` | `/api/v2/analytics/usage/history` | Usage history over date range | | `GET` | `/api/v2/analytics/errors` | Error distribution (`limit`) | | `GET` | `/api/v2/analytics/onboarding` | Workspace onboarding status | ## Common query parameters The following are supported where date filtering is available: - `startDate` (ISO 8601) - `endDate` (ISO 8601) - `granularity` (`hour`, `day`, `week`, `month`) - `limit` for `/errors` The four product analytics endpoints support all parameters above, plus `environmentId`. Session-authenticated dashboard requests can select an environment or query the whole workspace. API keys and OAuth tokens always remain restricted to their bound environment. ## Examples ### Product overview ```bash curl "https://api.anlyon.com/api/v2/analytics/overview?startDate=2026-07-01T00:00:00Z&endDate=2026-07-19T00:00:00Z&granularity=day" \ -H "Authorization: Bearer anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### Usage history ```bash curl "https://api.anlyon.com/api/v2/analytics/usage/history?startDate=2025-01-01T00:00:00Z&endDate=2025-01-31T23:59:59Z" \ -H "Authorization: Bearer anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### Error distribution ```bash curl "https://api.anlyon.com/api/v2/analytics/errors?startDate=2025-01-01T00:00:00Z&endDate=2025-01-31T23:59:59Z&limit=10" \ -H "Authorization: Bearer anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ## Notes - Date filters are optional. When you leave them out, the API applies its defaults. - Product analytics responses include `range` and `scope` metadata. `range.truncated` is true when the requested start date is older than the workspace plan's analytics retention window. - Metered product usage is workspace-wide and is labeled as such. Environment-scoped products are filtered by `environmentId`. - Responses include `success` and `data`, and many endpoints also include `charts` payloads for dashboard visualization. --- # Error Handling & Rate Limiting Source: https://docs.anlyon.com/advanced/error-handling This guide covers the errors the API returns, rate limits, and how to retry safely. ## Error response format Errors share one format: ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable error message" }, "requestId": "..." } ``` Match on `error.code`. The hosted API leaves `error.details` out, so read the code and the message. Quote `requestId` when you contact support. ## A refused action is not an HTTP error An action invoke that Anlyon refuses at dispatch, or whose outcome is unknown, answers HTTP `200`. The refusal is in the body. Read `status` and `grade`, not the status code alone. ```json { "status": "failed", "grade": "refused", "error": "Dispatch refused (impact_limit_exceeded): Not enough allowance: daily emails needs 3, has 2." } ``` See [When the limit is hit](/execution/when-the-limit-is-hit) and [When the outcome is unknown](/execution/when-the-outcome-is-unknown). ## HTTP status codes | Status code | Meaning | |-------------|---------| | `200` | Success. For an action invoke, read `status` and `grade` in the body | | `202` | An action invoke is parked for approval | | `400` | Invalid input | | `401` | Authentication failed | | `403` | A missing scope or role, or a capability the plan does not include | | `404` | The resource does not exist in this workspace and environment | | `409` | A conflict, such as a reused idempotency key or a preview that no longer matches | | `429` | A rate limit, a quota or a per-key budget was reached | | `500` | Internal server error | | `503` | Service unavailable, or new expensive work is paused | ## Common error codes **AUTHENTICATION_ERROR (401)** The key did not authenticate. An invalid, expired or revoked key gets the same answer. **Solutions**: - Verify the API key is correct - Check the `Authorization` header format: `Bearer anlyon_live_...` - Check in the dashboard that the key has not been revoked or expired **AUTHORIZATION_ERROR (403)** The credential authenticated and lacks a scope or a role. The message names the missing scope, for example `API key is missing required scope: actions:invoke`. **Solutions**: - Create a key with that scope. Scopes are fixed when a key is created - See [Scopes](/authentication#scopes) **RATE_LIMIT_EXCEEDED (429)** Too many requests in one minute. A `429` response carries no rate limit headers. Read `X-RateLimit-Remaining` on the responses before it. The window is one minute. **Solutions**: - Wait for the window to reset - Retry with exponential backoff - Reduce request frequency - Rate limits are fixed during Early Beta Access. Paid plans with higher limits arrive after the beta **QUOTA_EXCEEDED (429 or 402)** A monthly allowance, storage limit, or resource cap was reached. Nothing is charged and nothing is upgraded automatically. The message says which limit and what to do about it. Most paths answer `429` and a few answer `402`, so match on the code. **Solutions**: - Monthly allowance (executions, messages, egress, memory processing): wait for the reset at 00:00 UTC on the first of the month, shown in **Settings → Plan & Billing** and in `GET /api/v2/billing/plan` as `access.usageResetsAt`, or contact support. There is no guaranteed increase. - Storage or a resource cap (data, seats, actions, memories): remove unused items to free room. These limits do not reset. - Memory processing: existing data is kept, and processing resumes at the reset. - Monitor usage with `GET /api/v2/billing/quota` and set up usage alerts - Do not call the upgrade endpoint in response: until paid plans open it returns `PAID_PLANS_UNAVAILABLE` **BUDGET_EXCEEDED (429)** The calling key reached its monthly cap on an Anlyon operation. A budget caps Anlyon operations. It does not cap model tokens or the money an action moves. **Solutions**: - Wait for the next UTC month, or have an operator raise the cap with `budgets:write` **VALIDATION_ERROR (400)** Invalid request data. **Solutions**: - Read the message for the field that failed - Check the [API reference](/api-reference) for required fields **FEATURE_NOT_AVAILABLE (403)** The workspace's plan does not include the capability. **Solutions**: - Early Beta Access includes environment protection, controlled promotion and the email notification channel - Paid plans arrive after the beta. Review [Billing & Pricing](/account/billing) for what each plan includes **PAID_PLANS_UNAVAILABLE (403)** A paid plan or card setup was requested before paid plans opened. Paid plans arrive after the beta. The request is refused before any payment system is contacted, so nothing was charged and no payment object was created. The message reads "Paid plans arrive after the beta. Nothing was charged. Your workspace keeps its Early Beta allowances." **Solutions**: - Stay on Early Beta Access. Your workspace keeps its Early Beta allowances - Read `access.paidPlansAvailable` from `GET /api/v2/billing/plan` and do not offer an upgrade unless it is `true` **EXPENSIVE_WORK_PAUSED (503)** New expensive work (memory processing, hosted action execution, file and cache writes) is temporarily paused for every workspace to protect the service. Signing in, viewing, and deleting your data still work. **Solutions**: - Retry later with backoff - Check the [status page](https://stats.uptimerobot.com/IkZSpnHLVG) **NOT_FOUND (404)** The resource does not exist where the credential can see it. **Solutions**: - Verify the resource ID is correct - Check that the key belongs to the environment the resource is in - Check that the resource has not been deleted Action invokes have their own `409` codes, such as `PREVIEW_MISMATCH`, `PREVIEW_EXPIRED`, `PREVIEW_ALREADY_USED`, `OPERATION_KEY_REUSE` and `idempotency_key_reuse`. See [When approval expires or changes](/execution/when-approval-expires-or-changes) and [Idempotency and retries](/execution/idempotency). ## Rate limiting ### Rate limits by plan | Plan | Requests per minute | |------|----------------| | Free | 300 | | Production / Growth (planned) | 1,000 | | Enterprise | No fixed limit | Rate limits are applied per workspace, in one-minute windows. ### Rate limit headers Responses that pass the limit carry rate limit headers. A `429` does not. `X-RateLimit-Reset` is a Unix time in milliseconds. ``` X-RateLimit-Limit: 300 X-RateLimit-Remaining: 299 X-RateLimit-Reset: 1737381660000 ``` ## Error handling best practices **Check the response status** ```typescript const response = await fetch(url, options); const body = await response.json(); if (!response.ok) { throw Object.assign(new Error(body.error.message), { code: body.error.code, status: response.status, }); } return body.data; ``` **Handle specific error codes** ```typescript try { await invokeAction('update-status-file', input, idempotencyKey); } catch (error) { if (error.code === 'QUOTA_EXCEEDED') { console.warn('Quota exceeded. Wait for the monthly reset or free up room.'); } else if (error.code === 'RATE_LIMIT_EXCEEDED') { // Wait, then send the same request with the same Idempotency-Key. } else { console.error('Unexpected error:', error); } } ``` **Retry an invoke with the same Idempotency-Key** Anlyon does not retry an action invocation. If your code retries an invoke, send the same `Idempotency-Key` with the same request. The retry is answered from the recorded invocation, and the action does not run again. ```typescript async function retryWithBackoff( fn: () => Promise, maxRetries = 3, baseDelay = 1000 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { if (attempt === maxRetries - 1) throw error; // Do not retry client errors, except a rate limit. if (error.status >= 400 && error.status < 500 && error.status !== 429) { throw error; } const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('Max retries exceeded'); } ``` Do not retry an invocation whose `status` is `unknown` under a new key. Reconcile it first. ## Code examples The SDKs do this for you. These clients show the same handling over plain HTTP. ```typescript TypeScript class AnlyonClient { private apiKey: string; private baseUrl: string; constructor(apiKey: string, baseUrl = 'https://api.anlyon.com') { this.apiKey = apiKey; this.baseUrl = baseUrl; } async request(endpoint: string, options: RequestInit = {}): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', ...options.headers, }, }); const data = await response.json(); if (!response.ok) { const error = new Error(data.error?.message || 'Request failed'); (error as any).code = data.error?.code; (error as any).status = response.status; (error as any).requestId = data.requestId; throw error; } return data.data; } async invokeAction(ref: string, input: Record, idempotencyKey: string) { return this.request(`/api/v2/actions/${ref}/invoke`, { method: 'POST', headers: { 'Idempotency-Key': idempotencyKey }, body: JSON.stringify({ input }), }); } } ``` ```python Python import requests from typing import Any, Dict class AnlyonClient: def __init__(self, api_key: str, base_url: str = "https://api.anlyon.com"): self.api_key = api_key self.base_url = base_url self.session = requests.Session() self.session.headers.update({ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }) def request(self, endpoint: str, method: str = "GET", **kwargs) -> Dict[str, Any]: url = f"{self.base_url}{endpoint}" response = self.session.request(method, url, **kwargs) try: data = response.json() except ValueError: data = {} if not response.ok: error = data.get("error", {}) raise Exception( f"API Error: {error.get('message', 'Request failed')} " f"(Code: {error.get('code')}, Status: {response.status_code})" ) return data.get("data", data) def invoke_action(self, ref: str, input: Dict[str, Any], idempotency_key: str) -> Dict[str, Any]: return self.request( f"/api/v2/actions/{ref}/invoke", method="POST", json={"input": input}, headers={"Idempotency-Key": idempotency_key}, ) ``` ## Next steps **[Controls](/trust-control)** Approvals, environments, kill switch, rollback. **[Authentication](/authentication)** API keys, environments and scopes. **[Idempotency and retries](/execution/idempotency)** Replay an invocation without repeating a side effect. --- # Agent setup Source: https://docs.anlyon.com/agent-setup/prompt You are setting up a project to use **Anlyon**, governed execution for AI agents. The agent names an action and passes input. Anlyon holds the credential, evaluates policy, waits for a human where one is required, makes the production call itself and records the outcome. Credentials are scoped: an agent key for the work, and a separate operator key that defines actions and vaults secrets. Work through the steps below in order. Every step is idempotent: if something is already done, confirm it and move on. Do not invent configuration that is not described here or in the fetched context. ## 1. Find the relevant documentation Retrieve `https://docs.anlyon.com/llms.txt` for the page index, then read `https://docs.anlyon.com/quickstart.md` and the Markdown pages relevant to the integration. The [agent-readable docs guide](/agent-setup/read-docs) lists the other discovery endpoints. Use `https://docs.anlyon.com/llms-full.txt` only when you need the entire documentation in one file. Check the API reference for exact endpoints and schemas. Do not infer them from examples. ## 2. Install the SDK Detect the project's language and package manager, then install the official client: - **TypeScript / JavaScript**: `@anlyonhq/sdk` - npm: `npm install @anlyonhq/sdk` - pnpm: `pnpm add @anlyonhq/sdk` - yarn: `yarn add @anlyonhq/sdk` - bun: `bun add @anlyonhq/sdk` - **Python**: `anlyon` - pip: `pip install anlyon` - uv: `uv add anlyon` - poetry: `poetry add anlyon` If the project is neither, stop and ask the user which SDK to use. ## 3. Write the agent rules file Create or update the rules file for the coding tool in use, adding the Anlyon project block shown below. Match the tool: | Tool | File | | --- | --- | | Claude Code | `CLAUDE.md` | | Cursor | `.cursor/rules/anlyon.mdc` | | Windsurf | `.windsurf/rules.md` | | GitHub Copilot | `.github/copilot-instructions.md` | | Codex / generic | `AGENTS.md` | If the file already exists, append the block rather than overwriting it. ````markdown # Anlyon ## Project context - This project routes agent side effects through Anlyon actions. The agent never holds a provider credential. - TypeScript SDK: `@anlyonhq/sdk` · Python SDK: `anlyon` - API base URL: https://api.anlyon.com - Docs: https://docs.anlyon.com · Full context: https://docs.anlyon.com/llms-full.txt ## Two clients, two keys ```typescript import { Client } from "@anlyonhq/sdk"; // Defines actions and vaults credentials. Never used by agent code. const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! }); // The agent's key. Can invoke actions; cannot read a credential or rewrite an action. const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! }); ``` ## Define an action once (operator) ```typescript await ops.secrets.put("STRIPE_KEY", { value: process.env.STRIPE_KEY!, allowedHosts: ["api.stripe.com"] }); await ops.actions.create({ name: "refund-order", method: "POST", urlTemplate: "https://api.stripe.com/v1/refunds", headers: { Authorization: "Bearer {{secret:STRIPE_KEY}}", "Content-Type": "application/x-www-form-urlencoded" }, requiresApproval: true, }); ``` ## Invoke it by name (agent) ```typescript const { data } = await anlyon.actions.invoke( "refund-order", { charge: chargeId, amount: amountCents }, { idempotencyKey: `refund-${orderId}` }, ); if (data!.pendingApproval) { const decided = await anlyon.approvals.waitForDecision(data!.approvalId!); if (decided.status !== "approved") return; // denied, expired, or still pending } ``` ## Working guidelines - Never put an API key or a provider credential in code. Read keys from `ANLYON_OPERATOR_KEY` and `ANLYON_AGENT_KEY`. - Production calls go through `actions.invoke`. Do not call a provider API directly from agent code. - Set an idempotency key on every invocation that must not run twice. - An invocation with status `unknown` may have taken effect. Do not retry it. Reconcile with the destination first. - Use a staging environment key in non-production. It cannot touch production data. - Verify inbound webhooks with the SDK's `serve()` helper or `Receiver` class. ## Do not - Commit API keys or signing secrets. - Print, log or store a provider credential. Actions reference `{{secret:NAME}}` and Anlyon resolves it at dispatch. - Skip webhook signature verification. - Retry an `unknown` invocation without reconciling. ```` ## 4. Wire up the environment variables Add `ANLYON_AGENT_KEY` and `ANLYON_OPERATOR_KEY` (and `ANLYON_SIGNING_SECRET` if the project receives webhooks) to `.env.example` and the project's environment documentation. Do not write real secret values. Mint keys at [https://console.anlyon.com](https://console.anlyon.com): the operator key with `secrets:write`, `secrets:read` and `actions:write` (an action definition that references a vaulted secret needs `secrets:read`), the agent key with `actions:invoke`, `actions:read` and `approvals:read` (to read invocations and wait for decisions), both in the same non-production environment. ## 5. Add a first integration point If the project has an obvious place for it (a call to a third-party API made on a user's behalf, a job that changes production data), define it as an action with the operator client and replace the direct call with `actions.invoke` from the agent client, using the patterns above. Leave a `TODO` where the user must supply the real URL template, headers and input schema. ## 6. Point the user at next steps - Quickstart: [https://docs.anlyon.com/quickstart](https://docs.anlyon.com/quickstart) - Actions: [https://docs.anlyon.com/execution/actions](https://docs.anlyon.com/execution/actions) - Approvals and policies: [https://docs.anlyon.com/trust-control/approvals](https://docs.anlyon.com/trust-control/approvals) - TypeScript SDK: [https://docs.anlyon.com/guides/typescript-sdk](https://docs.anlyon.com/guides/typescript-sdk) - API reference: [https://docs.anlyon.com/api-reference](https://docs.anlyon.com/api-reference) ## If asked to verify With `ANLYON_OPERATOR_KEY` set, define an action named `echo-order` that POSTs to `https://httpbin.org/anything` with a made-up secret (`DEMO_TOKEN`, value `demo_not_a_real_secret`, `allowedHosts: ["httpbin.org"]`) and `requiresApproval: false`. With `ANLYON_AGENT_KEY` set, invoke it with `{ orderId: "test_order_1", amount: 100 }` and confirm the invocation's `status` is `succeeded` and `responseStatus` is `200`. Report the invocation id back to the user. Do not define an action against a production URL for this check. --- # Read these docs as an agent Source: https://docs.anlyon.com/agent-setup/read-docs Anlyon's documentation is available as focused Markdown pages and as a machine-readable index. Start with the smallest source that answers your question. | Need | URL | | --- | --- | | Discover every agent-facing artifact | [`/agent-readability.json`](https://docs.anlyon.com/agent-readability.json) | | Find a page by title and summary | [`/llms.txt`](https://docs.anlyon.com/llms.txt) | | Read one page without site navigation | Append `.md` to its URL, for example [`/quickstart.md`](/quickstart.md) | | List pages with their Markdown and JSON URLs | [`/api/docs/pages.json`](https://docs.anlyon.com/api/docs/pages.json) | | Read the full documentation in one file | [`/llms-full.txt`](https://docs.anlyon.com/llms-full.txt) | The [quickstart](/quickstart) and [execution model](/execution) explain how Anlyon works. Use the [API reference](/api-reference) for exact Anlyon operations, parameters, and response schemas. The generated [`/openapi.json`](https://docs.anlyon.com/openapi.json) describes the **documentation JSON API**, not the Anlyon product API. The site also publishes an [AI catalog](https://docs.anlyon.com/.well-known/ai-catalog.json), an [API catalog](https://docs.anlyon.com/.well-known/api-catalog), and an [ARD manifest](https://docs.anlyon.com/.well-known/ard.json) for discovery from the docs domain. Each HTML page links to its own Markdown mirror and the discovery files. This documentation is served as static assets on Cloudflare. The page index, per-page JSON, Markdown, and catalogs are available without a running documentation server. The live documentation search endpoint and MCP server are not enabled on this deployment. --- # ChatGPT Source: https://docs.anlyon.com/ai-tools/chatgpt The Anlyon ChatGPT app listing was withdrawn. Anlyon executes named production actions on an agent's behalf and routes sensitive ones to a human reviewer first. Connect a workspace to list its actions, invoke one, request approval, and, with the decide permission, approve or deny pending requests. A tool your own code calls directly is not routed through Anlyon and is not governed by it. Anlyon is not listed in ChatGPT's app directory. The connection below uses ChatGPT's custom connector support with Anlyon's hosted MCP endpoint. ## Connect ChatGPT connects to Anlyon's hosted MCP endpoint and signs you in with OAuth ("Login with Anlyon"). There is no API key to paste. ``` https://api.anlyon.com/mcp ``` Add the endpoint as a custom connector (ChatGPT calls this developer mode, and the menu names change between releases, so check ChatGPT's help for the current steps), with OAuth as the authentication method. When you connect: 1. Sign in to Anlyon, or create an account. A new account gets a free workspace during sign-in, and you do not need to open the dashboard first. 2. If you belong to more than one workspace, choose the workspace and environment ChatGPT acts in. With one workspace, the connection is bound to its default environment. 3. Choose permissions. Actions are on by default. "Approve on your behalf" is off unless you turn it on, and without it ChatGPT cannot list or decide pending approvals. ChatGPT sees only the tools the permissions you granted cover. You can disconnect at any time from Anlyon's settings, and the connection stops working on its next request. ## Actions and approvals Once your team has defined actions in the dashboard: - **"What actions can my Anlyon workspace run?"** lists them (`list_actions`) and says which require approval. - **"Raise support ticket T-1042 to high priority."** invokes the action (`invoke_action`). The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. If the action requires approval, the request waits for a reviewer and ChatGPT can wait for the decision (`wait_for_approval`). - **"Show my pending approvals."** With "Approve on your behalf" granted, ChatGPT lists what is waiting (`list_pending_approvals`) and can approve or deny with the reason you give (`decide_approval`). The reason is recorded against your name. Where a rule requires N distinct approvers, your decision counts as one user, and a connection can never decide an approval it requested itself. Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying. ## Other tools The same endpoint works from [Claude Code](/ai-tools/claude-code), [Cursor](/ai-tools/cursor) and [Codex](/ai-tools/codex). Connect each one to the **same workspace** and environment, and the approvals one tool requests can be reviewed from another. --- # Claude Code Source: https://docs.anlyon.com/ai-tools/claude-code Anlyon executes named production actions on an agent's behalf and routes sensitive ones to a human reviewer first. Connect a workspace to list its actions, invoke one, request approval, and, with the decide permission, approve or deny pending requests. A tool your own code calls directly is not routed through Anlyon and is not governed by it. ## Connect Claude Code connects to Anlyon's hosted MCP endpoint (streamable HTTP) and signs you in with OAuth ("Login with Anlyon"). There is no API key to paste. Add the server: ```bash claude mcp add --transport http anlyon https://api.anlyon.com/mcp ``` Then, inside Claude Code, run: ``` /mcp ``` Select `anlyon` and authenticate. A browser opens. Sign in to Anlyon or create an account (a new account gets a free workspace during sign-in), and choose permissions. If you belong to more than one workspace, Anlyon first asks which workspace and environment Claude Code acts in. With one workspace, the connection is bound to its default environment. "Approve on your behalf" is off unless you turn it on. Claude Code sees only the tools the permissions you granted cover. `claude mcp add` registers the server for the current project by default. Add `--scope user` to make it available in every project. Claude Code's MCP commands change between releases. If the command above is refused, check Claude Code's MCP documentation for your version. ### Running Claude Code in CI or without a browser OAuth needs a browser once. For a headless run, use the self-hosted server with an API key instead, scoped to what the run should be able to do. In a project's `.mcp.json`, reference the key from the environment rather than writing it into the file: ```json { "mcpServers": { "anlyon": { "command": "npx", "args": ["-y", "@anlyonhq/mcp-server"], "env": { "ANLYON_API_KEY": "${ANLYON_API_KEY}" } } } } ``` Keep the key out of the repository. The key's scopes are exactly what Claude Code can do. ## Tools By default the server exposes the governed set and nothing else: | Tool | What it does | | --- | --- | | `list_actions` | List the workspace's actions and say which require approval | | `invoke_action` | Invoke an action by name. Actions also appear as tools named `action_` | | `request_approval` | Ask a human to decide something outside an action | | `check_approval` | Read an approval's status once | | `wait_for_approval` | Wait for a reviewer's decision, up to 120 seconds a call, 60 by default. Returns the still pending approval if nobody has decided | | `list_pending_approvals`, `decide_approval` | Review and decide. On the hosted endpoint, shown only with "Approve on your behalf". With an API key, the key needs `approvals:decide` | | `list_workspaces`, `select_workspace` | Choose which workspace and environment Claude Code acts in. Switching is OAuth only and needs "Switch between your workspaces" | ## Actions and approvals Claude Code lists the workspace's actions and invokes them as tools named `action_`. The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. This covers credentials in the Anlyon vault, not a key your own process already holds. An action that requires approval waits for a reviewer, and Claude Code can wait with `wait_for_approval`. Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying. With "Approve on your behalf" granted, `list_pending_approvals` and `decide_approval` let you review from Claude Code. Your decision is recorded against your name and counts as one user toward an N-of-M rule. A connection cannot decide an approval it requested itself. Decide those from the dashboard or another connection. ## Project context file To tell Claude Code how this project uses Anlyon, add a block to `CLAUDE.md` at the project root. For example: ````markdown ## Anlyon - Production calls go through Anlyon actions, not direct API calls. List them with the Anlyon `list_actions` tool and invoke one through its `action_` tool. - Never ask for, print or store a provider credential. Actions reference `{{secret:NAME}}`; Anlyon resolves it at dispatch. - If an action needs approval, call `wait_for_approval` with the `apr_` id and report the decision. - An invocation with status `unknown` may have taken effect. Do not retry it; tell me so I can reconcile. - Docs: https://docs.anlyon.com/llms.txt ```` To set up a project to call Anlyon from its own code with the SDK, paste the [agent setup prompt](/agent-setup/prompt) into Claude Code instead. ## Prefer a terminal? The [Anlyon CLI](/cli) lists actions, invokes them and decides approvals from a shell, with the same permissions as the API. Claude Code can run it too. Writes from a non-interactive shell are refused unless `--yes` is passed, so a write never happens by accident. ## Connect a second tool An approval one tool requests can be reviewed from another. Connect the second one to the **same workspace** and environment. **Codex:** add the server to `~/.codex/config.toml`, then sign in: ```toml [mcp_servers.anlyon] url = "https://api.anlyon.com/mcp" ``` ```bash codex mcp login anlyon ``` **Cursor:** add `https://api.anlyon.com/mcp` through Settings, "Add MCP server". See [Cursor](/ai-tools/cursor). --- # Codex Source: https://docs.anlyon.com/ai-tools/codex Anlyon executes named production actions on an agent's behalf and routes sensitive ones to a human reviewer first. Connect a workspace to list its actions, invoke one, request approval, and, with the decide permission, approve or deny pending requests. A tool your own code calls directly is not routed through Anlyon and is not governed by it. ## Connect Codex connects to Anlyon's hosted MCP endpoint and signs you in with OAuth ("Login with Anlyon"). There is no API key to paste. Add the server to `~/.codex/config.toml`: ```toml [mcp_servers.anlyon] url = "https://api.anlyon.com/mcp" ``` Then sign in: ```bash codex mcp login anlyon ``` A browser opens. Sign in to Anlyon or create an account (a new account gets a free workspace during sign-in), and choose permissions. If you belong to more than one workspace, Anlyon first asks which workspace and environment Codex acts in. With one workspace, the connection is bound to its default environment. "Approve on your behalf" is off unless you turn it on. Codex sees only the tools the permissions you granted cover. Codex's configuration format changes between releases. If the block above is refused, check Codex's MCP documentation for your version. ### Running Codex in CI or without a browser OAuth needs a browser once. For a headless run, use the self-hosted server with an API key instead, scoped to what the run should be able to do: ```toml [mcp_servers.anlyon] command = "npx" args = ["-y", "@anlyonhq/mcp-server"] env = { ANLYON_API_KEY = "anlyon_live_..." } ``` Keep the key out of the repository. The key's scopes are exactly what Codex can do. The self-hosted server exposes the governed tools only. ## Tools By default the server exposes the governed set and nothing else: | Tool | What it does | | --- | --- | | `list_actions` | List the workspace's actions and say which require approval | | `invoke_action` | Invoke an action by name. Actions also appear as tools named `action_` | | `request_approval` | Ask a human to decide something outside an action | | `check_approval` | Read an approval's status once | | `wait_for_approval` | Wait for a reviewer's decision, up to 120 seconds a call, 60 by default. Returns the still pending approval if nobody has decided | | `list_pending_approvals`, `decide_approval` | Review and decide. On the hosted endpoint, shown only with "Approve on your behalf". With an API key, the key needs `approvals:decide` | | `list_workspaces`, `select_workspace` | Choose which workspace and environment Codex acts in. Switching is OAuth only and needs "Switch between your workspaces" | ## Actions and approvals Codex lists the workspace's actions and invokes them as tools named `action_`. The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. This covers credentials in the Anlyon vault, not a key your own process already holds. An action that requires approval waits for a reviewer, and Codex can wait with `wait_for_approval`. Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying. With "Approve on your behalf" granted, `list_pending_approvals` and `decide_approval` let you review from Codex. Your decision is recorded against your name and counts as one user toward an N-of-M rule. ## Connect a second tool An approval one tool requests can be reviewed from another. Connect the second one to the **same workspace** and environment. **Claude Code** ```bash claude mcp add --transport http anlyon https://api.anlyon.com/mcp # then run /mcp to sign in ``` **Cursor:** add `https://api.anlyon.com/mcp` through Settings, "Add MCP server". --- # Cursor Source: https://docs.anlyon.com/ai-tools/cursor Anlyon executes named production actions on an agent's behalf and routes sensitive ones to a human reviewer first. Connect a workspace to list its actions, invoke one, request approval, and, with the decide permission, approve or deny pending requests. A tool your own code calls directly is not routed through Anlyon and is not governed by it. ## Connect Cursor connects to Anlyon's hosted MCP endpoint (streamable HTTP) and signs you in with OAuth ("Login with Anlyon"). There is no API key to paste. Add the server through Cursor's settings, "Add MCP server", with this URL: ``` https://api.anlyon.com/mcp ``` Or add it to an MCP configuration file: `.cursor/mcp.json` in a project for that project only, or `~/.cursor/mcp.json` for every project: ```json { "mcpServers": { "anlyon": { "url": "https://api.anlyon.com/mcp" } } } ``` Cursor then asks you to sign in to the server. A browser opens. Sign in to Anlyon or create an account (a new account gets a free workspace during sign-in), and choose permissions. If you belong to more than one workspace, Anlyon first asks which workspace and environment Cursor acts in. With one workspace, the connection is bound to its default environment. "Approve on your behalf" is off unless you turn it on. Cursor sees only the tools the permissions you granted cover. Cursor's settings and configuration format change between releases. If the steps above do not match your version, check Cursor's MCP documentation. ### Running without a browser OAuth needs a browser once. Where that is not possible, use the self-hosted server with an API key instead, scoped to what Cursor should be able to do: ```json { "mcpServers": { "anlyon": { "command": "npx", "args": ["-y", "@anlyonhq/mcp-server"], "env": { "ANLYON_API_KEY": "anlyon_live_..." } } } } ``` Put this in `~/.cursor/mcp.json`, not in a project file that is committed. The key's scopes are exactly what Cursor can do. The self-hosted server exposes the governed tools only. ## Tools By default the server exposes the governed set and nothing else: | Tool | What it does | | --- | --- | | `list_actions` | List the workspace's actions and say which require approval | | `invoke_action` | Invoke an action by name. Actions also appear as tools named `action_` | | `request_approval` | Ask a human to decide something outside an action | | `check_approval` | Read an approval's status once | | `wait_for_approval` | Wait for a reviewer's decision, up to 120 seconds a call, 60 by default. Returns the still pending approval if nobody has decided | | `list_pending_approvals`, `decide_approval` | Review and decide. On the hosted endpoint, shown only with "Approve on your behalf". With an API key, the key needs `approvals:decide` | | `list_workspaces`, `select_workspace` | Choose which workspace and environment Cursor acts in. Switching is OAuth only and needs "Switch between your workspaces" | ## Actions and approvals Cursor lists the workspace's actions and invokes them as tools named `action_`. The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. This covers credentials in the Anlyon vault, not a key your own process already holds. An action that requires approval waits for a reviewer, and Cursor can wait with `wait_for_approval`. Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying. With "Approve on your behalf" granted, `list_pending_approvals` and `decide_approval` let you review from Cursor. Your decision is recorded against your name and counts as one user toward an N-of-M rule. A connection cannot decide an approval it requested itself. Decide those from the dashboard or another connection. ## Project rules To tell Cursor how this project uses Anlyon, add a rule file such as `.cursor/rules/anlyon.mdc`. For example: ````markdown ## Anlyon - Production calls go through Anlyon actions, not direct API calls. List them with the Anlyon `list_actions` tool and invoke one through its `action_` tool. - Never ask for, print or store a provider credential. Actions reference `{{secret:NAME}}`; Anlyon resolves it at dispatch. - If an action needs approval, call `wait_for_approval` with the `apr_` id and report the decision. - An invocation with status `unknown` may have taken effect. Do not retry it; tell me so I can reconcile. - Docs: https://docs.anlyon.com/llms.txt ```` To set up a project to call Anlyon from its own code with the SDK, paste the [agent setup prompt](/agent-setup/prompt) into Cursor instead. ## Prefer a terminal? The [Anlyon CLI](/cli) lists actions, invokes them and decides approvals from a shell, with the same permissions as the API. ## Connect a second tool An approval one tool requests can be reviewed from another. Connect the second one to the **same workspace** and environment. **Claude Code** ```bash claude mcp add --transport http anlyon https://api.anlyon.com/mcp # then run /mcp to sign in ``` **Codex:** add the server to `~/.codex/config.toml`, then sign in: ```toml [mcp_servers.anlyon] url = "https://api.anlyon.com/mcp" ``` ```bash codex mcp login anlyon ``` --- # OpenAI Agents API Source: https://docs.anlyon.com/ai-tools/openai-agents-api OpenAI's Responses API and Agents API can connect to a remote MCP server as a tool source. Anlyon's MCP server works as that server: the model sees your workspace's actions as tools, calls one, and Anlyon executes it against production systems. The statements about OpenAI's APIs on this page come from OpenAI's documentation as checked on 2026-09-30. Check [OpenAI's MCP tool guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp) for the current behaviour. Your agent declares what it wants to do. Anlyon executes it against production systems. The agent names an action and passes input, and Anlyon resolves the credentials at dispatch and makes the request. That is true of actions routed through Anlyon's hosted executor. It is not true of a tool your own code calls directly, which is not routed through Anlyon and is not governed by it. ## When OpenAI's approval switch is enough OpenAI's Agents API and Responses API can pause an MCP tool call for approval. The pause is answered by your own code. Anlyon routes the call to a named reviewer, executes the reviewed request with credentials bound to that action, and records the outcome. Use OpenAI's switch when one developer is the reviewer. Use Anlyon when a second person, a record, or a second agent runtime is involved. Anlyon's half holds for actions routed through its hosted executor. ## Choose the bearer The Responses API sends the value you put in the `mcp` tool's `authorization` field as a bearer on every request, and does not store it. It does not run an OAuth flow for you: you supply a token that is already valid. | Endpoint | Accepts | Use it from the Responses API? | | --- | --- | --- | | Self-hosted `anlyon-mcp --http` | An Anlyon API key as `Authorization: Bearer anlyon_live_...` | **Yes.** This is the setup this page documents. | | Hosted `https://api.anlyon.com/mcp` | OAuth access tokens issued through "Login with Anlyon" only | Not in practice: an API key is refused, and an OAuth access token expires after 15 minutes. Use the hosted endpoint from clients that run the OAuth flow themselves, such as Claude Code, Codex and Cursor. | Run the self-hosted transport somewhere OpenAI can reach over HTTPS: ```bash npx -y @anlyonhq/mcp-server --http # listens on PORT (default 3001), serves POST /mcp ``` It holds no credential of its own. Each request's bearer is the Anlyon API key the tools act with, so the key's scopes are exactly what the model can do. Create two keys in the [dashboard](https://console.anlyon.com): - **Agent key** (`ANLYON_AGENT_KEY`): `actions:read`, `actions:invoke`, `approvals:read`. This is the MCP bearer. Do not give it `approvals:decide`. - **Operator key** (`ANLYON_OPERATOR_KEY`): `approvals:decide` and `actions:read`, held by the person or service that reviews. A key can never decide an approval it requested itself. Add `actions:resolve` if this key will also record the outcome of an `unknown` invocation. ## Call a gated action from the Responses API Set `require_approval: "never"` on the OpenAI side. The approval lives in Anlyon, which parks the call for your reviewer. Asking OpenAI to pause as well would put a second approval, one Anlyon does not record, in your own process in front of the one that counts. This example asks the model to change a support ticket's priority through an action named `update-ticket-priority` that requires approval, then to wait for the decision: ```javascript Responses API // Node 22+. Needs OPENAI_API_KEY, OPENAI_MODEL, ANLYON_MCP_URL and ANLYON_AGENT_KEY. const response = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: process.env.OPENAI_MODEL, tools: [{ type: 'mcp', server_label: 'anlyon', server_url: process.env.ANLYON_MCP_URL, authorization: process.env.ANLYON_AGENT_KEY, // The approval happens in Anlyon, not here. require_approval: 'never', allowed_tools: ['action_update-ticket-priority', 'wait_for_approval'], }], input: 'Set ticket T-1042 to priority high with action_update-ticket-priority. ' + 'If it is parked for approval, call wait_for_approval with the approvalId and report the decision.', }), }); const result = await response.json(); if (!response.ok) throw new Error(JSON.stringify(result)); for (const item of result.output ?? []) { if (item.type === 'mcp_call') { console.log(JSON.stringify({ tool: item.name, output: item.output, error: item.error })); } } ``` What happens: 1. The model calls `action_update-ticket-priority`. Anlyon records the invocation, creates an approval, and returns `Awaiting human approval` with its `apr_...` id. Nothing has been sent to the destination. 2. The model calls `wait_for_approval`, which holds the call open (60 seconds by default, 120 at most) until a reviewer decides. If it comes back still `pending`, the model calls it again. 3. Your reviewer approves in the Anlyon inbox, or with the operator key: ```bash Decide curl -sS -X POST "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/approvals/$APPROVAL_ID/approve" \ -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \ -H "Content-Type: application/json" \ -d '{"note": "Checked the ticket; priority change is correct."}' ``` 4. Anlyon executes the approved request snapshot using the credentials bound to that action. The snapshot freezes what the approver reviewed. It does not preserve a credential that has since been revoked, and destination bindings are re-checked at dispatch. 5. `wait_for_approval` returns the approval record. The model reads the decision (`approved`, `denied` or `expired`) and who made it. `payload.invocationId` names the invocation it gated: ```json { "id": "apr_...", "kind": "action", "status": "approved", "payload": { "invocationId": "inv_...", "actionName": "update-ticket-priority", "method": "PATCH" }, "decidedBy": { "type": "api_key", "id": "...", "displayName": null }, "requiredApprovals": 1, "approvedCount": 1 } ``` A denied or expired approval comes back with that status, and the gated request is never sent. ## Read the outcome The approval says who decided. The invocation says what happened when Anlyon made the request. Read it with any key that has `actions:read`: ```bash Outcome curl -sS "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/actions/invocations/$INVOCATION_ID" \ -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" ``` `data.status` is `succeeded`, `failed` or `unknown`, with `responseStatus` and the response body. The same record is in the dashboard's invocation log. ### `unknown` Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying: check the destination, then record what happened with `POST /api/v2/actions/invocations/{id}/resolve`. That call needs the `actions:resolve` scope. No key receives it unless its creator names it, and the key that made the invocation cannot resolve it. Idempotency keys are the supported way to replay an invocation safely. ## Credentials The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. The secret is bound to a destination and a placement, checked again at fire time. This covers credentials in the Anlyon vault, not a key your own process already holds. The MCP bearer itself is an Anlyon API key. OpenAI receives it on each request and does not store it. ## More than one approver To require more than one decision, set an approval policy on the action. N counts distinct **credentials** for API-key deciders and distinct **users** for dashboard and OAuth deciders: two keys held by one person are two deciders. ## The Agents API The Agents API takes the same MCP server as a tool with `transport: { type: "http", server_url, authorization }` and `connection_origin: "service"`, so OpenAI makes the HTTP connection. Put the agent key in `authorization` as `Bearer anlyon_live_...`. The hand-off is identical: the call parks in Anlyon, a reviewer decides, and Anlyon executes and records the outcome. The Agents API was in beta when this was checked on 2026-09-30. Check [OpenAI's MCP connection reference](https://developers.openai.com/api/docs/guides/agents-api/tools/mcp) for the current field names. ## How this page is checked A script runs the three code blocks on this page in CI, against the self-hosted transport and a local API fixture. --- # Windsurf Source: https://docs.anlyon.com/ai-tools/windsurf Windsurf's Cascade assistant writes code from your project context. This guide shows how to give it the rules for an Anlyon integration, so the code it writes routes production calls through actions and never carries a credential. ## Prerequisites - Windsurf editor installed - Node.js 18+ installed - Two Anlyon API keys from your [dashboard](https://console.anlyon.com): an operator key (`secrets:write`, `secrets:read`, `actions:write`) and an agent key (`actions:invoke`, `actions:read`, `approvals:read`) ## Workspace rules setup 1. **Create rules directory** ```bash mkdir -p .windsurf ``` 2. **Create rules file** Create `.windsurf/rules.md` in your project root. ## Windsurf rules for Anlyon projects Create `.windsurf/rules.md`: ````markdown # Anlyon Development Rules ## Project context This project uses Anlyon for governed agent execution: - Production calls are Anlyon actions the agent invokes by name. The agent never holds a provider credential. - Sensitive actions require a human approval before Anlyon makes the call. - Every invocation has a recorded outcome. `unknown` is reconciled, never retried blindly. ## SDK reference ### Installation ```bash npm install @anlyonhq/sdk ``` ### Two clients, two keys ```typescript import { Client } from '@anlyonhq/sdk'; // Defines actions and vaults credentials. Not the key the agent runs with. const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! }); // The agent's key. Can invoke; cannot read a credential or rewrite an action. const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! }); ``` ## Essential patterns ### Define an action (operator, once) ```typescript await ops.secrets.put('HELPDESK_TOKEN', { value: process.env.HELPDESK_TOKEN!, allowedHosts: ['api.example.com'], }); await ops.actions.create({ name: 'update-ticket-priority', method: 'PATCH', urlTemplate: 'https://api.example.com/tickets/{{input.ticketId}}', headers: { Authorization: 'Bearer {{secret:HELPDESK_TOKEN}}' }, inputSchema: { type: 'object', properties: { ticketId: { type: 'string' }, priority: { type: 'string' } }, required: ['ticketId', 'priority'], }, requiresApproval: true, }); ``` ### Invoke by name (agent) ```typescript const { data } = await anlyon.actions.invoke( 'update-ticket-priority', { ticketId: 'T-1042', priority: 'high' }, { idempotencyKey: 'ticket-T-1042-priority-high' }, ); if (data!.pendingApproval) { const decided = await anlyon.approvals.waitForDecision(data!.approvalId!); // decided.status: 'approved' | 'denied' | 'expired' | 'pending' (timed out, still decidable) } ``` ### Decide (operator key with approvals:decide) ```typescript const pending = await ops.approvals.list({ status: 'pending' }); await ops.approvals.approve('apr_...', { note: 'Checked the ticket.' }); await ops.approvals.deny('apr_...', { note: 'Wrong customer.' }); ``` ### Read outcomes ```typescript const { data: invocation } = await anlyon.actions.invocation('inv_...'); // invocation.status: 'succeeded' | 'failed' | 'unknown' | ... // 'failed' means the call did not happen. 'unknown' means it may have: reconcile, do not retry. ``` ### Webhook verification ```typescript // Next.js App Router (recommended) import { serve } from '@anlyonhq/sdk/nextjs'; export const { POST } = serve(async (request) => { const data = await request.json(); return { success: true }; }); // Express/Node import { Receiver } from '@anlyonhq/sdk'; const receiver = new Receiver({ signingSecret: process.env.ANLYON_SIGNING_SECRET!, }); const isValid = receiver.verify({ signature, body }); ``` ## Error handling ```typescript import { AuthenticationError, RateLimitError, QuotaExceededError, ValidationError, NotFoundError, } from '@anlyonhq/sdk'; try { await anlyon.actions.invoke('update-ticket-priority', { ticketId: 'T-1042', priority: 'high' }); } catch (error) { if (error instanceof RateLimitError) { // Wait and retry the request (the SDK already retries transport errors) } else if (error instanceof QuotaExceededError) { // Wait for the reset or free up room } } ``` ## Environment variables ```env ANLYON_OPERATOR_KEY=operator_key_never_in_agent_code ANLYON_AGENT_KEY=agent_key ANLYON_SIGNING_SECRET=your_signing_secret ``` ## Best practices 1. Production calls go through actions. Never call a provider API with a credential from agent code. 2. Reference credentials as `{{secret:NAME}}` in the action definition. Never print or log one. 3. Set an idempotency key on every invocation that must not run twice. 4. Treat `unknown` as "may have happened". Reconcile with the destination before anyone retries. 5. Use a staging key while building. It cannot reach production data. 6. Always verify webhook signatures. ## API base URL - Production: https://api.anlyon.com - Docs for agents: https://docs.anlyon.com/llms.txt ```` ## Using Cascade for Anlyon development Windsurf's Cascade understands your project context from the rules file. Here are common tasks: ### Defining an action from an existing API call **Ask Cascade:** ``` This service calls the helpdesk API directly with HELPDESK_TOKEN. Turn that call into an Anlyon action defined with the operator client, then replace the direct call with actions.invoke from the agent client. ``` **Expected output:** ```typescript // setup/actions.ts (operator, run once) import { Client } from '@anlyonhq/sdk'; const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! }); await ops.secrets.put('HELPDESK_TOKEN', { value: process.env.HELPDESK_TOKEN!, allowedHosts: ['api.example.com'], }); await ops.actions.create({ name: 'update-ticket-priority', method: 'PATCH', urlTemplate: 'https://api.example.com/tickets/{{input.ticketId}}', headers: { Authorization: 'Bearer {{secret:HELPDESK_TOKEN}}' }, inputSchema: { type: 'object', properties: { ticketId: { type: 'string' }, priority: { type: 'string' } }, required: ['ticketId', 'priority'], }, requiresApproval: true, }); // agent/tickets.ts const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! }); export async function raisePriority(ticketId: string) { const { data } = await anlyon.actions.invoke( 'update-ticket-priority', { ticketId, priority: 'high' }, { idempotencyKey: `priority-${ticketId}` }, ); return data!.pendingApproval ? { parked: data!.approvalId } : { status: data!.responseStatus }; } ``` ### Waiting for approval **Ask Cascade:** ``` After invoking a gated action, wait up to ten minutes for the decision with approvals.waitForDecision and handle approved, denied, expired and still-pending separately ``` ### Building a reviewer tool **Ask Cascade:** ``` Write a small CLI that lists pending Anlyon approvals with the operator key and approves or denies one by id with a note ``` ### Reconciling unknown outcomes **Ask Cascade:** ``` Write a job that lists Anlyon invocations with status unknown, checks each one against the destination, and records the outcome with actions.resolveInvocation ``` ## Best practices with Cascade **Provide context** Mention "using Anlyon actions" when asking for code that calls a production API **Be specific** Say which client (operator or agent) the code runs with, so the right key is used **Request verification** Always ask for webhook signature verification in handler code **Include error handling** Request proper error handling with SDK typed errors, and separate `failed` from `unknown` ## Resources - [Anlyon SDK](https://www.npmjs.com/package/@anlyonhq/sdk): the official TypeScript SDK on npm - [TypeScript SDK Guide](/guides/typescript-sdk): Complete SDK documentation - [Actions](/execution/actions): Templates, input schemas, versions and gates --- # Anlyon API Source: https://docs.anlyon.com/api-reference Anlyon is the production execution runtime for AI agents. An agent invokes a named action; Anlyon resolves the action and its version, checks the caller's scopes, evaluates policy, requests human approval when required, resolves credentials at dispatch, makes the production call itself, and records the outcome. These guarantees cover actions routed through the hosted executor. A tool the caller's own code invokes directly, with a credential that process holds, is not routed through Anlyon and is not governed by it. ## Authentication Developer API endpoints use a workspace API key as a bearer token: ``` Authorization: Bearer anlyon_your_api_key ``` ## API-key budget admission Budgets reserve supported logical operations in Postgres with integer sub-cent cost accounting. They cover messages, event publishes, cache lookups, memory operations and action invocations; external provider bills and session callers are outside these caps. UTC calendar months own the reservations. Batches reserve all planned items before execution; partial failures retain that amount. Memory ingest reserves one job admission, not each generated chunk or token. Approval-pending actions reserve one invocation admission even before dispatch. These conservative admission estimates are distinct from invoice metering. 429 BUDGET_EXCEEDED refuses the whole admission. 503 BUDGET_RECONCILIATION_REQUIRED pauses keys whose legacy usage is unknown. A cached Idempotency-Key success replays normally. If that cache is lost or expires but a durable budget reservation exists, 409 BUDGET_EFFECT_ALREADY_RESERVED prevents executing the effect again. Investigate the original outcome before choosing a new key; a new key can duplicate work. Errors after admission keep their reservations until an operator proves no work occurred. Disabling or deleting a budget does not erase recorded usage. ## Delivery signature environment attribution Configured message and event-subscription signers emit legacy v1 plus signed env/v2 attribution. v2 is HMAC-SHA256 over the exact UTF-8 string `anlyon-delivery-v2.${timestamp}.${environmentId}.${rawBody}`. Receivers must configure the expected environment independently; v1 alone provides no environment boundary. Signing keys remain workspace-wide unless a subscription uses its own secret. X-Anlyon-Environment-Id is reserved and informational, not authority. Legacy queued work without recorded scope has no v2 claim. See /verification for coverage, raw-body verification and staged migration. ## Rate Limiting Rate limits are based on your workspace plan: - **Free Plan**: 300 requests/minute, 10,000 requests/hour - **Pay as You Go**: Unlimited - **Enterprise**: Custom limits ## Error Handling All errors follow a consistent format: ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable error message", "details": {} } } ``` Common error codes: - `AUTHENTICATION_ERROR` - Invalid or missing API key - `RATE_LIMIT_EXCEEDED` - Too many requests - `QUOTA_EXCEEDED` - Usage quota exceeded - `VALIDATION_ERROR` - Invalid request data - `NOT_FOUND` - Resource not found - `FEATURE_NOT_AVAILABLE` - Feature not available on your plan Version 2.0.0 Base URL: `https://api.anlyon.com`, `http://localhost:4000` ## Messages Publish and manage scheduled messages - [`POST /api/v2/messages/publish`](/api-reference/messages/publish-message) — Publish a single message. - [`POST /api/v2/messages/batch`](/api-reference/messages/publish-batch) — Publish multiple messages in batch. - [`GET /api/v2/messages`](/api-reference/messages/list-messages) — List messages. - [`GET /api/v2/messages/{messageId}`](/api-reference/messages/get-message) — Get message status. - [`DELETE /api/v2/messages/{messageId}`](/api-reference/messages/cancel-message) — Cancel a scheduled message. ## Schedules Create and manage recurring schedules (cron jobs) - [`GET /api/v2/schedules`](/api-reference/schedules/list-schedules) — List all schedules. - [`POST /api/v2/schedules`](/api-reference/schedules/create-schedule) — Create a new schedule. - [`GET /api/v2/schedules/{scheduleId}`](/api-reference/schedules/get-schedule) — Get a specific schedule. - [`DELETE /api/v2/schedules/{scheduleId}`](/api-reference/schedules/delete-schedule) — Delete a schedule. - [`PATCH /api/v2/schedules/{scheduleId}`](/api-reference/schedules/update-schedule) — Update a schedule. - [`POST /api/v2/schedules/{scheduleId}/pause`](/api-reference/schedules/pause-schedule) — Pause a schedule. - [`POST /api/v2/schedules/{scheduleId}/resume`](/api-reference/schedules/resume-schedule) — Resume a paused schedule. ## URL Groups Manage URL groups for broadcasting messages - [`GET /api/v2/url-groups`](/api-reference/url-groups/list-url-groups) — List URL groups. - [`POST /api/v2/url-groups`](/api-reference/url-groups/create-url-group) — Create a URL group. - [`GET /api/v2/url-groups/{groupId}`](/api-reference/url-groups/get-url-group) — Get a URL group. - [`DELETE /api/v2/url-groups/{groupId}`](/api-reference/url-groups/delete-url-group) — Delete a URL group. - [`PATCH /api/v2/url-groups/{groupId}`](/api-reference/url-groups/update-url-group) — Update a URL group. - [`POST /api/v2/url-groups/{groupId}/endpoints`](/api-reference/url-groups/add-endpoint) — Add an endpoint to a URL group. - [`DELETE /api/v2/url-groups/{groupId}/endpoints/{endpointId}`](/api-reference/url-groups/remove-endpoint) — Remove an endpoint from a URL group. - [`POST /api/v2/url-groups/{groupId}/publish`](/api-reference/url-groups/publish-to-group) — Publish to all endpoints in a group. ## Analytics View analytics and usage statistics - [`GET /api/v2/analytics/overview`](/api-reference/analytics/get-analytics-overview) — Get cross-product analytics overview. - [`GET /api/v2/analytics/agents`](/api-reference/analytics/get-agent-analytics) — Get agent analytics. - [`GET /api/v2/analytics/trust-control`](/api-reference/analytics/get-trust-control-analytics) — Get trust and control analytics. - [`GET /api/v2/analytics/delivery`](/api-reference/analytics/get-delivery-analytics) — Get delivery analytics. - [`GET /api/v2/analytics/messages`](/api-reference/analytics/get-message-analytics) — Get message analytics. - [`GET /api/v2/analytics/time-series`](/api-reference/analytics/get-message-time-series) — Get message time series data. - [`GET /api/v2/analytics/top-urls`](/api-reference/analytics/get-top-urls) — Get top destination URLs. - [`GET /api/v2/analytics/usage/current`](/api-reference/analytics/get-current-usage) — Get current usage. - [`GET /api/v2/analytics/usage/history`](/api-reference/analytics/get-usage-history) — Get usage history. - [`GET /api/v2/analytics/schedules`](/api-reference/analytics/get-schedule-analytics) — Get schedule statistics. - [`GET /api/v2/analytics/schedule-executions`](/api-reference/analytics/list-recent-schedule-executions) — List recent schedule execution summaries. - [`GET /api/v2/analytics/errors`](/api-reference/analytics/get-error-distribution) — Get failed-message error distribution. - [`GET /api/v2/analytics/onboarding`](/api-reference/analytics/get-onboarding-status) — Get workspace onboarding progress. ## Billing Manage billing, plans, and usage - [`GET /api/v2/billing/budget`](/api-reference/billing/get-workspace-billing-budget) — Read workspace overage alert target. - [`GET /api/v2/billing/plan`](/api-reference/billing/get-current-plan) — Get current plan details. - [`GET /api/v2/billing/usage/current`](/api-reference/billing/get-current-billing-usage) — Get current billing period usage. - [`GET /api/v2/billing/plans`](/api-reference/billing/list-plans) — List available plans. - [`GET /api/v2/billing/quota`](/api-reference/billing/get-billing-quota) — Get current quota utilization. - [`GET /api/v2/billing/usage/history`](/api-reference/billing/get-billing-usage-history) — Get daily billing usage history. - [`GET /api/v2/billing/cost-estimate`](/api-reference/billing/get-billing-cost-estimate) — Estimate current-month cost. - [`GET /api/v2/billing/invoices`](/api-reference/billing/list-billing-invoices) — List workspace invoices. - [`GET /api/v2/billing/stripe-config`](/api-reference/billing/get-billing-stripe-config) — Get browser-safe Stripe configuration. ## Notifications Manage notifications and preferences - [`GET /api/v2/notifications`](/api-reference/notifications/list-notifications) — List notifications. - [`PATCH /api/v2/notifications/{id}/read`](/api-reference/notifications/mark-notification-as-read) — Mark notification as read. - [`PATCH /api/v2/notifications/read-all`](/api-reference/notifications/mark-all-notifications-as-read) — Mark all notifications as read. - [`GET /api/v2/notifications/preferences`](/api-reference/notifications/get-notification-preferences) — Get notification preferences. - [`PATCH /api/v2/notifications/preferences`](/api-reference/notifications/update-notification-preferences) — Update notification preferences. - [`GET /api/v2/notifications/unread-count`](/api-reference/notifications/get-unread-notification-count) — Get unread notification count. - [`DELETE /api/v2/notifications/{id}`](/api-reference/notifications/delete-notification) — Dismiss a notification. - [`GET /api/v2/notifications/preferences/me`](/api-reference/notifications/get-my-notification-preferences) — Get your own notification preferences. - [`PATCH /api/v2/notifications/preferences/me`](/api-reference/notifications/update-my-notification-preferences) — Update your own notification preferences. - [`POST /api/v2/notifications/preferences/test-webhook`](/api-reference/notifications/test-notification-webhook) — Send a test notification webhook. ## Sessions Persist agent conversation threads and prompt-ready history - [`GET /api/v2/sessions`](/api-reference/sessions/list-sessions) — List sessions. - [`POST /api/v2/sessions`](/api-reference/sessions/create-session) — Create a session. - [`GET /api/v2/sessions/{id}`](/api-reference/sessions/get-session) — Get a session. - [`DELETE /api/v2/sessions/{id}`](/api-reference/sessions/delete-session) — Delete a session. - [`PATCH /api/v2/sessions/{id}`](/api-reference/sessions/update-session) — Update a session. - [`GET /api/v2/sessions/{id}/messages`](/api-reference/sessions/list-session-messages) — List session messages. - [`POST /api/v2/sessions/{id}/messages`](/api-reference/sessions/append-session-messages) — Append session messages. - [`DELETE /api/v2/sessions/{id}/messages`](/api-reference/sessions/clear-session-messages) — Clear session messages. - [`DELETE /api/v2/sessions/{id}/messages/last`](/api-reference/sessions/pop-session-message) — Pop the newest session message. - [`GET /api/v2/sessions/{id}/context`](/api-reference/sessions/get-session-context) — Get prompt-ready session context. ## Context Assemble memory and session state into prompt-ready context - [`POST /api/v2/context`](/api-reference/context/assemble-context) — Assemble prompt-ready agent context. ## Actions Define and invoke secret-backed hosted tools - [`GET /api/v2/actions`](/api-reference/actions/list-actions) — List actions. - [`POST /api/v2/actions`](/api-reference/actions/create-action) — Create an action. - [`POST /api/v2/actions/import`](/api-reference/actions/import-actions) — Import actions from OpenAPI. - [`GET /api/v2/actions/invocations`](/api-reference/actions/list-action-invocations) — List action invocations. - [`GET /api/v2/actions/invocations/{id}`](/api-reference/actions/get-action-invocation) — Get an action invocation. - [`GET /api/v2/actions/{ref}`](/api-reference/actions/get-action) — Get an action. - [`DELETE /api/v2/actions/{ref}`](/api-reference/actions/delete-action) — Delete an action. - [`PATCH /api/v2/actions/{ref}`](/api-reference/actions/update-action) — Update an action. - [`POST /api/v2/actions/{ref}/invoke`](/api-reference/actions/invoke-action) — Invoke an action. - [`GET /api/v2/actions/{ref}/versions`](/api-reference/actions/list-action-versions) — List one action's immutable versions. - [`GET /api/v2/actions/effects`](/api-reference/actions/list-action-effects) — List governed effects. - [`GET /api/v2/actions/effects/{id}`](/api-reference/actions/get-action-effect) — Get an effect with its evidence. - [`POST /api/v2/actions/effects/{id}/reconcile`](/api-reference/actions/reconcile-action-effect) — Reconcile an effect now. - [`POST /api/v2/actions/effects/{id}/notes`](/api-reference/actions/add-action-effect-note) — Add an investigation note. - [`POST /api/v2/actions/effects/{id}/resolve`](/api-reference/actions/resolve-action-effect) — Record a manual resolution. - [`POST /api/v2/actions/effects/{id}/replay`](/api-reference/actions/replay-action-effect) — Replay a lost write under the same identity. - [`GET /api/v2/actions/previews/{id}`](/api-reference/actions/get-action-preview) — Get a preview. - [`POST /api/v2/actions/previews/{id}/refresh`](/api-reference/actions/refresh-action-preview) — Refresh a preview's evidence. - [`POST /api/v2/actions/{ref}/previews`](/api-reference/actions/create-action-preview) — Preview a governed invocation. - [`POST /api/v2/actions/invocations/{id}/resolve`](/api-reference/actions/resolve-action-invocation) — Resolve an unknown invocation. - [`GET /api/v2/actions/{ref}/invocations`](/api-reference/actions/list-invocations-for-action) — List one action's invocations. ## Impact Limits Shared business-impact limits (money, resource mutations) that governed effects reserve before dispatch. Separate from Budgets, which cap Anlyon usage. - [`GET /api/v2/impact-limits`](/api-reference/impact-limits/list-impact-limits) — List impact limits. - [`POST /api/v2/impact-limits`](/api-reference/impact-limits/create-impact-limit) — Create an impact limit. - [`GET /api/v2/impact-limits/{id}`](/api-reference/impact-limits/get-impact-limit) — Get an impact limit. - [`DELETE /api/v2/impact-limits/{id}`](/api-reference/impact-limits/archive-impact-limit) — Archive an impact limit. - [`PATCH /api/v2/impact-limits/{id}`](/api-reference/impact-limits/update-impact-limit) — Update an impact limit. ## Approval Policies Configure the environment-scoped rules that decide whether a request needs approval, is auto-approved, or is auto-denied. Operator credentials only. - [`POST /api/v2/approval-policies/evaluate`](/api-reference/approval-policies/evaluate-approval-policies) — Evaluate current policies without executing. - [`GET /api/v2/approval-policies`](/api-reference/approval-policies/list-environment-approval-policies) — List approval policies. - [`POST /api/v2/approval-policies`](/api-reference/approval-policies/create-environment-approval-policy) — Create an approval policy. - [`POST /api/v2/approval-policies/files/plan`](/api-reference/approval-policies/plan-approval-policy-file) — Plan a policy file. - [`POST /api/v2/approval-policies/files/apply`](/api-reference/approval-policies/apply-approval-policy-file) — Apply a policy file. - [`POST /api/v2/approval-policies/files/test`](/api-reference/approval-policies/test-approval-policy-file) — Test a policy file against fixture requests. - [`GET /api/v2/approval-policies/{policyId}/versions`](/api-reference/approval-policies/list-approval-policy-versions) — Read immutable policy history. - [`GET /api/v2/approval-policies/{policyId}/versions/{version}`](/api-reference/approval-policies/get-approval-policy-version) — Read one policy version. - [`GET /api/v2/approval-policies/{policyId}`](/api-reference/approval-policies/get-environment-approval-policy) — Get an approval policy. - [`DELETE /api/v2/approval-policies/{policyId}`](/api-reference/approval-policies/delete-environment-approval-policy) — Delete an approval policy. - [`PATCH /api/v2/approval-policies/{policyId}`](/api-reference/approval-policies/update-environment-approval-policy) — Update an approval policy. ## Authentication Inspect the credential making the request - [`GET /api/v2/auth/identity`](/api-reference/authentication/get-credential-identity) — Inspect the current credential. ## Approvals Create and decide human-in-the-loop safety gates - [`GET /api/v2/approvals`](/api-reference/approvals/list-approvals) — List approvals. - [`POST /api/v2/approvals`](/api-reference/approvals/create-approval) — Request an approval. - [`GET /api/v2/approvals/{id}`](/api-reference/approvals/get-approval) — Get or wait for an approval. - [`POST /api/v2/approvals/{id}/approve`](/api-reference/approvals/approve-approval) — Approve a request. - [`POST /api/v2/approvals/{id}/deny`](/api-reference/approvals/deny-approval) — Deny a request. ## Budgets Control per-API-key usage and estimated spend - [`GET /api/v2/budgets`](/api-reference/budgets/list-budgets) — List API-key budgets. - [`GET /api/v2/budgets/{apiKeyId}`](/api-reference/budgets/get-budget) — Get an API-key budget. - [`PUT /api/v2/budgets/{apiKeyId}`](/api-reference/budgets/put-budget) — Create or replace an API-key budget. - [`DELETE /api/v2/budgets/{apiKeyId}`](/api-reference/budgets/delete-budget) — Delete an API-key budget. ## Secrets Store write-only credentials used by hosted actions - [`GET /api/v2/secrets`](/api-reference/secrets/list-secrets) — List secret metadata. - [`GET /api/v2/secrets/{name}`](/api-reference/secrets/get-secret) — Get secret metadata. - [`PUT /api/v2/secrets/{name}`](/api-reference/secrets/put-secret) — Create or rotate a secret. - [`DELETE /api/v2/secrets/{name}`](/api-reference/secrets/delete-secret) — Delete a secret. ## Files Upload, inspect, download, and delete workspace files - [`GET /api/v2/files`](/api-reference/files/list-files) — List files. - [`POST /api/v2/files`](/api-reference/files/upload-file-direct) — Upload a file directly. - [`POST /api/v2/files/presign`](/api-reference/files/presign-file-upload) — Create a presigned file upload. - [`GET /api/v2/files/{fileId}`](/api-reference/files/get-file) — Get file metadata. - [`DELETE /api/v2/files/{fileId}`](/api-reference/files/delete-file) — Delete a file. - [`POST /api/v2/files/{fileId}/confirm`](/api-reference/files/confirm-file-upload) — Confirm a presigned upload. - [`GET /api/v2/files/{fileId}/download`](/api-reference/files/download-file) — Download a file. ## Cache Store and semantically retrieve reusable model results - [`GET /api/v2/cache`](/api-reference/cache/list-cache-entries) — List semantic cache entries. - [`POST /api/v2/cache`](/api-reference/cache/store-cache-entry) — Store a semantic cache entry. - [`POST /api/v2/cache/lookup`](/api-reference/cache/lookup-cache) — Look up a semantic cache entry. - [`POST /api/v2/cache/flush`](/api-reference/cache/flush-cache) — Flush semantic cache entries. - [`GET /api/v2/cache/stats`](/api-reference/cache/get-cache-stats) — Get semantic cache statistics. - [`GET /api/v2/cache/{entryId}`](/api-reference/cache/get-cache-entry) — Get a semantic cache entry. - [`DELETE /api/v2/cache/{entryId}`](/api-reference/cache/delete-cache-entry) — Delete a semantic cache entry. ## Events Publish typed events and deliver them to filtered webhook subscriptions - [`GET /api/v2/events/topics`](/api-reference/events/list-event-topics) — List event topics. - [`POST /api/v2/events/topics`](/api-reference/events/create-event-topic) — Create an event topic. - [`GET /api/v2/events/topics/{ref}`](/api-reference/events/get-event-topic) — Get an event topic. - [`DELETE /api/v2/events/topics/{ref}`](/api-reference/events/delete-event-topic) — Delete an event topic. - [`PATCH /api/v2/events/topics/{ref}`](/api-reference/events/update-event-topic) — Update an event topic. - [`GET /api/v2/events/topics/{ref}/subscriptions`](/api-reference/events/list-event-subscriptions) — List topic subscriptions. - [`POST /api/v2/events/topics/{ref}/subscriptions`](/api-reference/events/create-event-subscription) — Create a webhook subscription. - [`DELETE /api/v2/events/subscriptions/{subscriptionId}`](/api-reference/events/delete-event-subscription) — Delete a webhook subscription. - [`PATCH /api/v2/events/subscriptions/{subscriptionId}`](/api-reference/events/update-event-subscription) — Update a webhook subscription. - [`POST /api/v2/events/subscriptions/{subscriptionId}/rotate-secret`](/api-reference/events/rotate-event-subscription-secret) — Rotate a subscription signing secret. - [`GET /api/v2/events`](/api-reference/events/list-events) — List published events. - [`POST /api/v2/events`](/api-reference/events/publish-event) — Publish an event. - [`GET /api/v2/events/{eventId}`](/api-reference/events/get-event) — Get an event and its deliveries. ## DLQ Inspect, retry, and permanently remove exhausted message deliveries - [`GET /api/v2/dlq`](/api-reference/dlq/list-dlq-messages) — List failed messages. - [`DELETE /api/v2/dlq`](/api-reference/dlq/clear-dlq) — Permanently clear the DLQ. - [`GET /api/v2/dlq/stats`](/api-reference/dlq/get-dlq-stats) — Get DLQ statistics. - [`POST /api/v2/dlq/retry-all`](/api-reference/dlq/retry-all-dlq-messages) — Retry failed messages. - [`GET /api/v2/dlq/{messageId}`](/api-reference/dlq/get-dlq-message) — Get a failed message. - [`DELETE /api/v2/dlq/{messageId}`](/api-reference/dlq/delete-dlq-message) — Permanently delete a failed message. - [`POST /api/v2/dlq/{messageId}/retry`](/api-reference/dlq/retry-dlq-message) — Retry a failed message. ## Flow Control Coordinate per-key rate limits, parallelism, pausing, and delivery waitlists - [`GET /api/v2/flow-control`](/api-reference/flow-control/list-flow-control-keys) — List flow-control keys. - [`POST /api/v2/flow-control`](/api-reference/flow-control/create-flow-control-key) — Create a flow-control key. - [`GET /api/v2/flow-control/{flowControlKeyId}`](/api-reference/flow-control/get-flow-control-key) — Get a flow-control key. - [`DELETE /api/v2/flow-control/{flowControlKeyId}`](/api-reference/flow-control/delete-flow-control-key) — Delete a flow-control key. - [`PATCH /api/v2/flow-control/{flowControlKeyId}`](/api-reference/flow-control/update-flow-control-key) — Update a flow-control key. - [`POST /api/v2/flow-control/{flowControlKeyId}/pause`](/api-reference/flow-control/pause-flow-control-key) — Pause waitlist release. - [`POST /api/v2/flow-control/{flowControlKeyId}/resume`](/api-reference/flow-control/resume-flow-control-key) — Resume waitlist release. - [`POST /api/v2/flow-control/{flowControlKeyId}/pin`](/api-reference/flow-control/pin-flow-control-key) — Pin flow-control configuration. - [`POST /api/v2/flow-control/{flowControlKeyId}/unpin`](/api-reference/flow-control/unpin-flow-control-key) — Unpin flow-control configuration. - [`POST /api/v2/flow-control/{flowControlKeyId}/reset-rate`](/api-reference/flow-control/reset-flow-control-rate) — Reset the current rate counter. - [`GET /api/v2/flow-control/{flowControlKeyId}/status`](/api-reference/flow-control/get-flow-control-status) — Get live flow-control status. ## Integrations Connect Slack and Discord notification destinations without exposing stored credentials - [`GET /api/v2/integrations`](/api-reference/integrations/list-integrations) — List workspace integrations. - [`POST /api/v2/integrations/discord`](/api-reference/integrations/create-discord-integration) — Create a Discord webhook integration. - [`POST /api/v2/integrations/discord/{id}/test`](/api-reference/integrations/test-discord-integration) — Send a Discord test notification. - [`GET /api/v2/integrations/slack/{id}/channels`](/api-reference/integrations/list-slack-integration-channels) — List Slack channels. - [`PATCH /api/v2/integrations/slack/{id}/channel`](/api-reference/integrations/update-slack-integration-channel) — Select a Slack channel. - [`POST /api/v2/integrations/slack/{id}/test`](/api-reference/integrations/test-slack-integration) — Send a Slack test notification. - [`DELETE /api/v2/integrations/{id}`](/api-reference/integrations/delete-integration) — Delete an integration. - [`PATCH /api/v2/integrations/{id}/toggle`](/api-reference/integrations/toggle-integration) — Enable or disable an integration. - [`PATCH /api/v2/integrations/{id}/events`](/api-reference/integrations/update-integration-event-types) — Update integration event types. ## Logs Query, inspect, and export redacted message and schedule execution history - [`GET /api/v2/logs`](/api-reference/logs/list-execution-logs) — Query unified execution logs. - [`GET /api/v2/logs/stats`](/api-reference/logs/get-execution-log-stats) — Get detailed execution-log statistics. - [`GET /api/v2/logs/export`](/api-reference/logs/export-execution-logs) — Export up to 10,000 execution logs. - [`GET /api/v2/messages/{messageId}/logs`](/api-reference/logs/list-message-execution-logs) — List execution logs for a message. - [`GET /api/v2/schedules/{scheduleId}/logs`](/api-reference/logs/list-schedule-execution-logs) — List execution logs for a schedule. ## Queues Configure isolated message streams with parallelism, pause/resume controls, and live lag - [`GET /api/v2/queues`](/api-reference/queues/list-queues) — List queues. - [`POST /api/v2/queues`](/api-reference/queues/create-queue) — Create a queue. - [`GET /api/v2/queues/{queueId}`](/api-reference/queues/get-queue) — Get a queue. - [`DELETE /api/v2/queues/{queueId}`](/api-reference/queues/delete-queue) — Soft-delete a queue. - [`PATCH /api/v2/queues/{queueId}`](/api-reference/queues/update-queue) — Update a queue. - [`POST /api/v2/queues/{queueId}/pause`](/api-reference/queues/pause-queue) — Pause a queue. - [`POST /api/v2/queues/{queueId}/resume`](/api-reference/queues/resume-queue) — Resume a queue. ## Workflows Define callback-driven workflows, trigger runs, inspect steps, and resume event waits - [`GET /api/v2/workflows`](/api-reference/workflows/list-workflows) — List workflows. - [`POST /api/v2/workflows`](/api-reference/workflows/create-workflow) — Create a workflow. - [`GET /api/v2/workflows/runs`](/api-reference/workflows/list-workflow-runs) — List workflow runs. - [`GET /api/v2/workflows/runs/{runId}`](/api-reference/workflows/get-workflow-run) — Get a workflow run. - [`GET /api/v2/workflows/runs/{runId}/steps`](/api-reference/workflows/list-workflow-run-steps) — List steps for a workflow run. - [`POST /api/v2/workflows/runs/{runId}/cancel`](/api-reference/workflows/cancel-workflow-run) — Cancel a workflow run. - [`POST /api/v2/workflows/events/notify`](/api-reference/workflows/notify-workflow-event) — Notify workflows waiting for an event. - [`GET /api/v2/workflows/{workflowId}`](/api-reference/workflows/get-workflow) — Get a workflow. - [`DELETE /api/v2/workflows/{workflowId}`](/api-reference/workflows/delete-workflow) — Delete a workflow and its runs. - [`PATCH /api/v2/workflows/{workflowId}`](/api-reference/workflows/update-workflow) — Update a workflow. - [`POST /api/v2/workflows/{workflowId}/trigger`](/api-reference/workflows/trigger-workflow) — Trigger a workflow run. ## Memory - [`GET /api/v2/memory`](/api-reference/memory/list-memories) — List memories. - [`POST /api/v2/memory`](/api-reference/memory/remember) — Remember (store a memory). - [`POST /api/v2/memory/batch`](/api-reference/memory/remember-many) — Batch remember (v1.1). - [`POST /api/v2/memory/recall`](/api-reference/memory/recall) — Recall (semantic/hybrid search). - [`POST /api/v2/memory/forget`](/api-reference/memory/forget-batch) — Batch forget by ids or documentId. - [`POST /api/v2/memory/ingest`](/api-reference/memory/ingest) — Ingest a document (async). - [`POST /api/v2/memory/files`](/api-reference/memory/upload-file) — Upload a file to R2 (v1.1). - [`POST /api/v2/memory/summarize`](/api-reference/memory/summarize) — Summarize memories (sync, v1.1). - [`POST /api/v2/memory/consolidate`](/api-reference/memory/consolidate) — Consolidate memories (async, v1.1). - [`GET /api/v2/memory/jobs/{jobId}`](/api-reference/memory/get-job) — Get job status. - [`GET /api/v2/memory/{memoryId}`](/api-reference/memory/get-memory) — Get a single memory. - [`DELETE /api/v2/memory/{memoryId}`](/api-reference/memory/forget) — Forget a single memory. - [`GET /api/v2/memory/collections`](/api-reference/memory/list-collections) — List collections. - [`POST /api/v2/memory/collections`](/api-reference/memory/create-collection) — Create a collection. - [`GET /api/v2/memory/collections/{collectionId}`](/api-reference/memory/get-collection) — Get a collection. - [`DELETE /api/v2/memory/collections/{collectionId}`](/api-reference/memory/delete-collection) — Delete a collection (cascades to memories). - [`PATCH /api/v2/memory/collections/{collectionId}`](/api-reference/memory/update-collection) — Update a collection. - [`POST /api/v2/memory/collections/{collectionId}/migrate`](/api-reference/memory/migrate-collection) — Migrate collection to new embedding model (v1.1). ## Stream - [`GET /api/v2/stream`](/api-reference/stream/stream-events) — Subscribe to the realtime event stream. ## Runs - [`GET /api/v2/runs`](/api-reference/runs/list-runs) — List runs. - [`POST /api/v2/runs`](/api-reference/runs/start-run) — Start a run. - [`POST /api/v2/runs/import`](/api-reference/runs/import-run) — Import an OTLP/JSON trace. - [`GET /api/v2/runs/{runId}`](/api-reference/runs/get-run) — Get a run with its span tree. - [`PATCH /api/v2/runs/{runId}`](/api-reference/runs/end-run) — End or update a run. - [`GET /api/v2/runs/{runId}/export`](/api-reference/runs/export-run) — Export a run as OTLP/JSON. - [`GET /api/v2/runs/{runId}/feedback`](/api-reference/runs/list-run-feedback) — List feedback for a run. - [`POST /api/v2/runs/{runId}/feedback`](/api-reference/runs/add-run-feedback) — Attach feedback to a run. - [`POST /api/v2/runs/{runId}/spans`](/api-reference/runs/ingest-spans) — Ingest spans. ## Environments - [`GET /api/v2/environment`](/api-reference/environments/get-own-environment) — Get this key's environment. - [`POST /api/v2/environment/halt`](/api-reference/environments/halt-own-environment) — Halt this key's environment. - [`POST /api/v2/environment/resume`](/api-reference/environments/resume-own-environment) — Resume this key's environment. --- # Add an investigation note Source: https://docs.anlyon.com/api-reference/actions/add-action-effect-note Append an operator's note to the effect's evidence history. Changes no outcome. Needs `actions:resolve` or a workspace admin. `POST /api/v2/actions/effects/{id}/notes` --- # Create an action Source: https://docs.anlyon.com/api-reference/actions/create-action Define a secret-backed, SSRF-protected hosted HTTP tool. `POST /api/v2/actions` --- # Preview a governed invocation Source: https://docs.anlyon.com/api-reference/actions/create-action-preview Normalize the input, resolve the exact target, read the provider (never write), and return a typed, execution-bound preview: environment and target, the exact change (field summaries and text diffs), expected impact against current impact limits, material preconditions with the time they were observed, expiry, and the adapter's guarantees and limitations. Pass its id as `previewId` when invoking; any material difference is refused. For a declared action the preview is the normalized request itself, with every secret still a `{{secret:NAME}}` reference. Anlyon does not read an API it has no adapter for before the write, and the preview lists what a declared action does not guarantee. `POST /api/v2/actions/{ref}/previews` --- # Delete an action Source: https://docs.anlyon.com/api-reference/actions/delete-action Archive an action. It stops resolving and its name becomes free to reuse, while its immutable versions and its invocation history are retained -- an agent version may pin one of those versions, and destroying them would dismantle rollback and erase the audit trail in the same call. Archiving an action that currently requires approval additionally needs `actions:govern`, because removing a gate is removing a gate whichever verb gets you there. `DELETE /api/v2/actions/{ref}` --- # Get an action Source: https://docs.anlyon.com/api-reference/actions/get-action `GET /api/v2/actions/{ref}` --- # Get an effect with its evidence Source: https://docs.anlyon.com/api-reference/actions/get-action-effect The effect with every attempt, the append-only evidence history (source, observation time, provider identifiers, redacted details, actor), its impact reservations, and the preview its approval was bound to. Scoped to the caller's environment: another environment's effect id is not found. `GET /api/v2/actions/effects/{id}` --- # Get an action invocation Source: https://docs.anlyon.com/api-reference/actions/get-action-invocation `GET /api/v2/actions/invocations/{id}` --- # Get a preview Source: https://docs.anlyon.com/api-reference/actions/get-action-preview `GET /api/v2/actions/previews/{id}` --- # Import actions from OpenAPI Source: https://docs.anlyon.com/api-reference/actions/import-actions Translate supported OpenAPI 3.x operations into hosted actions, reporting every skipped operation and translation warning. A dry run creates nothing. `POST /api/v2/actions/import` --- # Invoke an action Source: https://docs.anlyon.com/api-reference/actions/invoke-action Execute an enabled action inline, or park it behind a human approval when the action is gated. Which definition runs depends on how the call is attributed. Passing a `runId` whose run names an agent runs the action version that agent's published version pins, if it pins one -- this is the path a rollback restores. Without a `runId`, or when the agent pins nothing for this action, the action's current definition runs. The response reports the version that actually executed as `actionVersionId`. With an `Idempotency-Key`, the key is also recorded on the invocation and kept for its whole life, including after it fails. A retry with the same key after the 24-hour replay window (or after the replay store loses the key) is answered from the invocation's current state with `X-Idempotent-Replay: current-state` and `200`, or `202` while it still waits for approval. It is never executed, admitted or charged again. The same key from a different credential or with a different body returns `409 idempotency_key_reuse`. A request refused before the invocation is recorded (for example invalid input) leaves the key unused. To run the action again deliberately, use a new key. `POST /api/v2/actions/{ref}/invoke` --- # List governed effects Source: https://docs.anlyon.com/api-reference/actions/list-action-effects Effects are the business operations governed actions perform: one per operation, however many attempts it took. A governed action is one with an adapter or one that carries a declaration. Each effect carries its execution `stage`, its business `outcome` (`pending`, `succeeded`, `failed`, `unknown`), whether a failure may have had partial impact, who established the outcome (`verification`: the provider's evidence, an operator's manual resolution, or nobody yet), and its receipt `grade`. Filter by `unresolved=true` for everything still holding impact-limit capacity, or by `grade`. `GET /api/v2/actions/effects` --- # List action invocations Source: https://docs.anlyon.com/api-reference/actions/list-action-invocations `GET /api/v2/actions/invocations` --- # List one action's immutable versions Source: https://docs.anlyon.com/api-reference/actions/list-action-versions Every definition this action has had, oldest first. Editing an action appends a version; nothing updates one. An agent version pins a version id in its `config.actions`, which is what makes rolling an agent back restore the tool definitions it was published with rather than whatever those tools have since been edited to. `GET /api/v2/actions/{ref}/versions` --- # List actions Source: https://docs.anlyon.com/api-reference/actions/list-actions List every hosted tool available in the workspace. `GET /api/v2/actions` --- # List one action's invocations Source: https://docs.anlyon.com/api-reference/actions/list-invocations-for-action `GET /api/v2/actions/{ref}/invocations` --- # Reconcile an effect now Source: https://docs.anlyon.com/api-reference/actions/reconcile-action-effect Ask the provider, read-only, what happened to a `pending` or `unknown` effect, and record the answer as evidence. Never writes to the provider. For a declared action this repeats the declared read-back. A read-back that finds nothing leaves the effect `unknown`. Available while the environment is halted. `409` when the effect is already final or another worker is reconciling or writing it. `POST /api/v2/actions/effects/{id}/reconcile` --- # Refresh a preview's evidence Source: https://docs.anlyon.com/api-reference/actions/refresh-action-preview Re-read the provider for the same input and the same action version this preview reviewed, and return a NEW preview that `supersedes` this one. Its binding differs, so an approval of the old preview never carries over; an effect already bound to the old one still executes only as reviewed. A refresh never moves the review to a newer action version: if the action changed since, invoking with the refreshed preview is refused with PREVIEW_MISMATCH and a new preview is needed. `POST /api/v2/actions/previews/{id}/refresh` --- # Replay a lost write under the same identity Source: https://docs.anlyon.com/api-reference/actions/replay-action-effect Repeat the write of an `unknown` effect under the same provider identity, for adapters that declare `sameKeyReplay` backed by a provider idempotency key (`stripe.refund` and `resend.email_send`, each within its 24-hour window). `github.file_update` does not offer replay: GitHub has no idempotency key, and the blob-SHA precondition cannot tell a restored file from an unwritten one. A declared action never offers replay either, because it sends no provider idempotency key. A replay is a write and passes every control a first dispatch passes: the reviewed binding and preview expiry, the approval, the action version that would run now, the halt switch, the requesting credential's authority, current policy and dependencies. Every refusal has one shape: `409` with code `REPLAY_REFUSED`, the reason in `details.reason` (`replay_unsupported`, `not_unknown`, `replay_window_passed`, `credential_unavailable`, `allowance_released`, `write_in_flight`, or the dispatch control that failed, such as `policy_denies` or `preview_expired`) and the effect's current outcome in `details.outcome`. A refusal is recorded in the effect's evidence, sends nothing and changes nothing else. An admitted replay that fails (never sent, or refused by the provider) describes that attempt, not the original write: the effect stays `unknown` with its allowance held. Only the provider's own record of the operation in a failed state settles it `failed`. It never creates a second effect. `POST /api/v2/actions/effects/{id}/replay` --- # Record a manual resolution Source: https://docs.anlyon.com/api-reference/actions/resolve-action-effect Record an operator's reasoned conclusion for a `pending` or `unknown` effect, or settle a failed effect's possible partial impact. The result is labelled `verification: manual` and is never presented as provider verification. Settles impact limits: `succeeded` consumes the declared (or `actualImpact`) quantity; `failed` with no partial impact releases it. A confirmed partial must state `actualImpact`. A confirmed outcome is never rewritten (`409`). The key that requested the effect cannot resolve it. `POST /api/v2/actions/effects/{id}/resolve` --- # Resolve an unknown invocation Source: https://docs.anlyon.com/api-reference/actions/resolve-action-invocation Record what the destination confirms happened to an invocation whose status is `unknown`. Nothing is sent to the destination. Resolved `succeeded` is billed; resolved `failed` releases the reserved usage and refunds the key's budget reservation. The resolution, with its evidence, operator and time, is kept permanently and returned as `resolution`. Only one resolution can win: an invocation that is no longer `unknown` returns `409`. Needs `actions:resolve` (never granted by default) or a workspace admin in the console. The key that requested the invocation can never resolve it. The Idempotency-Key of the original stays taken; to run the action again, invoke with a new key and `retryOf`. An invocation governed by an effect (`effectId` set) is refused with `409` `EFFECT_RESOLUTION_REQUIRED` and the effect in `details.effectId`: resolve the effect at `POST /api/v2/actions/effects/{id}/resolve`, which settles its business allowance too. `POST /api/v2/actions/invocations/{id}/resolve` --- # Update an action Source: https://docs.anlyon.com/api-reference/actions/update-action `PATCH /api/v2/actions/{ref}` --- # Get agent analytics Source: https://docs.anlyon.com/api-reference/analytics/get-agent-analytics Returns agent run volume, outcomes, duration, tokens, costs, and top agents. `GET /api/v2/analytics/agents` --- # Get cross-product analytics overview Source: https://docs.anlyon.com/api-reference/analytics/get-analytics-overview Returns reliability, usage, and attention signals across the workspace's products. `GET /api/v2/analytics/overview` --- # Get current usage Source: https://docs.anlyon.com/api-reference/analytics/get-current-usage Retrieve current day usage statistics `GET /api/v2/analytics/usage/current` --- # Get delivery analytics Source: https://docs.anlyon.com/api-reference/analytics/get-delivery-analytics Returns message, schedule, workflow, destination, bandwidth, and error analytics. `GET /api/v2/analytics/delivery` --- # Get failed-message error distribution Source: https://docs.anlyon.com/api-reference/analytics/get-error-distribution Return the most frequent error messages in a date range, plus chart-ready data. `GET /api/v2/analytics/errors` --- # Get message analytics Source: https://docs.anlyon.com/api-reference/analytics/get-message-analytics Retrieve aggregated message statistics for a date range `GET /api/v2/analytics/messages` --- # Get message time series data Source: https://docs.anlyon.com/api-reference/analytics/get-message-time-series Retrieve message counts over time with configurable granularity `GET /api/v2/analytics/time-series` --- # Get workspace onboarding progress Source: https://docs.anlyon.com/api-reference/analytics/get-onboarding-status Report setup milestones and the first-message activation state, plus chart-ready progress data. `GET /api/v2/analytics/onboarding` --- # Get schedule statistics Source: https://docs.anlyon.com/api-reference/analytics/get-schedule-analytics Counts schedules in the calling credential's environment. A key may only name its own environment; a session caller that names none reads the whole workspace. `GET /api/v2/analytics/schedules` --- # Get top destination URLs Source: https://docs.anlyon.com/api-reference/analytics/get-top-urls Retrieve the most frequently used destination URLs `GET /api/v2/analytics/top-urls` --- # Get trust and control analytics Source: https://docs.anlyon.com/api-reference/analytics/get-trust-control-analytics Returns action invocation and approval decision outcomes, latency, and top entities. `GET /api/v2/analytics/trust-control` --- # Get usage history Source: https://docs.anlyon.com/api-reference/analytics/get-usage-history Retrieve historical usage data `GET /api/v2/analytics/usage/history` --- # List recent schedule execution summaries Source: https://docs.anlyon.com/api-reference/analytics/list-recent-schedule-executions Lists the most recent execution summary for schedules in the calling credential's environment. A key may only name its own environment; a session caller that names none reads the whole workspace. `GET /api/v2/analytics/schedule-executions` --- # Apply a policy file Source: https://docs.anlyon.com/api-reference/approval-policies/apply-approval-policy-file Applies exactly the reviewed plan in one transaction: every change and its history entry commit together or not at all. Refuses with `409 STALE_PLAN` when the file, the environment's policies or the referenced actions changed since the plan, and with `400 POLICY_FILE_INVALID` when the plan has errors. The digest is a staleness check, not an authorization. The credential is recorded as the actor on each version. `POST /api/v2/approval-policies/files/apply` --- # Create an approval policy Source: https://docs.anlyon.com/api-reference/approval-policies/create-environment-approval-policy Needs `policies:write`. Targeting is required: send `matchKind`, and `matchActionName` to target one action. The environment is the credential's, never the request's. This scope is privileged and never in the default set: the agent a policy governs must not be able to rewrite it. `POST /api/v2/approval-policies` --- # Delete an approval policy Source: https://docs.anlyon.com/api-reference/approval-policies/delete-environment-approval-policy Needs `policies:write`. Recorded invocation decisions keep their explanation. `DELETE /api/v2/approval-policies/{policyId}` --- # Evaluate current policies without executing Source: https://docs.anlyon.com/api-reference/approval-policies/evaluate-approval-policies Read-only evaluation against the credential's environment. Uses production matcher ordering and fail-closed behavior. Attribution is hypothetical. The result is policy-only, not an authorization or a final invocation decision; action version pins, approval floor, schema validation, budgets, halt state and runtime authority are not evaluated. Amount is derived from payload.amount, custom tags from payload.tags. Action contexts require an existing action name. `POST /api/v2/approval-policies/evaluate` --- # Read one policy version Source: https://docs.anlyon.com/api-reference/approval-policies/get-approval-policy-version One retained version, for example the exact version an approval or invocation decision recorded. Readable after the policy is deleted. Scoped to the credential environment. `GET /api/v2/approval-policies/{policyId}/versions/{version}` --- # Get an approval policy Source: https://docs.anlyon.com/api-reference/approval-policies/get-environment-approval-policy Needs `policies:read`. A policy in another environment is reported as not found. `GET /api/v2/approval-policies/{policyId}` --- # Read immutable policy history Source: https://docs.anlyon.com/api-reference/approval-policies/list-approval-policy-versions Returns retained snapshots newest first, a page at a time, including the deletion tombstone after a policy is removed. Page with `before` set to `pagination.nextBefore`; the cursor is stable while new versions are added. `pagination.earliestVersion` is the oldest retained version, so a decision citing an older one predates retained history. Older overwritten versions are not reconstructed; baseline entries mark migration-time evidence. Scoped to the credential environment. Tenant or environment erasure removes its retained history. `GET /api/v2/approval-policies/{policyId}/versions` --- # List approval policies Source: https://docs.anlyon.com/api-reference/approval-policies/list-environment-approval-policies Policies in the credential's own environment, in evaluation order. Needs `policies:read`, or a console session. With `?actionName=` only the policies that could govern that action are returned: the ones naming it plus the broad ones that also catch it. A credential is bound to one environment; an `environmentId` query that disagrees with it is refused. `GET /api/v2/approval-policies` --- # Plan a policy file Source: https://docs.anlyon.com/api-reference/approval-policies/plan-approval-policy-file Read-only. Parses a policy file (YAML or JSON, at most 256 KiB, no duplicate keys or aliases), checks every policy (targets, CEL conditions against the action's input schema, name clashes, broad denies, the plan's policy allowance) and returns what applying it would create, update and delete. Only policies owned by this file are ever changed; console-made and other files' policies are never taken over. `errors` lists everything that prevents applying. `digest` identifies the reviewed state: pass it to apply, which refuses if anything changed since. The file must declare the credential's environment. `POST /api/v2/approval-policies/files/plan` --- # Test a policy file against fixture requests Source: https://docs.anlyon.com/api-reference/approval-policies/test-approval-policy-file Decides each fixture request against the environment as it would be after applying the file (every enabled policy the file does not own, plus the file's policies), using the production decision function. Writes nothing and dispatches nothing. Policy decision only: an action's own approval floor is not applied. `POST /api/v2/approval-policies/files/test` --- # Update an approval policy Source: https://docs.anlyon.com/api-reference/approval-policies/update-environment-approval-policy Needs `policies:write`. Omitted fields are unchanged; an explicit `null` clears a nullable matcher. Enabling or disabling a policy is a patch of `enabled`. Bumps the version. Decisions already recorded on invocations keep the explanation they were made with. `PATCH /api/v2/approval-policies/{policyId}` --- # Approve a request Source: https://docs.anlyon.com/api-reference/approvals/approve-approval Decide a pending request. For an action approval, this executes and returns the parked invocation. A credential cannot decide an approval it requested, whether it is the requesting API key or the requesting OAuth app acting for the same user. `POST /api/v2/approvals/{id}/approve` --- # Request an approval Source: https://docs.anlyon.com/api-reference/approvals/create-approval `POST /api/v2/approvals` --- # Deny a request Source: https://docs.anlyon.com/api-reference/approvals/deny-approval Deny a pending request. A credential cannot decide an approval it requested, whether it is the requesting API key or the requesting OAuth app acting for the same user. `POST /api/v2/approvals/{id}/deny` --- # Get or wait for an approval Source: https://docs.anlyon.com/api-reference/approvals/get-approval Return immediately, or wait up to 55 seconds for the approval to leave pending state. `GET /api/v2/approvals/{id}` --- # List approvals Source: https://docs.anlyon.com/api-reference/approvals/list-approvals `GET /api/v2/approvals` --- # Inspect the current credential Source: https://docs.anlyon.com/api-reference/authentication/get-credential-identity Returns only the caller's own credential: its type, API key id (API keys only), workspace and environment, effective scopes and expiry. Needs no scope beyond being authenticated. Use it at startup to check the application holds the scopes it needs and none it should not. Holding a scope is not proof a call will run: policies, budgets, environment halt and quotas still apply at request time. Never cached. `GET /api/v2/auth/identity` --- # Estimate current-month cost Source: https://docs.anlyon.com/api-reference/billing/get-billing-cost-estimate `GET /api/v2/billing/cost-estimate` --- # Get current quota utilization Source: https://docs.anlyon.com/api-reference/billing/get-billing-quota `GET /api/v2/billing/quota` --- # Get browser-safe Stripe configuration Source: https://docs.anlyon.com/api-reference/billing/get-billing-stripe-config `GET /api/v2/billing/stripe-config` --- # Get daily billing usage history Source: https://docs.anlyon.com/api-reference/billing/get-billing-usage-history `GET /api/v2/billing/usage/history` --- # Get current billing period usage Source: https://docs.anlyon.com/api-reference/billing/get-current-billing-usage Retrieve usage statistics for the current billing period `GET /api/v2/billing/usage/current` --- # Get current plan details Source: https://docs.anlyon.com/api-reference/billing/get-current-plan Retrieve details about your current pricing plan `GET /api/v2/billing/plan` --- # Read workspace overage alert target Source: https://docs.anlyon.com/api-reference/billing/get-workspace-billing-budget Alert-only configuration, not a spending hard stop. `GET /api/v2/billing/budget` --- # List workspace invoices Source: https://docs.anlyon.com/api-reference/billing/list-billing-invoices `GET /api/v2/billing/invoices` --- # List available plans Source: https://docs.anlyon.com/api-reference/billing/list-plans Retrieve all available pricing plans `GET /api/v2/billing/plans` --- # Delete an API-key budget Source: https://docs.anlyon.com/api-reference/budgets/delete-budget Remove the cap; the API key becomes unbudgeted. `DELETE /api/v2/budgets/{apiKeyId}` --- # Get an API-key budget Source: https://docs.anlyon.com/api-reference/budgets/get-budget `GET /api/v2/budgets/{apiKeyId}` --- # List API-key budgets Source: https://docs.anlyon.com/api-reference/budgets/list-budgets List configured key budgets with current-month usage and estimated spend. An API-key or OAuth caller sees only keys in its own environment; a dashboard session sees the whole workspace. `GET /api/v2/budgets` --- # Create or replace an API-key budget Source: https://docs.anlyon.com/api-reference/budgets/put-budget `PUT /api/v2/budgets/{apiKeyId}` --- # Delete a semantic cache entry Source: https://docs.anlyon.com/api-reference/cache/delete-cache-entry `DELETE /api/v2/cache/{entryId}` --- # Flush semantic cache entries Source: https://docs.anlyon.com/api-reference/cache/flush-cache `POST /api/v2/cache/flush` --- # Get a semantic cache entry Source: https://docs.anlyon.com/api-reference/cache/get-cache-entry `GET /api/v2/cache/{entryId}` --- # Get semantic cache statistics Source: https://docs.anlyon.com/api-reference/cache/get-cache-stats `GET /api/v2/cache/stats` --- # List semantic cache entries Source: https://docs.anlyon.com/api-reference/cache/list-cache-entries `GET /api/v2/cache` --- # Look up a semantic cache entry Source: https://docs.anlyon.com/api-reference/cache/lookup-cache Try an exact key match first, then a top-1 semantic match within the requested threshold. `POST /api/v2/cache/lookup` --- # Store a semantic cache entry Source: https://docs.anlyon.com/api-reference/cache/store-cache-entry Embed and upsert an entry by workspace, namespace, and normalized key. `POST /api/v2/cache` --- # Assemble prompt-ready agent context Source: https://docs.anlyon.com/api-reference/context/assemble-context Merge relevant memories and/or a session summary and message tail into structured blocks and ready-to-paste text. `POST /api/v2/context` --- # Permanently clear the DLQ Source: https://docs.anlyon.com/api-reference/dlq/clear-dlq Permanently delete failed messages in the calling credential's environment, up to 1000 per call; `remaining` reports what is left. Another environment's failures are untouched. This cannot be undone. `DELETE /api/v2/dlq` --- # Permanently delete a failed message Source: https://docs.anlyon.com/api-reference/dlq/delete-dlq-message `DELETE /api/v2/dlq/{messageId}` --- # Get a failed message Source: https://docs.anlyon.com/api-reference/dlq/get-dlq-message `GET /api/v2/dlq/{messageId}` --- # Get DLQ statistics Source: https://docs.anlyon.com/api-reference/dlq/get-dlq-stats Counts failed messages in the calling credential's environment. `GET /api/v2/dlq/stats` --- # List failed messages Source: https://docs.anlyon.com/api-reference/dlq/list-dlq-messages Lists failed messages in the calling credential's environment. `GET /api/v2/dlq` --- # Retry failed messages Source: https://docs.anlyon.com/api-reference/dlq/retry-all-dlq-messages Queue each failed message in the calling credential's environment independently, up to 1000 per call, and report how many succeeded; individual retry failures do not abort the batch and `remaining` reports what is left. `POST /api/v2/dlq/retry-all` --- # Retry a failed message Source: https://docs.anlyon.com/api-reference/dlq/retry-dlq-message Reset attempts and failure state, then place the message back on the delivery stream it was published to, with the environment, flow-control key and queue it was published under. Only the calling credential's environment is visible. `POST /api/v2/dlq/{messageId}/retry` --- # Get this key's environment Source: https://docs.anlyon.com/api-reference/environments/get-own-environment The environment this API key is bound to, including whether it is currently halted. `GET /api/v2/environment` --- # Halt this key's environment Source: https://docs.anlyon.com/api-reference/environments/halt-own-environment The kill switch. The agent stops acting and nothing leaves the environment: action invocations are blocked, deliveries defer, workflow steps park, and writes to memory, files, sessions, cache and Connect are refused. Publishes still queue and approvals can still be decided, so nothing in flight is lost. Applies to the environment this key is bound to -- a staging key cannot halt production. `POST /api/v2/environment/halt` --- # Resume this key's environment Source: https://docs.anlyon.com/api-reference/environments/resume-own-environment Release the deferred work and let the agent act again. `POST /api/v2/environment/resume` --- # Create a webhook subscription Source: https://docs.anlyon.com/api-reference/events/create-event-subscription Subscribe a URL to matching event types. With `generateSecret`, the plaintext signing secret is returned exactly once. `POST /api/v2/events/topics/{ref}/subscriptions` --- # Create an event topic Source: https://docs.anlyon.com/api-reference/events/create-event-topic Create a named workspace topic. The name `anlyon` is reserved for platform lifecycle events. `POST /api/v2/events/topics` --- # Delete a webhook subscription Source: https://docs.anlyon.com/api-reference/events/delete-event-subscription `DELETE /api/v2/events/subscriptions/{subscriptionId}` --- # Delete an event topic Source: https://docs.anlyon.com/api-reference/events/delete-event-topic Delete a topic and its subscriptions and events. The reserved platform topic cannot be deleted. `DELETE /api/v2/events/topics/{ref}` --- # Get an event and its deliveries Source: https://docs.anlyon.com/api-reference/events/get-event `GET /api/v2/events/{eventId}` --- # Get an event topic Source: https://docs.anlyon.com/api-reference/events/get-event-topic `GET /api/v2/events/topics/{ref}` --- # List topic subscriptions Source: https://docs.anlyon.com/api-reference/events/list-event-subscriptions `GET /api/v2/events/topics/{ref}/subscriptions` --- # List event topics Source: https://docs.anlyon.com/api-reference/events/list-event-topics List workspace topics, including the reserved `anlyon` platform topic after it has been created. `GET /api/v2/events/topics` --- # List published events Source: https://docs.anlyon.com/api-reference/events/list-events `GET /api/v2/events` --- # Publish an event Source: https://docs.anlyon.com/api-reference/events/publish-event Accept an event for asynchronous fan-out. Publishing to the reserved `anlyon` topic is blocked. `idempotencyKey` provides durable topic-scoped deduplication; `Idempotency-Key` protects an HTTP retry. `POST /api/v2/events` --- # Rotate a subscription signing secret Source: https://docs.anlyon.com/api-reference/events/rotate-event-subscription-secret Replace the signing secret and return the new plaintext value exactly once. `POST /api/v2/events/subscriptions/{subscriptionId}/rotate-secret` --- # Update a webhook subscription Source: https://docs.anlyon.com/api-reference/events/update-event-subscription `PATCH /api/v2/events/subscriptions/{subscriptionId}` --- # Update an event topic Source: https://docs.anlyon.com/api-reference/events/update-event-topic Rename a topic or change its description. The reserved platform topic can only be re-described. `PATCH /api/v2/events/topics/{ref}` --- # Confirm a presigned upload Source: https://docs.anlyon.com/api-reference/files/confirm-file-upload Verify the uploaded object and enforce the authoritative stored size. Repeated confirmation of a ready file is safe. `POST /api/v2/files/{fileId}/confirm` --- # Delete a file Source: https://docs.anlyon.com/api-reference/files/delete-file `DELETE /api/v2/files/{fileId}` --- # Download a file Source: https://docs.anlyon.com/api-reference/files/download-file Redirect to a short-lived signed URL, or return the URL as JSON when redirect=false. `GET /api/v2/files/{fileId}/download` --- # Get file metadata Source: https://docs.anlyon.com/api-reference/files/get-file `GET /api/v2/files/{fileId}` --- # List files Source: https://docs.anlyon.com/api-reference/files/list-files `GET /api/v2/files` --- # Create a presigned file upload Source: https://docs.anlyon.com/api-reference/files/presign-file-upload `POST /api/v2/files/presign` --- # Upload a file directly Source: https://docs.anlyon.com/api-reference/files/upload-file-direct Upload a plan-limited file through the API using multipart form data. `POST /api/v2/files` --- # Create a flow-control key Source: https://docs.anlyon.com/api-reference/flow-control/create-flow-control-key Creates a key in the calling credential’s environment. Names and runtime limits are independent between environments. `POST /api/v2/flow-control` --- # Delete a flow-control key Source: https://docs.anlyon.com/api-reference/flow-control/delete-flow-control-key Re-enqueue all waitlisted messages to their original streams before deleting configuration and live counters. `DELETE /api/v2/flow-control/{flowControlKeyId}` --- # Get a flow-control key Source: https://docs.anlyon.com/api-reference/flow-control/get-flow-control-key Returns 404 for a key outside the calling credential’s environment. `GET /api/v2/flow-control/{flowControlKeyId}` --- # Get live flow-control status Source: https://docs.anlyon.com/api-reference/flow-control/get-flow-control-status Read active, waitlist, and current-window rate counters from Redis with persisted pause/pin state. `GET /api/v2/flow-control/{flowControlKeyId}/status` --- # List flow-control keys Source: https://docs.anlyon.com/api-reference/flow-control/list-flow-control-keys Lists only keys in the calling credential’s environment. `GET /api/v2/flow-control` --- # Pause waitlist release Source: https://docs.anlyon.com/api-reference/flow-control/pause-flow-control-key `POST /api/v2/flow-control/{flowControlKeyId}/pause` --- # Pin flow-control configuration Source: https://docs.anlyon.com/api-reference/flow-control/pin-flow-control-key Prevent per-message publishing options from overriding this key's stored controls. `POST /api/v2/flow-control/{flowControlKeyId}/pin` --- # Reset the current rate counter Source: https://docs.anlyon.com/api-reference/flow-control/reset-flow-control-rate Remove current and previous Redis window counters. This is a no-op when the key has no rate window. `POST /api/v2/flow-control/{flowControlKeyId}/reset-rate` --- # Resume waitlist release Source: https://docs.anlyon.com/api-reference/flow-control/resume-flow-control-key `POST /api/v2/flow-control/{flowControlKeyId}/resume` --- # Unpin flow-control configuration Source: https://docs.anlyon.com/api-reference/flow-control/unpin-flow-control-key `POST /api/v2/flow-control/{flowControlKeyId}/unpin` --- # Update a flow-control key Source: https://docs.anlyon.com/api-reference/flow-control/update-flow-control-key Set a numeric control or clear it with null. Clear rate limit and window together. `PATCH /api/v2/flow-control/{flowControlKeyId}` --- # Archive an impact limit Source: https://docs.anlyon.com/api-reference/impact-limits/archive-impact-limit The limit stops applying to new effects. Its existing reservations still settle and release. `DELETE /api/v2/impact-limits/{id}` --- # Create an impact limit Source: https://docs.anlyon.com/api-reference/impact-limits/create-impact-limit Cap `money` (one currency, integer minor units), `resource_mutations`, or a workspace-declared unit such as `emails` for the whole environment, optionally narrowed to one action and/or a resource prefix, per `day`, `month` or `total`. Shared by every agent, session and key: none of them is part of a limit's identity. A limit in a declared unit applies to actions that declare an `impact` in that unit. A declared action's resource key is `http:`, so `resourcePrefix` can scope a limit to a path prefix. Needs `impact-limits:write`, which no key receives by default. `POST /api/v2/impact-limits` --- # Get an impact limit Source: https://docs.anlyon.com/api-reference/impact-limits/get-impact-limit `GET /api/v2/impact-limits/{id}` --- # List impact limits Source: https://docs.anlyon.com/api-reference/impact-limits/list-impact-limits Business-impact limits in the caller's environment, each with its limit, `consumed` in the current period, `reserved` (everything held and unresolved, across periods), `unknownExposure` (the part of `reserved` whose outcome is unknown or possibly partial, never an additional amount), and `available`. `GET /api/v2/impact-limits` --- # Update an impact limit Source: https://docs.anlyon.com/api-reference/impact-limits/update-impact-limit Rename a limit or change its amount. Scope, dimension and period are fixed. Lowering the amount never releases existing exposure; it blocks new reservations until availability recovers. `PATCH /api/v2/impact-limits/{id}` --- # Create a Discord webhook integration Source: https://docs.anlyon.com/api-reference/integrations/create-discord-integration Validate and encrypt a Discord webhook URL; the URL is never returned. `POST /api/v2/integrations/discord` --- # Delete an integration Source: https://docs.anlyon.com/api-reference/integrations/delete-integration `DELETE /api/v2/integrations/{id}` --- # List workspace integrations Source: https://docs.anlyon.com/api-reference/integrations/list-integrations `GET /api/v2/integrations` --- # List Slack channels Source: https://docs.anlyon.com/api-reference/integrations/list-slack-integration-channels `GET /api/v2/integrations/slack/{id}/channels` --- # Send a Discord test notification Source: https://docs.anlyon.com/api-reference/integrations/test-discord-integration `POST /api/v2/integrations/discord/{id}/test` --- # Send a Slack test notification Source: https://docs.anlyon.com/api-reference/integrations/test-slack-integration `POST /api/v2/integrations/slack/{id}/test` --- # Enable or disable an integration Source: https://docs.anlyon.com/api-reference/integrations/toggle-integration `PATCH /api/v2/integrations/{id}/toggle` --- # Update integration event types Source: https://docs.anlyon.com/api-reference/integrations/update-integration-event-types `PATCH /api/v2/integrations/{id}/events` --- # Select a Slack channel Source: https://docs.anlyon.com/api-reference/integrations/update-slack-integration-channel Attempt to join the channel, then persist it even when auto-join reports an error. `PATCH /api/v2/integrations/slack/{id}/channel` --- # Export up to 10,000 execution logs Source: https://docs.anlyon.com/api-reference/logs/export-execution-logs Exports only logs whose parent belongs to the calling credential’s environment. `GET /api/v2/logs/export` --- # Get detailed execution-log statistics Source: https://docs.anlyon.com/api-reference/logs/get-execution-log-stats Aggregates only logs whose parent belongs to the calling credential’s environment. `GET /api/v2/logs/stats` --- # Query unified execution logs Source: https://docs.anlyon.com/api-reference/logs/list-execution-logs Returns message and schedule logs in one redacted, page-paginated timeline within the calling credential’s environment. Logs without a surviving environment-owned parent are omitted. `GET /api/v2/logs` --- # List execution logs for a message Source: https://docs.anlyon.com/api-reference/logs/list-message-execution-logs Returns the retained attempt history in ascending timestamp order with sensitive headers redacted. `GET /api/v2/messages/{messageId}/logs` --- # List execution logs for a schedule Source: https://docs.anlyon.com/api-reference/logs/list-schedule-execution-logs `GET /api/v2/schedules/{scheduleId}/logs` --- # Consolidate memories (async, v1.1) Source: https://docs.anlyon.com/api-reference/memory/consolidate LLM-driven merge/dedupe of a collection's memories. `POST /api/v2/memory/consolidate` --- # Create a collection Source: https://docs.anlyon.com/api-reference/memory/create-collection `POST /api/v2/memory/collections` --- # Delete a collection (cascades to memories) Source: https://docs.anlyon.com/api-reference/memory/delete-collection `DELETE /api/v2/memory/collections/{collectionId}` --- # Forget a single memory Source: https://docs.anlyon.com/api-reference/memory/forget `DELETE /api/v2/memory/{memoryId}` --- # Batch forget by ids or documentId Source: https://docs.anlyon.com/api-reference/memory/forget-batch `POST /api/v2/memory/forget` --- # Get a collection Source: https://docs.anlyon.com/api-reference/memory/get-collection `GET /api/v2/memory/collections/{collectionId}` --- # Get job status Source: https://docs.anlyon.com/api-reference/memory/get-job Status + progress for ingest, consolidate, or reembed jobs. `GET /api/v2/memory/jobs/{jobId}` --- # Get a single memory Source: https://docs.anlyon.com/api-reference/memory/get-memory `GET /api/v2/memory/{memoryId}` --- # Ingest a document (async) Source: https://docs.anlyon.com/api-reference/memory/ingest Queue a document for async chunking + embedding. Supports inline text, URL, or fileKey (v1.1). `POST /api/v2/memory/ingest` --- # List collections Source: https://docs.anlyon.com/api-reference/memory/list-collections `GET /api/v2/memory/collections` --- # List memories Source: https://docs.anlyon.com/api-reference/memory/list-memories `GET /api/v2/memory` --- # Migrate collection to new embedding model (v1.1) Source: https://docs.anlyon.com/api-reference/memory/migrate-collection Creates a new frozen collection and queues a re-embed job. `POST /api/v2/memory/collections/{collectionId}/migrate` --- # Recall (semantic/hybrid search) Source: https://docs.anlyon.com/api-reference/memory/recall `POST /api/v2/memory/recall` --- # Remember (store a memory) Source: https://docs.anlyon.com/api-reference/memory/remember Store a piece of knowledge. Content is embedded synchronously and available to recall() immediately. `POST /api/v2/memory` --- # Batch remember (v1.1) Source: https://docs.anlyon.com/api-reference/memory/remember-many Bulk-embed and store many memories in one call. Up to 100 items. `POST /api/v2/memory/batch` --- # Summarize memories (sync, v1.1) Source: https://docs.anlyon.com/api-reference/memory/summarize LLM-driven summary of a set of memories into one new memory. `POST /api/v2/memory/summarize` --- # Update a collection Source: https://docs.anlyon.com/api-reference/memory/update-collection `PATCH /api/v2/memory/collections/{collectionId}` --- # Upload a file to R2 (v1.1) Source: https://docs.anlyon.com/api-reference/memory/upload-file Upload a file and get a fileKey for use with ingest(). `POST /api/v2/memory/files` --- # Cancel a scheduled message Source: https://docs.anlyon.com/api-reference/messages/cancel-message Cancel a message that is scheduled for future delivery. Only messages with status "scheduled" can be cancelled. `DELETE /api/v2/messages/{messageId}` --- # Get message status Source: https://docs.anlyon.com/api-reference/messages/get-message Retrieve detailed information about a specific message `GET /api/v2/messages/{messageId}` --- # List messages Source: https://docs.anlyon.com/api-reference/messages/list-messages Retrieve a paginated list of messages with optional filters `GET /api/v2/messages` --- # Publish multiple messages in batch Source: https://docs.anlyon.com/api-reference/messages/publish-batch Publish up to 100 messages in a single request. Against a key budget a batch costs one unit per message, and admission is all-or-nothing: a batch that does not fit the remaining cap is refused whole with `429 BUDGET_EXCEEDED` and none of its messages are enqueued. Split the batch or raise the cap. `POST /api/v2/messages/batch` --- # Publish a single message Source: https://docs.anlyon.com/api-reference/messages/publish-message Publish immediately or schedule delivery with a Unix timestamp. The legacy scheduledFor field remains accepted for compatibility. `POST /api/v2/messages/publish` --- # Dismiss a notification Source: https://docs.anlyon.com/api-reference/notifications/delete-notification Delete a notification for the whole workspace. Needs `notifications:write`. `DELETE /api/v2/notifications/{id}` --- # Get your own notification preferences Source: https://docs.anlyon.com/api-reference/notifications/get-my-notification-preferences Returns the calling member's own preferences in this workspace. Needs a user, so a console session or an OAuth connection. An API key has no user and answers `403` `USER_IDENTITY_REQUIRED`. `GET /api/v2/notifications/preferences/me` --- # Get notification preferences Source: https://docs.anlyon.com/api-reference/notifications/get-notification-preferences Retrieve notification preferences for your workspace `GET /api/v2/notifications/preferences` --- # Get unread notification count Source: https://docs.anlyon.com/api-reference/notifications/get-unread-notification-count `GET /api/v2/notifications/unread-count` --- # List notifications Source: https://docs.anlyon.com/api-reference/notifications/list-notifications Retrieve notifications for your workspace. For a caller with a user (a console session or an OAuth connection), types that user muted in their own preferences are left out of the list and the unread count. `GET /api/v2/notifications` --- # Mark all notifications as read Source: https://docs.anlyon.com/api-reference/notifications/mark-all-notifications-as-read Mark all notifications for your workspace as read. Read state is shared by the whole workspace. Needs `notifications:write`. `PATCH /api/v2/notifications/read-all` --- # Mark notification as read Source: https://docs.anlyon.com/api-reference/notifications/mark-notification-as-read Mark a specific notification as read. Read state is shared by the whole workspace. Needs `notifications:write`. `PATCH /api/v2/notifications/{id}/read` --- # Send a test notification webhook Source: https://docs.anlyon.com/api-reference/notifications/test-notification-webhook Send a synthetic signed notification through the SSRF-protected outbound client. The optional secret is never stored or returned. Needs `notifications:manage`, which no key receives by default. A console session or an OAuth connection also needs the admin role. `POST /api/v2/notifications/preferences/test-webhook` --- # Update your own notification preferences Source: https://docs.anlyon.com/api-reference/notifications/update-my-notification-preferences Replaces the notification types the calling member muted. A muted type is left out of that member's own list and unread count. It is still raised, still delivered to email, Slack, Discord and the webhook, and still shown to every other member. Needs `notifications:write` and a user. No role is needed. An API key has no user and answers `403` `USER_IDENTITY_REQUIRED`. `PATCH /api/v2/notifications/preferences/me` --- # Update notification preferences Source: https://docs.anlyon.com/api-reference/notifications/update-notification-preferences Update notification settings for your workspace, including which channels and alert types are on and who receives alerts by email and webhook. Needs `notifications:manage`, which no key receives by default. A console session or an OAuth connection also needs the admin role. `PATCH /api/v2/notifications/preferences` --- # Create a queue Source: https://docs.anlyon.com/api-reference/queues/create-queue Queue names beginning with `__` are reserved for hidden platform queues. `POST /api/v2/queues` --- # Soft-delete a queue Source: https://docs.anlyon.com/api-reference/queues/delete-queue Remove a user queue from active routing and future reads. Hidden system queues cannot be deleted. `DELETE /api/v2/queues/{queueId}` --- # Get a queue Source: https://docs.anlyon.com/api-reference/queues/get-queue `GET /api/v2/queues/{queueId}` --- # List queues Source: https://docs.anlyon.com/api-reference/queues/list-queues Hidden platform queues and soft-deleted queues are excluded. `lag` is read live from pending and scheduled Redis structures. `GET /api/v2/queues` --- # Pause a queue Source: https://docs.anlyon.com/api-reference/queues/pause-queue `POST /api/v2/queues/{queueId}/pause` --- # Resume a queue Source: https://docs.anlyon.com/api-reference/queues/resume-queue `POST /api/v2/queues/{queueId}/resume` --- # Update a queue Source: https://docs.anlyon.com/api-reference/queues/update-queue `PATCH /api/v2/queues/{queueId}` --- # Attach feedback to a run Source: https://docs.anlyon.com/api-reference/runs/add-run-feedback Explicit or implicit feedback on a run, or a specific span within it the start of the learning loop from production behaviour. `POST /api/v2/runs/{runId}/feedback` --- # End or update a run Source: https://docs.anlyon.com/api-reference/runs/end-run `PATCH /api/v2/runs/{runId}` --- # Export a run as OTLP/JSON Source: https://docs.anlyon.com/api-reference/runs/export-run Exports a redacted trace in OpenTelemetry Protocol JSON shape. `GET /api/v2/runs/{runId}/export` --- # Get a run with its span tree Source: https://docs.anlyon.com/api-reference/runs/get-run `GET /api/v2/runs/{runId}` --- # Import an OTLP/JSON trace Source: https://docs.anlyon.com/api-reference/runs/import-run Imports one bounded OTLP/JSON trace as a new run. The destination environment is always taken from the API key; source workspace and environment attributes are ignored. `POST /api/v2/runs/import` --- # Ingest spans Source: https://docs.anlyon.com/api-reference/runs/ingest-spans Batch-ingest spans for a run. Idempotent: a span carrying an `id` is upserted, so a retried batch never duplicates. `POST /api/v2/runs/{runId}/spans` --- # List feedback for a run Source: https://docs.anlyon.com/api-reference/runs/list-run-feedback `GET /api/v2/runs/{runId}/feedback` --- # List runs Source: https://docs.anlyon.com/api-reference/runs/list-runs Newest first. Search run ID, name, or application; filter by status, agent, user, and time range. `GET /api/v2/runs` --- # Start a run Source: https://docs.anlyon.com/api-reference/runs/start-run A run is an agent execution a tree of spans. The environment is taken from the API key. `POST /api/v2/runs` --- # Create a new schedule Source: https://docs.anlyon.com/api-reference/schedules/create-schedule Create a recurring schedule (cron job) to send messages at specified intervals `POST /api/v2/schedules` --- # Delete a schedule Source: https://docs.anlyon.com/api-reference/schedules/delete-schedule Permanently delete a schedule `DELETE /api/v2/schedules/{scheduleId}` --- # Get a specific schedule Source: https://docs.anlyon.com/api-reference/schedules/get-schedule Retrieve detailed information about a specific schedule `GET /api/v2/schedules/{scheduleId}` --- # List all schedules Source: https://docs.anlyon.com/api-reference/schedules/list-schedules Retrieve all schedules for your workspace `GET /api/v2/schedules` --- # Pause a schedule Source: https://docs.anlyon.com/api-reference/schedules/pause-schedule Temporarily pause a schedule without deleting it `POST /api/v2/schedules/{scheduleId}/pause` --- # Resume a paused schedule Source: https://docs.anlyon.com/api-reference/schedules/resume-schedule Resume a previously paused schedule `POST /api/v2/schedules/{scheduleId}/resume` --- # Update a schedule Source: https://docs.anlyon.com/api-reference/schedules/update-schedule Update an existing schedule's configuration `PATCH /api/v2/schedules/{scheduleId}` --- # Delete a secret Source: https://docs.anlyon.com/api-reference/secrets/delete-secret `DELETE /api/v2/secrets/{name}` --- # Get secret metadata Source: https://docs.anlyon.com/api-reference/secrets/get-secret Return metadata only; the stored value is never returned. `GET /api/v2/secrets/{name}` --- # List secret metadata Source: https://docs.anlyon.com/api-reference/secrets/list-secrets List names and metadata only. Secret values are never returned. `GET /api/v2/secrets` --- # Create or rotate a secret Source: https://docs.anlyon.com/api-reference/secrets/put-secret Store a new encrypted value or replace the existing value. The response contains metadata only. `PUT /api/v2/secrets/{name}` --- # Append session messages Source: https://docs.anlyon.com/api-reference/sessions/append-session-messages Append one message or an ordered batch of up to 100 messages atomically. `POST /api/v2/sessions/{id}/messages` --- # Clear session messages Source: https://docs.anlyon.com/api-reference/sessions/clear-session-messages Delete every message and reset the rolling summary while retaining the session. `DELETE /api/v2/sessions/{id}/messages` --- # Create a session Source: https://docs.anlyon.com/api-reference/sessions/create-session Create a persistent agent conversation thread, optionally seeded with messages. `POST /api/v2/sessions` --- # Delete a session Source: https://docs.anlyon.com/api-reference/sessions/delete-session Permanently delete a session and all of its messages. `DELETE /api/v2/sessions/{id}` --- # Get a session Source: https://docs.anlyon.com/api-reference/sessions/get-session `GET /api/v2/sessions/{id}` --- # Get prompt-ready session context Source: https://docs.anlyon.com/api-reference/sessions/get-session-context Return the rolling summary and newest message tail that fits the token budget. `GET /api/v2/sessions/{id}/context` --- # List session messages Source: https://docs.anlyon.com/api-reference/sessions/list-session-messages List messages oldest first. `GET /api/v2/sessions/{id}/messages` --- # List sessions Source: https://docs.anlyon.com/api-reference/sessions/list-sessions List sessions ordered by most recent activity, optionally filtered by title. `GET /api/v2/sessions` --- # Pop the newest session message Source: https://docs.anlyon.com/api-reference/sessions/pop-session-message Remove and return the newest message so a failed agent turn can be retried. `DELETE /api/v2/sessions/{id}/messages/last` --- # Update a session Source: https://docs.anlyon.com/api-reference/sessions/update-session Replace the title and/or metadata of a session. `PATCH /api/v2/sessions/{id}` --- # Subscribe to the realtime event stream Source: https://docs.anlyon.com/api-reference/stream/stream-events An authenticated, resumable Server-Sent Events feed of workspace state changes (runs, spans, approvals, messages, workflows, budgets, notifications, deliveries), scoped to the API key's workspace and environment (only events attributed to that environment). Resume after a disconnect by sending the last id you saw as the `Last-Event-ID` header (or `?cursor=`); events after that id are replayed from a bounded window, so a reconnect has no silent gap. `GET /api/v2/stream` --- # Add an endpoint to a URL group Source: https://docs.anlyon.com/api-reference/url-groups/add-endpoint Add a new endpoint URL to an existing URL group `POST /api/v2/url-groups/{groupId}/endpoints` --- # Create a URL group Source: https://docs.anlyon.com/api-reference/url-groups/create-url-group Create a group of URLs for broadcasting messages. The group belongs to the calling key's environment and is only visible to keys bound to it. `POST /api/v2/url-groups` --- # Delete a URL group Source: https://docs.anlyon.com/api-reference/url-groups/delete-url-group Permanently delete a URL group `DELETE /api/v2/url-groups/{groupId}` --- # Get a URL group Source: https://docs.anlyon.com/api-reference/url-groups/get-url-group Retrieve a specific URL group with all its endpoints `GET /api/v2/url-groups/{groupId}` --- # List URL groups Source: https://docs.anlyon.com/api-reference/url-groups/list-url-groups Retrieve the URL groups in the calling key's environment `GET /api/v2/url-groups` --- # Publish to all endpoints in a group Source: https://docs.anlyon.com/api-reference/url-groups/publish-to-group Send a message to all endpoints in a URL group. Fan-out is a publish, so it obeys the same rules as `POST /messages/publish`: the messages are created in the calling key's environment, a halted environment refuses an agent key's publish with `409`, and a `{{secret:NAME}}` reference in a delivery header requires `secrets:read`. Against a key budget the publish costs one unit per endpoint in the group, and admission is all-or-nothing: a fan-out that does not fit the remaining cap is refused whole with `429 BUDGET_EXCEEDED` and no endpoint receives a message. `POST /api/v2/url-groups/{groupId}/publish` --- # Remove an endpoint from a URL group Source: https://docs.anlyon.com/api-reference/url-groups/remove-endpoint Remove a specific endpoint from a URL group `DELETE /api/v2/url-groups/{groupId}/endpoints/{endpointId}` --- # Update a URL group Source: https://docs.anlyon.com/api-reference/url-groups/update-url-group Update the name of a URL group `PATCH /api/v2/url-groups/{groupId}` --- # Cancel a workflow run Source: https://docs.anlyon.com/api-reference/workflows/cancel-workflow-run Only pending, running, or waiting runs can be cancelled. `POST /api/v2/workflows/runs/{runId}/cancel` --- # Create a workflow Source: https://docs.anlyon.com/api-reference/workflows/create-workflow Define the callback endpoint used to execute runs. Callback and failure URLs are validated against the platform SSRF blocklist. `POST /api/v2/workflows` --- # Delete a workflow and its runs Source: https://docs.anlyon.com/api-reference/workflows/delete-workflow `DELETE /api/v2/workflows/{workflowId}` --- # Get a workflow Source: https://docs.anlyon.com/api-reference/workflows/get-workflow `GET /api/v2/workflows/{workflowId}` --- # Get a workflow run Source: https://docs.anlyon.com/api-reference/workflows/get-workflow-run `GET /api/v2/workflows/runs/{runId}` --- # List steps for a workflow run Source: https://docs.anlyon.com/api-reference/workflows/list-workflow-run-steps `GET /api/v2/workflows/runs/{runId}/steps` --- # List workflow runs Source: https://docs.anlyon.com/api-reference/workflows/list-workflow-runs An unknown but well-formed workflow ID returns an empty page and never falls through to unfiltered results. `GET /api/v2/workflows/runs` --- # List workflows Source: https://docs.anlyon.com/api-reference/workflows/list-workflows `GET /api/v2/workflows` --- # Notify workflows waiting for an event Source: https://docs.anlyon.com/api-reference/workflows/notify-workflow-event Stores an event for seven days and resumes one matching waiting run when present. Limited to 100 events/hour/workspace and 20 events/minute/event ID. `POST /api/v2/workflows/events/notify` --- # Trigger a workflow run Source: https://docs.anlyon.com/api-reference/workflows/trigger-workflow Accepts an optional JSON payload up to 1 MB. Inactive workflows return `WORKFLOW_INACTIVE`. `POST /api/v2/workflows/{workflowId}/trigger` --- # Update a workflow Source: https://docs.anlyon.com/api-reference/workflows/update-workflow `PATCH /api/v2/workflows/{workflowId}` --- # Authentication & API Keys Source: https://docs.anlyon.com/authentication Your code authenticates to the Anlyon API with an API key. This page covers creating keys, scoping them and checking them. ## Overview The API endpoints in this documentation take an API key unless a page says otherwise. MCP clients sign in with OAuth, and the console uses a session. A key carries two things that decide what it can reach: - **One environment.** A key belongs to exactly one environment and resolves it on authentication. A staging key cannot read or write production data, because the environment is bound to the credential itself. This is logical isolation: rows in shared storage, filtered by an enforced column. It is not dedicated storage or compute. See [Environments](/trust-control/environments). - **A set of scopes.** Scopes decide which operations the key may perform, from `actions:read` to `approvals:decide`. See [Scopes](#scopes) below. Neither is something you remember to do correctly. A key that is missing a scope gets a `403` with the scope named, and a key pointed at the wrong environment simply does not find the data. ## API Key Format API keys follow this format: ``` anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` - **Prefix**: every key starts with `anlyon_live_`. The prefix does not name an environment. A staging key carries it too. - **Key**: 32 random characters ## Getting Your API Key ### From the Dashboard 1. **Log in** Log in to your Anlyon account 2. **Navigate to Settings** Navigate to **Settings** → **API Keys** 3. **Create Key** Click **Create API key** and enter a descriptive name (for example "Support agent, staging") 4. **Choose the environment** Pick the environment this key belongs to. It cannot be changed later, and the key will never reach any other environment. 5. **Choose the permissions** **Standard** is the default and is what an agent needs for ordinary work. **Full access** adds every [privileged scope](#privileged-scopes): secrets, approval decisions, budgets, policies, impact limits, halting the environment, changing an approval gate and resolving unknown outcomes. Grant it rarely. **Read only** is a good fit for observability. You can also pick individual scopes. 6. **Set an expiry (optional)** Optionally set an expiration date 7. **Copy Key** **Copy the key when it is shown.** It is shown once. ## Using API Keys ### HTTP Header Include your API key in the `Authorization` header using the Bearer scheme: ```bash curl -X POST https://api.anlyon.com/api/v2/actions/update-status-file/invoke \ -H "Authorization: Bearer anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "input": { "path": "notes/status.txt", "content": "status: green\n", "message": "Update status" } }' ``` The example invokes the action the [quickstart](/quickstart) defines. **Note** The TypeScript and Python SDKs send this header for you. For direct API calls, use the `Authorization` header with a Bearer token. ### Code Examples ```typescript TypeScript/JavaScript const response = await fetch('https://api.anlyon.com/api/v2/actions/update-status-file/invoke', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ input: { path: 'notes/status.txt', content: 'status: green\n', message: 'Update status' }, }), }); const { data } = await response.json(); console.log(data.status, data.grade); ``` ```python Python import requests headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json' } response = requests.post( 'https://api.anlyon.com/api/v2/actions/update-status-file/invoke', headers=headers, json={ 'input': {'path': 'notes/status.txt', 'content': 'status: green\n', 'message': 'Update status'} } ) data = response.json()['data'] print(data['status'], data.get('grade')) ``` ```javascript Node.js const axios = require('axios'); const response = await axios.post( 'https://api.anlyon.com/api/v2/actions/update-status-file/invoke', { input: { path: 'notes/status.txt', content: 'status: green\n', message: 'Update status' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } } ); ``` ## Security Best Practices **Store Keys Securely** **Never commit API keys to version control!** Use environment variables: ```bash # .env file (add to .gitignore) ANLYON_API_KEY=anlyon_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ```typescript // In your code const apiKey = process.env.ANLYON_API_KEY; ``` **One Key per Environment** You do not have to remember this one. It is enforced. Every key belongs to exactly one environment, chosen when you create it. A key cannot reach another environment's actions, secrets, invocations, approvals, events, agents, runs or spans, or any other data scoped to it. So a staging key running against production credentials does not quietly write to production. It finds nothing there. Create one key per environment per service, and name them so the environment is obvious. **Rotate Keys Regularly** - Set expiration dates on API keys - Rotate keys every 90 days - Revoke unused or compromised keys **Use Least Privilege** - Give each key only the [scopes](#scopes) it uses. An agent that never touches the vault should not hold `secrets:read`. - Keep `secrets:*`, `approvals:decide`, `budgets:write`, `actions:govern`, `actions:resolve`, `policies:write` and `environments:halt` on separate operator keys. The agent must not be able to rewrite the policy that governs it. None of them is agent work, and none is granted by default. - Create separate keys for different services, so revoking one does not stop the others. - Use descriptive names, and monitor key usage in the dashboard. ## API Key Management ### List API Keys Get all API keys for your workspace: ```bash curl https://api.anlyon.com/api/v2/workspaces/{workspaceId}/api-keys \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` **`data` (array)** **`id` (string)** API key ID **`name` (string)** Descriptive name **`keyPrefix` (string)** Key prefix (full key only shown once) **`isActive` (boolean)** Whether the key is active **`createdAt` (string)** Creation timestamp **`expiresAt` (string | null)** Expiration timestamp **`lastUsedAt` (string | null)** Last usage timestamp ### Delete an API key Delete a key. It stops working and cannot be restored: ```bash curl -X DELETE https://api.anlyon.com/api/v2/workspaces/{workspaceId}/api-keys/{keyId} \ -H "Cookie: better-auth.session_token=YOUR_SESSION_TOKEN" ``` ## Scopes A scope is a `resource:action` pair, and a key holds a fixed set of them. They are enforced on every request: a call the key has no scope for is refused with `403` and the missing scope named, whatever the key is otherwise allowed to do. Scopes are chosen when the key is created and cannot be changed afterwards. To widen or narrow a key, create a new one and retire the old one. That is deliberate, so a key's authority cannot drift after you have reviewed it. ### Presets | Preset | What it holds | Use it for | | --- | --- | --- | | **Standard** | Every current scope except the privileged ones below. That includes reading, defining and invoking actions, requesting approvals, runs, events and reading impact limits | Agent keys. This is the default. | | **Read only** | Every current `:read` scope, including the privileged read scopes | Dashboards, monitoring, exports | | **Full access** | Every current scope, including the privileged ones below, except `integrations:read`, `integrations:write` and `messages:ingest` | Rare. Prefer a Standard key plus a separate operator key. | No console preset grants a scope for an earlier namespace, such as `messages:*` or `memory:*`. A key created through the API with no scopes named receives every scope that is not privileged, those included. ### Privileged scopes These are not granted by default. None of them is a way to use Anlyon. Each one either reads credentials or changes what Anlyon will stop you doing. An OAuth consent screen offers four of them as separate groups that are off unless you turn them on: approval decisions, notification channels, secrets and budgets. It does not offer `actions:govern`, `actions:resolve`, `environments:halt`, `policies:read`, `policies:write` or `impact-limits:write`. | Scope | What it allows | | --- | --- | | `secrets:read`, `secrets:write` | Manage vault secrets, and reference them from an action definition | | `approvals:decide` | Approve or deny pending approvals without a human clicking approve | | `budgets:read`, `budgets:write` | Read and change a key's monthly caps on Anlyon operations. A budget does not cap model tokens or the money an action moves | | `integrations:read`, `integrations:write` | Connect and change Slack and Discord channels, which redirect the workspace's alerts | | `notifications:manage` | Change the workspace's notification settings: which channels and alert types are on, which email addresses and which webhook URL receive alerts. Also send a test webhook. A console session or an OAuth connection also needs the admin role. An ordinary credential must not be able to redirect an alert | | `actions:govern` | Change whether an action requires approval, and change or remove what a governed action declares | | `actions:resolve` | Record the confirmed outcome of an invocation whose outcome is `unknown`. The key that made the call cannot resolve it. See [Receipts and grades](/execution/outcomes) | | `policies:read`, `policies:write` | Read, create, change, disable and delete the approval policies for the key's own environment. See [Policy as code](/trust-control/policy-as-code) | | `environments:halt` | Halt and resume the key's own environment | | `messages:ingest` | Keep publishing messages while the environment is halted | | `impact-limits:write` | Create, change and archive the shared [impact limits](/execution/impact-limits) for the key's own environment. An agent must not be able to raise the cap that bounds it | **Why referencing a secret needs a scope** An action template can carry `{{secret:STRIPE_KEY}}`, and Anlyon substitutes the value at fire time so your agent never sees it. That substitution is a *use* of the secret, so defining such an action needs `secrets:read`. Without that rule, `actions:write` would be a way to use every secret in the vault. A secret can also be restricted to the hosts it may be sent to and where in the request it may appear. ### Every scope `messages:read` `messages:write` `schedules:read` `schedules:write` `queues:read` `queues:write` `workflows:read` `workflows:write` `integrations:read` `integrations:write` `logs:read` `dlq:read` `dlq:write` `flow-control:read` `flow-control:write` `url-groups:read` `url-groups:write` `billing:read` `notifications:read` `feedback:write` `analytics:read` `memory:read` `memory:write` `events:read` `events:write` `files:read` `files:write` `cache:read` `cache:write` `sessions:read` `sessions:write` `context:read` `secrets:read` `secrets:write` `actions:read` `actions:write` `actions:invoke` `approvals:read` `approvals:write` `approvals:decide` `budgets:read` `budgets:write` `runs:read` `runs:write` `actions:govern` `actions:resolve` `environments:halt` `messages:ingest` `policies:read` `policies:write` `impact-limits:read` `impact-limits:write` `notifications:write` `notifications:manage` ### Check the key you were given `GET /api/v2/auth/identity` (`client.auth.identity()` in both SDKs) returns only the caller's own credential: type, key id (API keys only), workspace and environment, effective scopes and expiry. It needs no scope beyond being authenticated, is never cached, and returns no secret, hash or other key. Call it at startup to fail fast if the key is missing a scope you need, or holds one you should not. Holding a scope does not prove a call will run: policies, budgets, environment halt and quotas still apply at request time. ## Key Expiration API keys can have optional expiration dates. When a key expires: - Requests using that key return `401` - You'll need to create a new key - Expired keys cannot be reactivated Set expiration when creating a key: ```json { "name": "Temporary Integration", "expiresAt": "2027-12-31T23:59:59Z" } ``` ## Error Responses ### A key that does not authenticate An invalid, expired or revoked key gets the same answer, HTTP `401`. The response does not say which of the three it was. ```json { "success": false, "error": { "code": "AUTHENTICATION_ERROR", "message": "Authentication failed" }, "requestId": "..." } ``` ### A key that lacks a scope HTTP `403`, with the scope named. ```json { "success": false, "error": { "code": "AUTHORIZATION_ERROR", "message": "API key is missing required scope: actions:invoke" }, "requestId": "..." } ``` ## Troubleshooting **Authentication failed (401)** 1. Verify the key is copied correctly (no extra spaces) 2. Check that you're using the `Bearer` prefix 3. Check in the dashboard that the key has not expired or been revoked 4. Verify you're using a key for the right workspace **and** environment **Missing required scope Error** The key authenticated fine but is not allowed to perform this operation. The message names the scope it needs, e.g. `API key is missing required scope: actions:invoke`. 1. Check the key's scopes in **Settings** → **API Keys** 2. If the key should have it, create a new key with that scope. Scopes are fixed at creation 3. If it should not, the caller is doing something the key was not meant to do **A resource you expected is missing** Most often the key belongs to a different environment than the data. A key resolves its environment on authentication, so it sees that environment and no other. A missing resource usually means "not in this environment" rather than "deleted". **A key that expired or was revoked** The API answers `401` with `Authentication failed` in both cases. 1. Check the expiration date and status in the dashboard 2. Create a new API key 3. Update your application with the new key ## Check an agent key at startup `GET /api/v2/auth/identity` reports the caller's credential and scopes without returning a secret. Both SDKs also provide a startup check: ```typescript await client.auth.requireScopes( ['actions:invoke', 'runs:write'], { forbidden: ['policies:write', 'approvals:decide'] }, ); ``` ```python client.auth.require_scopes( ['actions:invoke', 'runs:write'], forbidden=['policies:write', 'approvals:decide'], ) # With the async client, await client.auth.require_scopes(...). ``` Choose required scopes from the operations your application actually performs. A `ScopeCheckError` exposes `missing` and `forbidden` lists. The helper inspects permission. It does not grant scopes or change a key. Malformed identity responses fail the check. Session credentials use workspace roles instead of a scope list and cannot satisfy a nonempty required-scope list. This is an early configuration check, not permission to bypass policies, budgets, quotas, environment boundaries or a later key revocation. Keep governance credentials separate from runtime agent credentials. ## Next Steps - [Controls](/trust-control): Approvals, environments, kill switch, rollback - [Approval gates](/guides/approval-gates): Gate dangerous calls on human approval - [Actions](/execution/actions): Define a production call once and invoke it by name - [Error Handling](/advanced/error-handling): Understand API errors and rate limits --- # Beta Source: https://docs.anlyon.com/beta Anlyon is in early beta. The API contract can change before 1.0, and every change is announced in the [changelog](/changelog) with a migration note. The **current** terms are always the ones the API reports: `GET /api/v2/public/pricing-policy` (unauthenticated), the [pricing page](https://anlyon.com/pricing), and **Settings → Plan & Billing** in the console. They are read from the API's release setting, so they match what the API enforces. This page explains Early Beta Access, the terms in force while `mode` is `early_beta`. ## Early Beta Access Early Beta Access is the free plan every workspace starts on. - **Self-serve signup.** No invitation required. - **Free within hard limits.** The allowances are listed below and enforced by the API. The console and [pricing page](https://anlyon.com/pricing) show the same numbers. - **No card, no subscription, no overage.** Nothing is charged automatically. Paid plans arrive after the beta. - **Changes are announced before they take effect.** We review the offer after 30 days. If an allowance changes we tell you before it takes effect. There is no account-expiry countdown. - **Paying is always opt-in.** When paid plans open, moving to one is your explicit choice. Nobody is converted, charged, reset, or deleted by that change. ### Allowances | Resource | Early Beta Access | | --- | --- | | Action executions | 10,000 / month | | Durable messages | 25,000 / month | | Action definitions | 10 | | Seats, including pending invitations | 3 | | Environments | 3, of any kind | | Combined storage | 1 GiB | | Memory storage | 50 MiB, within that total | | Stored memories | 2,000 | | Egress | 50 GiB / month | | Trace retention | 14 days | | Email alerts | 500 / month | | Internal memory-processing allowance | $0.25 / month | | Anlyon Vigil reviews (opt-in) | 1,000 / month | | Anlyon Vigil approvals (opt-in) | 100 / day | Monthly allowances (executions, messages, egress, email alerts, Vigil reviews) **reset** at 00:00 UTC on the first of each month. Stored data, seats, environments, and action definitions are limits on what you hold, so they **do not reset**. Other rate, retry, payload, and resource limits are unchanged from the standard Free plan. ### What is included - **Three environments of any kind.** A new workspace starts with one protected environment of kind `production`, named Production. You can add `staging` and `development` kinds and change an environment's kind in the console or through the API. See [Environments](/trust-control/environments). - **Environment protection and controlled promotion** between environments. - **The email notification channel**, up to 500 emails per workspace per UTC month. - **Halt, isolation, approvals and approval policies, immutable versions and rollback.** - **Traces, spans and approval decisions**, with no separate charge. ### When you reach a limit Requests stop at a hard limit, with a machine-readable `QUOTA_EXCEEDED` error. Nothing is charged. What to do: | Limit | What to do | | --- | --- | | A monthly allowance (executions, messages, egress) | Wait for the reset shown in **Settings → Plan & Billing**, or contact support. | | Storage or a resource cap (data, seats, environments, actions, memories) | Remove unused items to free room. | | Memory processing | Wait for the monthly reset. Existing data is kept. | | Email alerts | Wait for the monthly reset. Alerts keep reaching the in-app inbox and the other channels you connected. | | Anlyon Vigil reviews or approvals | Nothing to do: approvals still reach people, without a suggestion. Reviews reset monthly, Vigil approvals daily. See [Anlyon Vigil](/trust-control/vigil). | There is no guaranteed response time and no guaranteed limit increase. Inspecting and deleting your own data always works, even at a limit. ## Stability `openapi.yaml` is the contract. The API reference is generated from it, and CI checks the backend routes and both SDKs against it, so "the SDK does something the API reference doesn't" is a bug, not a grey area. What the contract covers today: - The v2 REST API and both SDKs (`@anlyonhq/sdk`, `anlyon`). - Environments and the kill switch, approvals and approval policies, immutable versions and rollback, budgets. - The earlier namespaces: messages, schedules, URL groups, workflows, events, queues, the dead letter queue, memory, sessions, context assembly, files and semantic cache. They are kept as they are, and no new capability is planned for them. - Runs and traces, including OpenTelemetry export. Most likely to change before 1.0: - Response-shape details on the analytics endpoints. - Newer SDK ergonomics helpers layered on top of the core client. ## Changes to the API The API contract can change before 1.0. Every change is listed in the [changelog](/changelog) with a migration note. That covers a removed or renamed field, a narrower type, a new required parameter, a changed default and a changed status code. New endpoints, new optional fields and new enum values are added as the product grows. Write your integrations to ignore fields they do not recognise. ## Availability Anlyon publishes a status page and has no uptime SLA yet. - The [status page](https://stats.uptimerobot.com/IkZSpnHLVG) shows live and historical availability. - Incident notes for user-visible outages are posted to the status page. - Three controls are worth stating precisely. Halting an environment stops new dispatch out of it. A request an external server has already accepted is not recalled. A capped key is refused further Anlyon operations. That is a cap on those operations and not on model tokens or on money an approved action moves. Rollback repoints an agent at an earlier immutable version. It restores the definitions later calls run and does not undo an effect already dispatched. ## Support - **Email** [support@anlyon.com](mailto:support@anlyon.com). We read everything. There is no guaranteed response time. - **GitHub.** The source repositories are private. Public discussions are at [github.com/AnlyonHQ/community](https://github.com/AnlyonHQ/community/discussions). Tell us what's rough. Feedback sent now changes the roadmap. **Send feedback** and **Contact support** are also in **Settings → Plan & Billing**. ## How paid plans arrive Paid plans arrive after the beta. Production is planned at about $49 a month and Growth at about $199 a month, and Enterprise is by contract. The prices on the [pricing page](https://anlyon.com/pricing) are planned and may change before paid plans open. Reference documentation for them is in [Billing & Pricing](/account/billing). When paid plans open: - Your workspace stays on Free. **Nothing converts you to a paid plan.** - Existing Early Beta workspaces keep their beta allowances for a **transition window that is announced in advance**. Its end date is fixed once set and does not restart. - Above the standard Free limits, creating new resources may be blocked after that window, while inspecting, exporting, and deleting stay available. - Your data and usage are never reset or deleted by the change. ## What changes at 1.0 Anlyon reaches 1.0 when these three things are in place: - There is a meaningful stretch of availability history on the status page. - A written SLA with service credits is published. - Support has response-time targets. At 1.0 the sentence at the top of this page is removed. There is no date for 1.0. --- # Changelog Source: https://docs.anlyon.com/changelog Every change to the API contract is listed here with a migration note. The newest entry is at the top. Dates are UTC. ## 2026-10-05 ### A plan counts environments, and every workspace starts with Production **Breaking, billing responses.** `limits.maxEnvironments` and `betaAllowances.environments` are one number, or the string `"unlimited"`. They were objects keyed by `development`, `staging` and `production`. Affected responses: - `GET /api/v2/billing/plan` - `GET /api/v2/billing/plans` - `GET /api/v2/public/pricing-policy` | Plan | Environments | | --- | --- | | Early Beta Access | 3 | | Free | 1 | | Production | 2 | | Growth | 3 | | Enterprise | unlimited | What else changed with it: - A new workspace gets a default environment of kind `production`, named Production, with protection on. Existing environments keep their kind, name and slug. - A workspace creates environments of kind `development`, `staging` and `production` inside one total. `PATCH /api/v2/workspaces/{workspaceId}/environments/{environmentId}` changes `kind` with no plan check. - A create over the total returns `429` `QUOTA_EXCEEDED`. - Early Beta Access includes environment protection, promotion between environments and the email notification channel. Email is capped at 500 emails per workspace per UTC month. `features.protectedProduction` and `features.controlledPromotion` are `true` there. - The message of `PAID_PLANS_UNAVAILABLE` reads "Paid plans arrive after the beta. Nothing was charged. Your workspace keeps its Early Beta allowances." The message of `HTTPS_REQUIRED` reads "HTTPS is required." Codes and statuses are unchanged. **Migration note.** - Read `maxEnvironments` and `betaAllowances.environments` as a number or the string `"unlimited"`. A client that read `maxEnvironments.production`, or summed the three keys, reads the one value. - `@anlyonhq/sdk` types `BillingPlanLimits.maxEnvironments` and `BetaAllowances.environments` as `number | 'unlimited'`. Update the package and fix the type errors it reports. - Deleting a secret or a schedule in a protected environment needs `confirmProtected: true`. That now applies to the default environment of a new workspace. - A client that matched on the old message text of `PAID_PLANS_UNAVAILABLE` or `HTTPS_REQUIRED` matches on the code. ### The Stripe refund adapter accepts a live or a test key The `stripe.refund` adapter config names `mode: "live"` or `mode: "test"`. The credential must be a Stripe key of that mode, and the payment intent must belong to that mode. A mismatch is refused before anything is sent. The fault drill runs with `mode: "test"`. **Migration note.** An action created with `mode: "test"` keeps working with its test key. To refund live payments, create an action with `mode: "live"` and store a live key as its secret. ## 2026-10-03 ### Notification routes are split into read, write and manage **Security fix, breaking.** Every method on `/api/v2/notifications` used to need `notifications:read`. A key with that scope could change the alert webhook, the email recipients and send a test webhook. | Route | Scope | Role | | --- | --- | --- | | `GET /`, `GET /unread-count`, `GET /preferences` | `notifications:read` | any member | | `GET /preferences/me` (new) | `notifications:read`, needs a user | any member | | `PATCH /preferences/me` (new) | `notifications:write`, needs a user | any member | | `PATCH /{id}/read`, `PATCH /read-all`, `DELETE /{id}` | `notifications:write` | any member | | `PATCH /preferences`, `POST /preferences/test-webhook` | `notifications:manage` | admin | - `notifications:write` is in the default API key scope set. - `notifications:manage` is privileged. No key receives it by default. - A member mutes notification types for themselves with `/preferences/me`. A muted type is still raised and still delivered to every channel and every other member. - An API key has no user and answers `403` `USER_IDENTITY_REQUIRED` on `/preferences/me`. **Migration note.** - An OAuth client connected before 2026-10-03 must re-consent before it can mark notifications read, dismiss them or change notification settings. - An API key with an explicit scope list needs `notifications:write` added to mark read or dismiss, and `notifications:manage` to change workspace notification settings. Until then those calls answer `403`. ### A third adapter, `resend.email_send` Additive. `POST /api/v2/actions` and `PATCH /api/v2/actions/{ref}` accept `adapter.type: resend.email_send` with the config `{ domain, fromAddresses? }`. - Impact is counted in `emails`, one per recipient across `to`, `cc` and `bcc`. - `succeeded` means Resend accepted the send and returned an email id. A read-back that matches grades the effect `confirmed`. Neither grade means the email was delivered. - A replay is offered for 24 hours, the time Resend keeps an idempotency key. **Migration note.** None. Existing actions are unchanged. ### Any action can be governed, and every effect returns a grade An action may carry `impact`, `verify` or `governed: true`. Such an action is governed: each invocation creates an effect, binds its approval to the exact request, reserves its declared quantity against every applicable impact limit before dispatch, and returns a receipt. An action with no declaration and no adapter is unchanged. Additions: - `Action` and `ActionVersion` return `governed`, `impact` and `verify`. - `Effect` returns `grade`: `confirmed`, `acknowledged`, `unknown`, `failed`, `refused`, `denied` or `pending`. `GET /api/v2/actions/effects` accepts `grade` as a filter. - `ImpactLimit` returns `unit`, and `POST /api/v2/impact-limits` accepts a declared unit as `dimension`. - Changing or removing what a governed action declares needs the `actions:govern` scope. **Breaking for typed clients.** `dimension` on `ImpactQuantity`, `ImpactLimit` and `CreateImpactLimitRequest` is no longer a closed enum of `money` and `resource_mutations`. A workspace that declares a unit, such as `emails`, sees that unit name there. **Migration note.** Treat `dimension` as a string. A workspace that declares no unit sees the same two values as before. Handle an unrecognised `grade` value as you would `unknown`. --- # Anlyon CLI Source: https://docs.anlyon.com/cli The `anlyon` command-line interface operates Anlyon from a terminal with the same permissions and the same server-side checks as the API. The CLI submits requests to Anlyon. Policy evaluation, approvals and provider execution stay on the server. An action invoked from the CLI is checked against the same policies, waits for the same approvals and is recorded the same way as one invoked through the SDK. Commands are organised by resource: `anlyon actions …`, `anlyon approvals …`, `anlyon runs …`. **Versioning** Anlyon is in early beta. The API contract can change before 1.0, and every change is announced in the changelog with a migration note. The CLI covers authentication, profiles, read commands, invoking actions, declaring what an action changes and deciding approvals. Policy changes and more write commands come in later releases. While the CLI is 0.x, a minor release may add commands, flags, JSON fields and exit codes. A minor release may also remove, rename or change one, and that change carries a changelog entry marked **Breaking**. See [Compatibility](/cli/scripting#compatibility). ## Quickstart 1. **Install** The CLI needs Node 20.3 or later. See [Install](/cli/install) for other options. ```bash npm install -g @anlyonhq/cli anlyon --version ``` 2. **Log in** A browser opens. Sign in. If you belong to more than one workspace, Anlyon asks which workspace and environment this login acts in. With one workspace, the login is bound to its default environment. ```bash anlyon auth login ``` 3. **Check where you are** Shows the user, workspace and environment the credential acts in. ```bash anlyon whoami ``` 4. **List the actions you can invoke** ```bash anlyon actions list ``` 5. **Invoke one** The CLI asks you to confirm, naming the workspace and environment. If the action requires approval, the command exits with code 12 and prints the approval id. Add `--wait 10m` to follow it to its outcome. ```bash anlyon actions invoke refund_payment --data '{"charge":"ch_123","amount":1200}' ``` On a server or in CI, where there is no browser, use an API key instead of `auth login`. See [Scripting and CI](/cli/scripting). ## Command map ``` anlyon auth login | status | logout | scopes anlyon profile list | use anlyon whoami anlyon doctor anlyon actions list | get | versions anlyon actions invoke [--data '' | --input ] [--idempotency-key ] [--wait 10m] anlyon actions declare [--dimension … --amount … --bound …] [--verify-url … --verify-status …] [--governed | --ungoverned] anlyon invocations list [--action ] [--status ] | get anlyon runs list [--status …] [--search …] [--agent …] [--since …] [--until …] | get anlyon approvals list [--status pending|…] [--origin …] | get anlyon approvals approve [--note …] | deny [--note …] anlyon policies list [--action ] | get | versions anlyon effects list [--action ] [--outcome ] [--grade ] [--unresolved] | get anlyon impact-limits list [--include-archived] | get ``` `anlyon --help` describes each one. The [command reference](/cli/commands) covers them all with examples. ## How it behaves - **Decisions happen on the server.** Policy evaluation, approvals and the provider call run there, so a command passes every gate the API applies. - **A credential is bound to one workspace and environment.** `--environment` asserts which one you expect and stops the command if it differs. The binding stays as it is. - **`anlyon policies` reads approval policies.** Change them from deployment code with [policy as code](/trust-control/policy-as-code). ## Next steps **[Install](/cli/install)** npm, and the standalone executables for Linux, macOS and Windows. **[Log in and profiles](/cli/authentication)** Browser login, API keys for automation, and named targets. **[Command reference](/cli/commands)** Every command, what it needs, and how writes are confirmed. **[Scripting and CI](/cli/scripting)** JSON output, exit codes, environment variables and a CI example. --- # Log in and profiles Source: https://docs.anlyon.com/cli/authentication The CLI acts with one credential, bound to one workspace and one environment. That credential is either a browser login or an API key. ```bash anlyon auth login # opens your browser; approve on the Anlyon consent screen anlyon whoami # who, which workspace, which environment anlyon auth scopes # what this login allows, command by command ``` ## Browser login `anlyon auth login` opens your browser at the Anlyon consent screen. If you belong to more than one workspace, you choose the workspace and environment there. With one workspace, the login is bound to its default environment. The server binds them to this login. The CLI does not pick them. Browser login is OAuth 2.1 for a native app (RFC 8252): - PKCE (S256). - A one-shot listener on `127.0.0.1` with a random port receives the redirect. - A `state` check on the redirect. - No embedded client secret. The CLI registers itself as a public client for each login. Set `ANLYON_NO_BROWSER=1` to print the login URL instead of opening a browser, for example over SSH. The login still needs a browser to complete, and the redirect goes to `127.0.0.1` on the machine running the CLI. ### Where the credential is kept The refresh token is kept in the operating system's credential store: | OS | Store | | --- | --- | | macOS | login Keychain (`security`) | | Windows | Credential Manager | | Linux | Secret Service (gnome-keyring, KWallet) via `secret-tool` (package `libsecret-tools`) | Where no OS store is available, such as a headless server, use an [API key](#api-keys-for-automation). As an explicit opt-in, `ANLYON_CREDENTIAL_STORE=file` keeps credentials in a `0600` file in the config directory. **That file is not encrypted.** `anlyon auth status` and `anlyon doctor` say so whenever it is in use. ### Session lifetime Sessions last while they are used: - An access token lives 15 minutes. The CLI refreshes it. - A login that is not used for 7 days expires. `anlyon auth logout` revokes the refresh token and deletes the local copy. An access token already issued remains valid until it expires, at most 15 minutes later. ## API keys for automation For scripts, CI and servers, set `ANLYON_API_KEY` (and `ANLYON_API_URL` for a non-default API). Profiles are then ignored, and naming a profile as well is an error rather than a guess. ```bash export ANLYON_API_KEY=anlyon_live_... anlyon whoami ``` To keep a key in the credential store under a profile instead, pipe it in. It is never read from a terminal, argv or shell history: ```bash printf %s "$KEY" | anlyon auth login --api-key-stdin --profile ci ``` An API key carries the scopes it was created with. Create one with only the scopes the job needs. See [API keys](/authentication). ## Scopes Browser login asks for `openid profile email offline_access anlyon:actions anlyon:observability`. That covers actions, invocations, runs and approvals, including invoking actions. For a credential limited to reading, use an API key created with read scopes. More sensitive permissions are consent groups you add explicitly. They are shown on the consent screen: ```bash anlyon auth login --scope anlyon:approvals.decide ``` | You want to | You need | | --- | --- | | List and read actions, invocations, approvals, effects and impact limits | Browser login (default), or an API key with the matching read scope | | Read runs | Browser login (default), or an API key with `runs:read` | | Invoke an action | Browser login (default), or an API key with `actions:invoke`. `--wait` also needs `actions:read` | | Approve or deny | `anlyon auth login --scope anlyon:approvals.decide`, or an API key with `approvals:decide` and `approvals:read` | | Read approval policies | An operator API key with `policies:read`. No consent group grants it, so browser login cannot. | `anlyon auth scopes` shows exactly what the current credential can run, command by command, and what to do for anything it cannot. ## Profiles A profile is a named target: an API origin, a credential, and the workspace and environment it resolved to at login. A profile never contains a secret. The secret is in the credential store. ```bash anlyon auth login --profile staging anlyon profile list anlyon profile use staging # default for new commands export ANLYON_PROFILE=staging # pin this terminal only ``` On PowerShell, pin a terminal with `$env:ANLYON_PROFILE = "staging"`. ### How a command picks its target Before any request, every command resolves, in order: 1. The profile: `--profile`, then `ANLYON_PROFILE`, then the default set by `profile use`, then `default`. 2. The API origin. 3. The credential. 4. The workspace and environment, **from the server**. Consequences: - **`--environment ` is an assertion.** If the credential is bound to another environment, the command stops with exit code 10. The CLI never sends an environment override. - **A rebound credential stops commands.** If a profile's credential now resolves to a different workspace or environment than it did at login, commands stop with exit code 10 until you log in again. - **A credential only goes to its own API origin.** A profile's credential is sent to that profile's API origin and nowhere else. `ANLYON_API_URL` pointing elsewhere is refused (exit code 10). - **Profiles are independent.** Each profile has its own credential entry, OAuth client and server-side binding. Two terminals on different profiles cannot retarget each other. - **Parallel commands are safe.** Concurrent commands on one profile serialise token refreshes across processes, so parallel jobs never invalidate each other's session. To act in another environment, log in again under another profile and choose that environment when Anlyon asks. Anlyon asks only when you belong to more than one workspace. With one workspace, use an API key created in the other environment. ```bash anlyon auth login --profile production anlyon whoami --profile production --environment production ``` ## Check your setup ```bash anlyon auth status # which profile, API and credential commands will use, and whether it works anlyon doctor # checks the install, config, credential store, API, clock and credential ``` `anlyon doctor` exits 1 when a check fails and 0 otherwise. A warning does not fail it. --- # Command reference Source: https://docs.anlyon.com/cli/commands Every command also accepts the [global flags](/cli/scripting#terminal-contract): `--json`, `--profile`, `--environment`, `--input`, `--timeout` and `--no-input`, and list commands accept `--limit` and `--cursor`. `anlyon --help` describes each command. ## Account | Command | What it does | | --- | --- | | `anlyon auth login` | Log in with your Anlyon account in the browser (OAuth + PKCE), or store an API key with `--api-key-stdin`. Add consent groups with `--scope`. | | `anlyon auth status` | Show which profile, API and credential commands will use, and whether it works. | | `anlyon auth logout` | Revoke this profile's refresh token and delete its stored credential. | | `anlyon auth scopes` | Show what the current credential is granted, and which commands it can run. | | `anlyon profile list` | List profiles. The active one is what commands in this terminal will use. | | `anlyon profile use ` | Set the default profile for new commands. `ANLYON_PROFILE` still wins per terminal. | | `anlyon whoami` | Show who and where the current credential acts: user, workspace, environment. | | `anlyon doctor` | Check the install, config, credential store, API reachability and credential. Exits 1 when a check fails, 0 otherwise. | ```bash anlyon auth login --scope anlyon:approvals.decide anlyon profile use staging anlyon whoami ``` See [Log in and profiles](/cli/authentication). ## Actions and invocations | Command | What it does | | --- | --- | | `anlyon actions list` | List the actions configured in this environment. | | `anlyon actions get ` | Show one action by name or `act_` id. | | `anlyon actions versions ` | Show an action's version history. | | `anlyon actions invoke ` | Invoke an action. Policy and approvals apply exactly as for any other caller. [Writes](#writes) below. | | `anlyon actions declare ` | Set or remove what an action declares: its impact, its read-back, or governed alone. [Writes](#writes) below. | | `anlyon invocations list` | List action invocations, newest first. Filter with `--action ` and `--status ` (for example `unknown`, `failed`, `succeeded`). | | `anlyon invocations get ` | Show one invocation by `inv_` id. | ```bash anlyon actions list anlyon actions get refund_payment anlyon actions invoke refund_payment --data '{"charge":"ch_123","amount":1200}' anlyon invocations list --status unknown anlyon invocations get inv_123 ``` The action input for `actions invoke` is a JSON object, passed with `--data ''`, or read from a file or stdin with `--input `. Use one or the other, not both. An invocation's status is described in [Receipts and grades](/execution/outcomes). `failed` means the destination rejected the request, or it was never sent. `unknown` means it may have taken effect. ### Governed actions An action is governed when it declares an impact, a read-back, or `--governed` alone. `actions declare` sets and removes those declarations. A flag you leave out keeps what the action already declares. ```bash # One call sends count(input.to) emails. Impact limits in the unit "emails" now apply. anlyon actions declare send_email --dimension emails --amount 'count(input.to)' --bound exact --yes # Money needs its currency. The amount is a ceiling here. anlyon actions declare refund_payment --dimension money --unit usd --amount input.amount --bound upper_bound --yes # Read the write back after dispatch. A match grades the effect confirmed. anlyon actions declare send_email \ --verify-url 'https://api.example.com/v1/messages/{{response.id}}' --verify-status 200 \ --verify-match data.status=sent --yes anlyon actions declare send_email --no-verify --yes # remove the read-back anlyon actions declare send_email --ungoverned --yes # remove every declaration ``` `actions declare` flags: - `--dimension`: `money`, `resource_mutations`, or a unit you name such as `emails`. - `--unit`: the currency when the dimension is `money`, for example `usd`. Leave it out otherwise. - `--amount`: an integer, `input.` or `count(input.)`, with an optional `* `. - `--bound`: `exact` or `upper_bound`. - `--verify-url`, `--verify-status`: a `GET` on the action's own host and the HTTP status it must answer with. They go together. - `--verify-match path=value`: a check on the read-back body. It may be repeated, up to 10 times. A value that reads as a number, `true`, `false` or `null` is sent as that. Quote it as JSON to send a string: `--verify-match 'id="42"'`. - `--governed`: govern an action that declares neither an impact nor a read-back. - `--no-impact`, `--no-verify`: remove that declaration. - `--ungoverned`: remove every declaration. It cannot be combined with another declaration flag. An impact needs `--dimension`, `--amount` and `--bound` together. Setting an impact replaces the whole impact, and the same holds for the read-back. Declaring needs `actions:write`. Changing or removing what a governed action already declares also needs `actions:govern`, which no browser login grants. Use an operator API key for that. `actions get` shows `governed`, `impact` and `verify`. For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. See [Actions](/execution/actions). ## Runs | Command | What it does | | --- | --- | | `anlyon runs list` | List runs, newest first. | | `anlyon runs get ` | Show one run with its span tree. | `runs list` filters: - `--status`: `running`, `succeeded`, `failed` or `cancelled`. - `--search`: substring match on run id, name or application. - `--agent`: only runs by this agent id. - `--since`, `--until`: only runs started at or after, or before, an ISO 8601 time. ```bash anlyon runs list --status failed --since 2026-09-01T00:00:00Z anlyon runs get ``` ## Approvals | Command | What it does | | --- | --- | | `anlyon approvals list` | List approval requests. `--status pending` is the inbox. | | `anlyon approvals get ` | Show one approval by `apr_` id. | | `anlyon approvals approve ` | Approve a pending request. For a gated action, Anlyon runs the reviewed request once enough approvers agree. [Writes](#writes) below. | | `anlyon approvals deny ` | Deny a pending request. A denied action never runs. [Writes](#writes) below. | `approvals list` filters: - `--status`: `pending`, `approved`, `denied` or `expired`. - `--origin`: only decisions of this origin: `human`, `policy`, `system`, `unknown`, `assistant` or `automated`. ```bash anlyon approvals list --status pending anlyon approvals get apr_123 anlyon approvals approve apr_123 --note "checked with the customer" anlyon approvals deny apr_456 --note "amount does not match the ticket" ``` `--note` is recorded with the decision. See [Approvals and policies](/trust-control/approvals). ## Policies | Command | What it does | | --- | --- | | `anlyon policies list` | List approval policies for this environment. Filter with `--action `. | | `anlyon policies get ` | Show one approval policy. | | `anlyon policies versions ` | Show a policy's version history, newest first. | ```bash anlyon policies list --action refund_payment anlyon policies versions apol_123 ``` Policy commands need `policies:read`, which no browser consent group grants. Use an operator API key. The CLI reads policies. To configure policies from deployment code, see [Policy as code](/trust-control/policy-as-code). The SDK's `anlyon-policy` executable is unchanged and keeps working. ## Effects | Command | What it does | | --- | --- | | `anlyon effects list` | List governed effects with their receipt grades, newest first. | | `anlyon effects get ` | Show one governed effect by `eff_` id, with its receipt grade and evidence. | `effects list` filters: - `--action `: effects of this action (name or `act_` id). - `--outcome `: `not_dispatched`, `pending`, `succeeded`, `failed` or `unknown`. - `--grade `: `confirmed`, `acknowledged`, `unknown`, `failed`, `refused`, `denied` or `pending`. - `--unresolved`: effects still holding impact-limit capacity: pending, unknown or possibly partial. ```bash anlyon effects list --unresolved anlyon effects list --action send_email --grade refused anlyon effects list --grade acknowledged --json anlyon effects get eff_123 ``` Every governed call leaves an effect with a receipt grade. `effects list` has a `GRADE` column, and `effects get` prints what the grade means. | Grade | Meaning | | --- | --- | | `confirmed` | A read-back matched the request. On a declared action that is the verify rule the workspace wrote. | | `acknowledged` | The provider accepted the request. Nothing read it back. | | `unknown` | No usable response. Anlyon does not send it again. It stays unknown until a read-back or an operator settles it. | | `failed` | The provider rejected the request, or it never left Anlyon. | | `refused` | Anlyon refused it at dispatch. | | `denied` | The approval was denied. | | `pending` | No receipt yet. | See [Governed effects](/execution/governed-effects) and [Receipts and grades](/execution/outcomes). ## Impact limits | Command | What it does | | --- | --- | | `anlyon impact-limits list` | List the impact limits shared by every agent in this environment. Add `--include-archived` to include archived limits. | | `anlyon impact-limits get ` | Show one impact limit by `lim_` id. | ```bash anlyon impact-limits list anlyon impact-limits get lim_123 ``` Both commands show the `unit` each limit counts: the currency for money, the declared unit otherwise. The CLI reads limits. It does not create or change them. See [Impact limits](/execution/impact-limits). ## Writes `actions invoke`, `actions declare`, `approvals approve` and `approvals deny` change things. They behave as follows. ### They confirm first Interactively, the CLI asks you to confirm, naming the workspace and environment the credential is bound to. Anywhere else (scripts, CI, an agent's shell) pass `--yes`. Without it, nothing is sent. ```bash anlyon actions invoke refund_payment --data '{"charge":"ch_123","amount":1200}' --yes ``` ### They carry an idempotency key Every write carries an idempotency key: yours, passed with `--idempotency-key `, or one the CLI generates and prints. Re-running with the same key returns the recorded result and does not run the action again. That is what makes an interrupted command safe to repeat. For `approvals approve` and `approvals deny`, a re-run after the decision was recorded exits 6, because the approval is no longer pending. A key is 8 to 255 characters of letters, digits, `.`, `_`, `:` or `-`. ```bash anlyon actions invoke refund_payment --data '{"charge":"ch_123","amount":1200}' \ --idempotency-key refund-ch_123 --yes ``` See [Idempotency and retries](/execution/idempotency). ### Policy and approvals apply as for any caller - An invoke that needs approval exits with code **12** and prints the approval id. `--wait ` follows it to its outcome, for example `--wait 10m`. - An action that is still running when the command returns also exits **12**. - An outcome the destination could not confirm, such as a timeout after it received the request, exits **13**. **Do not retry with a new key. Reconcile with the destination first.** Re-running with the same `--idempotency-key` is safe: it returns the recorded result. - An action that failed, was denied, or whose approval expired exits **14**. If `--timeout` or Ctrl-C cuts off a write before a reply arrives, the request may already have reached Anlyon, so the command also exits 13 and prints the key that reads the recorded result back. ```bash anlyon actions invoke refund_payment --data '{"charge":"ch_123","amount":1200}' --wait 10m ``` ### Deciding approvals needs `approvals:decide` Log in with the decide consent group. The consent screen names it: ```bash anlyon auth login --scope anlyon:approvals.decide ``` A credential can never decide an approval it requested: not the requesting API key, and not the OAuth app (such as a CLI login) that made the request for you. Decide those from the dashboard or a separate login. Approving or denying an approval that is no longer pending exits with code 6. If an approved action then runs and its outcome cannot be confirmed, `approvals approve` exits 13. The full exit code table is in [Scripting and CI](/cli/scripting#exit-codes). --- # Install the CLI Source: https://docs.anlyon.com/cli/install Install the CLI from npm. ## npm The npm package needs Node 20.3 or later. It has no runtime dependencies. The Anlyon SDK is bundled in. ```bash npm npm install -g @anlyonhq/cli ``` ```bash pnpm pnpm add -g @anlyonhq/cli ``` Check the install: ```bash anlyon --version ``` The version output also names the `@anlyonhq/sdk` version bundled into this build. To run a single command without a global install: ```bash npx @anlyonhq/cli --version ``` To upgrade, run the install command again. To remove it, `npm uninstall -g @anlyonhq/cli`. ## Supported platforms | Platform | Architectures | | --- | --- | | Linux | x64, arm64 | | macOS | x64 (Intel), arm64 (Apple silicon) | | Windows | x64 | The npm package runs wherever Node 20.3 or later runs. ## Standalone executables A standalone executable embeds its own runtime, so it runs on a clean machine with no Node installed. Each GitHub release tagged `@anlyonhq/cli@` carries one file per platform, with a `SHA256SUMS` file alongside: | Platform | File | | --- | --- | | Linux x64 / arm64 | `anlyon-linux-x64`, `anlyon-linux-arm64` | | macOS Intel / Apple silicon | `anlyon-darwin-x64`, `anlyon-darwin-arm64` | | Windows x64 | `anlyon-windows-x64.exe` | The release assets sit in the Anlyon source repository, which is private. Downloading one needs access to that repository. Without it, install from npm. ### Verify the download Compare the file's SHA-256 hash with its line in `SHA256SUMS`: ```bash Linux sha256sum anlyon-linux-x64 ``` ```bash macOS shasum -a 256 anlyon-darwin-arm64 ``` ```powershell Windows Get-FileHash anlyon-windows-x64.exe -Algorithm SHA256 ``` ### macOS and Linux Make the file executable and move it onto your `PATH` as `anlyon`: ```bash chmod +x anlyon-darwin-arm64 sudo mv anlyon-darwin-arm64 /usr/local/bin/anlyon anlyon --version ``` Use the file name for your platform and architecture. macOS may quarantine an unsigned download. Code signing and notarisation are not in place yet. If macOS refuses to run the file, clear the quarantine attribute before moving it: ```bash xattr -d com.apple.quarantine anlyon-darwin-arm64 ``` ### Windows Put `anlyon-windows-x64.exe` on your `PATH` as `anlyon.exe`. For example, in PowerShell, for the current user: ```powershell $dir = "$env:LOCALAPPDATA\Programs\anlyon" New-Item -ItemType Directory -Force $dir | Out-Null Move-Item anlyon-windows-x64.exe "$dir\anlyon.exe" [Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$dir", "User") ``` Open a new terminal so it picks up the new `PATH`, then check: ```powershell anlyon --version ``` ## Next [Log in](/cli/authentication) with `anlyon auth login`, or set `ANLYON_API_KEY` for automation. --- # Scripting and CI Source: https://docs.anlyon.com/cli/scripting Scripts, CI jobs and agents should rely on `--json` output and exit codes, not on human-readable output, which may change in any release. The flags and exit codes on this page are the machine contract. While the CLI is 0.x, a minor release changes one only with a changelog entry marked **Breaking** (see [Compatibility](#compatibility)). ## Terminal contract | Flag | Meaning | | --- | --- | | `--json` | stdout carries exactly one JSON document. Lists: `{"data": [...], "nextCursor": string\|null, "total"?: number}`. Single objects: `{"data": {...}}`. | | `--profile ` | Use this profile. | | `--environment ` | Assert the credential's environment. It never switches it. | | `--input ` | Read options as a JSON object (keys are the command's option names, plus `limit`/`cursor`). Flags win. Unknown keys are rejected. | | `--limit <1-100>` / `--cursor ` | Pagination for list commands. Pass back `nextCursor`. Cursors are opaque and bound to the list that issued them. | | `--timeout ` | Give up after e.g. `30s`, `2m`, `500ms` (default 60s, 10m for `auth login`, and the wait plus one minute when `--wait` is set). | | `--no-input` | Never prompt or open a browser. Fail instead. | For `actions invoke`, `--input` supplies the action input itself, as an alternative to `--data`. ### Output - Results go to **stdout**. Progress, hints, warnings and errors go to **stderr**. - With `--json`, an error is one JSON object on stderr: ```json {"error": {"code": "...", "message": "...", "exitCode": 4, "status": 403, "requestId": "...", "hint": "..."}} ``` `status`, `requestId` and `hint` are present when they apply. - Output is redacted. API keys, tokens and `Bearer` values are removed before anything is printed. - Ctrl-C cancels the in-flight request and exits 130. The exception is a write (`actions invoke`, `actions declare`, `approvals approve`, `approvals deny`) that was sent and has no reply yet. Ctrl-C or `--timeout` then exits 13, because the request may have reached Anlyon. The error names the idempotency key to re-run with. ### Paging through a list ```bash anlyon invocations list --status unknown --limit 100 --json > page1.json anlyon invocations list --status unknown --limit 100 --json \ --cursor "$(jq -r .nextCursor page1.json)" > page2.json ``` `nextCursor` is `null` on the last page. ## Exit codes | Code | Meaning | | --- | --- | | 0 | Success | | 1 | Error (unexpected, or no more specific code) | | 2 | Usage: bad flags, arguments or `--input`. Nothing was sent | | 3 | Not logged in, or the credential expired, was revoked or was rejected (401) | | 4 | Permission denied: missing scope or role (403) | | 5 | Not found in this workspace/environment (404) | | 6 | Rejected by the server as invalid or conflicting (400/409/422) | | 7 | Rate limited or over quota (429) | | 8 | Server error or unreachable (5xx, network) | | 9 | `--timeout` elapsed. A write cut off before its reply exits 13 instead | | 10 | Target mismatch: `--environment`, a changed binding, or an API origin override | | 11 | Needs a server endpoint that is not available yet. Nothing was sent | | 12 | Action accepted, not finished: waiting for approval or still running | | 13 | Outcome unknown: the action may have taken effect, or a write was cut off before its reply. Do not retry with a new key | | 14 | Action did not succeed: failed, denied, or its approval expired | | 130 | Cancelled (Ctrl-C). A write cut off before its reply exits 13 instead | `anlyon doctor` exits 1 when a check fails and 0 otherwise. A warning does not fail it. Exit 11 is reserved for a command whose endpoint a server does not have. No current command uses it. The codes that need a decision from a script are 12, 13 and 14: - **12**: Nothing has failed. The action is waiting for a person, or still running. Check later with `anlyon invocations get `, or invoke with `--wait `. - **13**: The destination may or may not have acted. Do not retry with a new idempotency key. Reconcile with the destination first. Re-running with the **same** `--idempotency-key` is safe: it returns the recorded result. - **14**: The action did not run successfully: it failed, a reviewer or policy denied it, or its approval expired. ## Environment variables | Variable | Purpose | | --- | --- | | `ANLYON_PROFILE` | Profile for this shell. | | `ANLYON_API_KEY` | API key for automation. It bypasses profiles. | | `ANLYON_API_URL` | API origin for `ANLYON_API_KEY` and new logins (`ANLYON_BASE_URL` is accepted too). | | `ANLYON_CONFIG_DIR` | Config directory. The default is `$XDG_CONFIG_HOME/anlyon` when `XDG_CONFIG_HOME` is set, `~/.config/anlyon` otherwise, and `%APPDATA%\anlyon` on Windows. | | `ANLYON_CREDENTIAL_STORE` | `auto` (default), `keychain`, or `file` (unencrypted opt-in). | | `ANLYON_NO_BROWSER=1` | Print the login URL instead of opening a browser. | ## CI example A GitHub Actions step that invokes an action with an API key stored as a repository secret, waits up to 15 minutes for an approval, and branches on the exit code: ```yaml - name: Refund via Anlyon env: ANLYON_API_KEY: ${{ secrets.ANLYON_API_KEY }} run: | npm install -g @anlyonhq/cli set +e anlyon actions invoke refund_payment \ --data '{"charge":"ch_123","amount":1200}' \ --idempotency-key "refund-ch_123-${{ github.run_id }}" \ --environment production \ --wait 15m --yes --no-input --json > result.json code=$? set -e case "$code" in 0) echo "Refund succeeded." ;; 12) echo "::warning::Still waiting for approval $(jq -r .data.approvalId result.json)." ;; 13) echo "::error::Outcome unknown. Reconcile with the destination before re-running; do not change the idempotency key."; exit 1 ;; 14) echo "::error::Refund did not succeed: $(jq -r .data.status result.json)."; exit 1 ;; *) exit "$code" ;; esac ``` What each part does: - **`ANLYON_API_KEY`** comes from a secret, never the repository. Create the key in the target environment with only the scopes the job needs (here `actions:invoke` and `actions:read`, because `--wait` reads the invocation while it waits). With an API key set, profiles are ignored. - **`--idempotency-key`** includes the workflow run id, so re-running a failed job reuses the key and returns the recorded result instead of invoking the action twice. - **`--environment production`** asserts the key is bound to production. If it is not, the command stops with exit code 10 before anything is sent. - **`--wait 15m`** follows an approval to its outcome. If nobody decides within 15 minutes, the command exits 12. - **`--yes`** confirms the write. Without it a non-interactive write is refused and nothing is sent. **`--no-input`** makes sure the CLI never waits on a prompt. - **`set +e`** stops the shell failing the step before the exit code can be read. For a successful, pending or finished invoke, `--json` writes the invocation under `data` (with `status` and `approvalId`), and the idempotency key used under `idempotencyKey`. ## Compatibility - **Versioning.** The CLI is versioned independently of the SDK and the API. While 0.x, a minor release may add commands, flags, JSON fields and exit codes. A minor release may also remove or rename one, change the meaning of an exit code, or change the shape of an existing `--json` field. Each such change carries a changelog entry marked **Breaking**. From 1.0, those are major-version changes only. - **Machine contract.** Scripts should rely on `--json` output and exit codes, not on human-readable output, which may change in any release. - **SDK.** Each build bundles a specific `@anlyonhq/sdk`, shown by `anlyon --version`. The CLI's tests run against that SDK's source and check every request path against the API's OpenAPI contract. - **Server.** The CLI requires `GET /api/v2/auth/identity` and `GET /api/v2/environment`, and, for browser login, OAuth discovery at `/.well-known/oauth-protected-resource/mcp`. - **Config.** `config.json` carries a `version`. A CLI refuses a config version it does not know rather than rewriting it. - **Platforms.** Linux (x64, arm64), macOS (x64, arm64) and Windows (x64). The npm package supports Node 20.3 and later. --- # The execution model Source: https://docs.anlyon.com/execution Anlyon puts shared limits and receipts on the actions AI agents take through it. Your agent does not perform the production operation. It names an action and passes input, and Anlyon performs the operation on its behalf. Everything else in these docs follows from that one move, so it is worth being precise about it before anything else. ## Without Anlyon ```text your agent -> an API credential in your process -> production API ``` The model is driving a process that holds a live key. Whatever sits between a stray sentence in a scraped web page and a real refund is code you wrote and have to remember to run on every path. There is nothing outside your process that can refuse, pause, cap or attribute the call, because by the time anything could look at it the request has already left. ## With Anlyon ```text your agent -> a named action -> Anlyon -> production API ``` You define the call once, referencing credentials rather than pasting them. From then on your agent's side of the boundary contains no URL, no header and no key: ```typescript await anlyon.actions.invoke('refund-order', { charge: 'ch_3P9x', amount: 12000 }); ``` Anlyon owns what happens next. ## What happens between those two arrows Every invocation passes the same checks, in this order. Nothing here is optional per call. What varies is whether a given stage has anything to do. 1. **Check the caller's authority** Invoking needs `actions:invoke`. Referencing a secret is checked when the action is saved, and needs `secrets:read`, because a reference is a use. No key receives `secrets:read` by default. 2. **Reserve budget** The calling key's monthly cap on action invocations is checked and reserved. Past the cap the invocation stops with `429 BUDGET_EXCEEDED`. 3. **Resolve the action and its version** The name resolves to an action in the calling key's environment. If the call is attributed to a run whose agent pins a version of that action, the pinned definition runs. Otherwise the current definition runs. The response reports which one actually executed. 4. **Evaluate policy** Approval policies are evaluated on **every** invocation, so a policy can tighten the gate and not only relax it. A policy scoped to an identity the request does not carry is treated as indeterminate and requires approval, so withholding attribution is not a way around it. 5. **Park for a human, if required** A gated invocation returns `202` with `pendingApproval: true` and an approval id. The request that the approver reviews is frozen at this point, so editing the action while they decide cannot change what their yes sends. 6. **Resolve credentials and build the request** Immediately before dispatch, Anlyon re-checks that the environment is not halted, decrypts the referenced secrets, interpolates them into the frozen definition, re-validates the destination, and sends the request. A workspace secret is decrypted here, at dispatch. The read-back of a governed action resolves it again under the same bindings. 7. **Record the outcome** The invocation records the response status, the action version that ran, and the run and span it belongs to. Where the outcome cannot be confirmed it says so rather than guessing. A governed action also returns a receipt grade. See [Receipts and grades](/execution/outcomes). ## Why this is the interesting part The controls Anlyon offers are not a separate product sitting alongside your agent. They are consequences of owning the call: **[Credentials the model never sees](/execution/secrets)** The key is resolved at dispatch, inside Anlyon, from a vault with no read endpoint. It is never in the context the model reads. **[Approvals that mean something](/trust-control/approvals)** Anlyon can hold the call because Anlyon is the one making it. A wrapper around your own function can only ask your code to wait. **[A stop control](/trust-control/environments)** Halting stops new dispatch at the point requests leave. You cannot halt a request that never passed through you. **[Pinned definitions and rollback](/trust-control/promotion)** An agent version pins the exact action definitions it shipped with, so rolling back restores behaviour and not just a pointer. It does not undo a request already dispatched. **[Idempotency](/execution/idempotency)** A retried request replays the original outcome rather than performing the operation twice. **[Attribution](/execution/outcomes)** An invocation records the action version and the approval that released it. Pass a `runId` and it also names the agent and run. ## The boundary of these claims All of the above applies to **actions routed through Anlyon's hosted executor**. A tool your own code calls directly, with a credential your own process holds, is not routed through Anlyon and is not governed by it. The SDK also ships a local approval gate, `approvals.gate()`, which wraps one of your own functions and waits for a decision before it runs. It is genuinely useful when a call cannot move, and it is a different thing: your process still executes the call with your credential, so it gives you the human decision and the record, not credential isolation, and Anlyon cannot attest that your function ran or did not. See [Local approval gates](/guides/approval-gates). ## Where to go next **[Actions](/execution/actions)** Define a production call once and invoke it by name. **[Secrets](/execution/secrets)** A write-only vault, referenced from an action rather than read. **[Idempotency and retries](/execution/idempotency)** Safe replays, and why an ambiguous side effect is not retried for you. **[Receipts and grades](/execution/outcomes)** What is recorded about every call Anlyon made for you, and the grade a governed action returns. **[Governed effects](/execution/governed-effects)** Three adapters and declared actions, side by side, and what happens when a response is lost. **[Bound approvals](/execution/previews)** An approval bound to a digest of the exact request, recomputed before dispatch. **[Impact limits](/execution/impact-limits)** Shared caps on money, resource changes or a unit you declare, reserved before dispatch. --- # Actions Source: https://docs.anlyon.com/execution/actions An **action** is an HTTP request that Anlyon makes on your agent's behalf. You define it once: a method, a URL template, headers, an optional body template, and a JSON Schema describing the input it accepts. Your agent then invokes it by name with input, and never learns the rest. ## Defining one Define actions from an **operator key**, not the key your agent runs with. Creating an action needs `actions:write`, and putting a secret in the vault needs `secrets:write`. Neither belongs in a process a model is driving. ```typescript TypeScript import { Client } from '@anlyonhq/sdk'; const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! }); await ops.secrets.put('STRIPE_KEY', { value: process.env.STRIPE_KEY!, allowedHosts: ['api.stripe.com'], // the only host this key may ever be sent to allowedPlacements: ['header'], }); await ops.actions.create({ name: 'refund-order', description: 'Refund a Stripe charge, in full or in part.', method: 'POST', urlTemplate: 'https://api.stripe.com/v1/refunds', headers: { Authorization: 'Bearer {{secret:STRIPE_KEY}}', 'Content-Type': 'application/x-www-form-urlencoded', // Stripe's v1 API reads form bodies }, inputSchema: { type: 'object', properties: { charge: { type: 'string', pattern: '^ch_' }, amount: { type: 'integer' }, }, required: ['charge', 'amount'], }, requiresApproval: true, }); ``` ```python Python import os from anlyon import Client ops = Client(api_key=os.environ["ANLYON_OPERATOR_KEY"]) ops.secrets.put( "STRIPE_KEY", value=os.environ["STRIPE_KEY"], allowed_hosts=["api.stripe.com"], # the only host this key may ever be sent to allowed_placements=["header"], ) ops.actions.create( name="refund-order", description="Refund a Stripe charge, in full or in part.", method="POST", url_template="https://api.stripe.com/v1/refunds", headers={ "Authorization": "Bearer {{secret:STRIPE_KEY}}", "Content-Type": "application/x-www-form-urlencoded", # Stripe's v1 API reads form bodies }, input_schema={ "type": "object", "properties": { "charge": {"type": "string", "pattern": "^ch_"}, "amount": {"type": "integer"}, }, "required": ["charge", "amount"], }, requires_approval=True, ) ``` ### Templates `urlTemplate`, `headers` and `bodyTemplate` accept two kinds of placeholder: | Placeholder | Resolves to | | --- | --- | | `{{input.field}}` | A field from the input the agent passed, after it has been validated against `inputSchema` | | `{{input.field:path}}` | In `urlTemplate`'s path only: an input that may span several path segments | | `{{secret:NAME}}` | A vaulted secret, decrypted at dispatch and never returned to the caller | In `urlTemplate`, an input is percent-encoded into the one path segment or query value it occupies, so it cannot add `/`, `?`, `#` or `..` and change which endpoint is called: `/v1/customers/{{input.id}}` with `../../v1/refunds` requests `/v1/customers/..%2F..%2Fv1%2Frefunds`, not `/v1/refunds`. When an input really is a multi-segment path, write `{{input.field:path}}`: each segment is encoded and empty, `.` and `..` segments are refused. An input in the host (allowed only for actions that reference no secret) must be a hostname with an optional `:port`. Headers are not URL-encoded. An input value is always data: a value that looks like `{{secret:NAME}}` is sent as that text, never resolved. ### Request encoding For methods that carry a body, the body is the rendered `bodyTemplate`, or the whole input when there is no template. It is sent as JSON with `Content-Type: application/json` unless the action's own headers say otherwise. Declare `Content-Type: application/x-www-form-urlencoded` in `headers` and the body is form-encoded instead, which many older APIs require. Nested values use bracket notation (`metadata[order]=42`, `expand[0]=charge`, `items[0][price]=p_1`), and `null` values are left out rather than sent as the text `null`. The header is part of the action definition, so the encoding is versioned and captured in an approval snapshot with the rest of the request. `inputSchema` is a JSON Schema subset: `type`, `properties`, `required`, `additionalProperties`, `enum`, `pattern` and `items`. Input that does not validate is refused before anything is sent, which is the cheapest place to catch a model that invented an argument. **A secret-bearing action has a fixed destination** If an action references a secret, its scheme and host must be literal. You cannot interpolate input into the host of a URL that carries a credential, because that would let the invoker choose who receives your Stripe key. A secret in the URL's authority is refused outright: it would be exposed to DNS resolution before the request was even made. ## Invoking one This is the only part your agent needs. ```typescript TypeScript const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! }); const { data } = await anlyon.actions.invoke('refund-order', { charge: 'ch_3P9x', amount: 12000, }); if (data!.pendingApproval) { console.log('Parked for a human:', data!.approvalId); } else { console.log('Upstream replied', data!.responseStatus); } ``` ```python Python anlyon = Client(api_key=os.environ["ANLYON_AGENT_KEY"]) result = anlyon.actions.invoke("refund-order", {"charge": "ch_3P9x", "amount": 12000}) if result.data["pendingApproval"]: print("Parked for a human:", result.data["approvalId"]) else: print("Upstream replied", result.data["responseStatus"]) ``` The agent key needs `actions:invoke`. It does **not** need `secrets:read`, which is checked on the key that defines the action. It does **not** need `actions:write`. A key that can rewrite the action it is about to call is a key that can move where your credential goes. ### What comes back | Status | Meaning | | --- | --- | | `200` | The call ran inline. `responseStatus` is what the upstream API replied. | | `202` | The call is parked behind a human decision. `pendingApproval` is `true` and `approvalId` names the approval. | Poll a parked invocation with `actions.invocation(invocationId)`, or wait on the approval itself. The invocation's `status` moves through `pending_approval`, `running`, and then one of `succeeded`, `failed`, `unknown`, `denied` or `expired`. Those last few are not interchangeable. A governed action also returns a receipt `grade`. See [Receipts and grades](/execution/outcomes). ## Versions Editing an action's request, approval requirement, adapter or declarations appends an immutable version. A rename, a description change, or an enable or disable does not. Nothing updates a version in place. An agent version can pin the exact action version ids it shipped with, and the invoke path runs the pinned definition, including its approval requirement, because the gate is part of the definition. Pinning is per invocation and opt-in: pass a `runId` whose run names an agent, and if that agent's published version pins this action, the pinned definition runs. Without a `runId`, or when the agent pins nothing for this action, the current definition runs. The response reports which version actually executed as `actionVersionId`. An agent version that pins nothing rolls back to a pointer and nothing else. The mechanism exists. Using it is a discipline it cannot enforce on you. See [Immutable versions and rollback](/trust-control/promotion). ## Approval gates Setting `requiresApproval: true` on the definition sends every invocation of it through an approval. Only a policy that names the action can auto-approve it. Changing that flag needs `actions:govern`, which is not granted by default, so a key that can edit an action still cannot ungate it. An explicit approval policy governs on top of the flag. Policies are evaluated on every invocation and can require approval for a call the flag would have let through, and a policy that does not name the action cannot remove the flag's gate. See [Approvals and policies](/trust-control/approvals). **A new action name needs its own gate** Deleting an action archives it, and its versions and invocations are kept. Deleting an action that requires approval needs `actions:govern`. A key with `actions:write` can create a new action, and a policy that matches by action name applies to that name. Give a new action its own gate or policy. ## Governed actions: `impact`, `verify` and `governed` Any action may carry three more fields. An action that carries one of them is a **governed action**. Each invocation then creates an effect. The effect is reserved against [shared limits](/execution/impact-limits), its approval is [bound to the exact request](/execution/previews), and it returns a [receipt grade](/execution/outcomes). | Field | What it declares | | --- | --- | | `impact` | How much one invocation changes: `{ dimension, unit, amount, bound }`. | | `verify` | A read-back that confirms the write: `{ url, status, match }`. | | `governed: true` | Governs an action that declares neither of the other two. Its `2xx` then grades `acknowledged`. | **`impact`** - `dimension` is `money`, `resource_mutations`, or a unit you name, such as `emails`, `messages` or `rows`. A unit you name matches `^[a-z][a-z0-9_]{0,31}$`. - `unit` is the currency for `money`, for example `usd`. For every other dimension it is the dimension name and may be left out. - `amount` is an expression over the validated input. It is an integer, `input.` or `count(input.)`, with one optional multiplication by an integer. The result must be a non-negative integer. - `bound` is `exact` or `upper_bound`. An amount that reads an optional input the caller left out gives no quantity. That effect is unbounded for the dimension. A limit that applies refuses it with `impact_unbounded`. With no applicable limit it runs. **`verify`** - `url` is a `GET` on the action's own host. It may reference `{{input.field}}`, `{{response.field}}` and `{{secret:NAME}}`. - `status` is the HTTP status the read-back must answer with. - `match` is a list of up to 10 equality checks, each `{ path, equals }`. `path` is a dotted JSON path such as `data.status`. `equals` is a literal, or exactly one `{{input.field}}` or `{{response.field}}`. Anlyon issues the read-back after the provider accepts the write, with the action's own headers. A match grades the effect `confirmed`. Without a match, a `2xx` grades `acknowledged`. **Rules a governed action must meet** - The URL must be `https`, and the host is fixed by the definition. An input may not choose the host. A definition whose host comes from an input is refused when it is saved. - The verify URL must be on the action's own host. - An input that spells a secret reference is refused. - An adapter action is already governed and accepts none of the three fields. ```typescript TypeScript await ops.actions.declare('send-email', { impact: { dimension: 'emails', amount: 'count(input.to)', bound: 'exact' }, verify: { url: 'https://api.example.com/v1/messages/{{input.messageId}}', status: 200, match: [{ path: 'subject', equals: '{{input.subject}}' }], }, }); ``` ```python Python ops.actions.declare( "send-email", impact={"dimension": "emails", "amount": "count(input.to)", "bound": "exact"}, verify={ "url": "https://api.example.com/v1/messages/{{input.messageId}}", "status": 200, "match": [{"path": "subject", "equals": "{{input.subject}}"}], }, ) ``` `actions.create()` and `actions.update()` take the same three fields. The action then reads back with its declarations: ```json { "governed": true, "adapter": null, "impact": { "dimension": "emails", "unit": "emails", "amount": "count(input.to)", "bound": "exact" }, "verify": { "url": "https://api.example.com/v1/messages/{{input.messageId}}", "status": 200, "match": [{ "path": "subject", "equals": "{{input.subject}}" }] } } ``` ### Changing what an action declares - A field you leave out keeps what the action declares. - `impact: null` or `verify: null` removes that declaration. - `governed: false` removes every declaration. - Setting `impact` replaces the whole impact. The same holds for `verify`. - Each change appends a version. An agent version that pins the action pins its declarations too, and a rollback restores them with the definition. Declaring on an action that has no declaration needs `actions:write`. Changing or removing what a governed action already declares also needs `actions:govern`. No key receives that scope by default. A key that can edit an action therefore cannot lower its declared amount or take it out from under a limit. See [Govern any HTTP API](/guides/any-http-api) for a worked example. ## Importing from OpenAPI `actions.importFromOpenApi()` reads an OpenAPI document and creates one action per operation, optionally attaching a vaulted secret as a header on all of them. It turns an internal service you already describe in a spec into a set of actions an agent can invoke by name. ## How it behaves - **A governed action has an adapter or a declaration.** It creates an effect, returns a grade and counts against an impact limit. An action with neither keeps the template executor and records the HTTP response. - **Anlyon governs the calls routed through an action.** A tool your own code calls directly runs in your process with your credential. So does a function behind the local `approvals.gate()` wrapper. - **A declared action sends its write once.** It sends no idempotency key to the provider, and a replay is refused before anything is sent. A lost response is recovered by the read-back or by an operator. - **The grade of a declared action follows its `verify` rule.** A `2xx` with a matching read-back grades `confirmed`. A `2xx` with no `verify` rule grades `acknowledged`. A verify URL that reads `{{response.*}}` needs the provider response, so after a lost response an operator resolves that effect. - **The approval of a declared action binds the exact request.** The preview shows the request as it will be sent. Provider state is outside the binding. - **The amount is your expression.** For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. An `upper_bound` set too low is an error in the definition. - **A verify rule matches whatever the read-back returns.** A resource that already existed can match. Write a rule that distinguishes this write, for example a match on a value this request set. - **Rolling back restores definitions for later invocations.** A request that was already dispatched stays as it is. - **Any workspace member can decide an approval.** ## Where to go next **[Secrets](/execution/secrets)** How `{{secret:NAME}}` is resolved, and what constrains where it may go. **[Idempotency and retries](/execution/idempotency)** Invoking the same operation twice, safely. **[Approvals and policies](/trust-control/approvals)** Deciding which invocations need a person. **[Receipts and grades](/execution/outcomes)** What is recorded, the seven grades, and how to read `unknown`. --- # Governed effects Source: https://docs.anlyon.com/execution/governed-effects An action with no declaration sends one HTTP request and records what came back. A **governed effect** goes further. Anlyon knows how much the operation changes, which exact request was reviewed, and how to read the result back. So it can reserve the impact against [shared limits](/execution/impact-limits), [bind the approval](/execution/previews) to exactly that request, and return a [graded receipt](/execution/outcomes). You opt in per action, in one of two ways: - Give the action an `adapter`. Three adapters exist: `stripe.refund`, `github.file_update` and `resend.email_send`. - Declare `impact`, `verify` or `governed: true` on an HTTP action of your own. That is a **declared action**. See [Actions](/execution/actions) and [Govern any HTTP API](/guides/any-http-api). Nothing about an existing action changes unless you give it an adapter or a declaration. ## The three adapters and declared actions, side by side | | `stripe.refund` | `github.file_update` | `resend.email_send` | A declared action | | --- | --- | --- | --- | --- | | Operation | A refund of an explicit amount on a payment intent | An update to one existing text file on a designated branch | One email from a designated sending domain | The HTTPS request your definition describes | | Counted as | `money` in the action's currency, plus one `resource_mutations` | One `resource_mutations` | `emails`, one per recipient across `to`, `cc` and `bcc` | Your `impact.amount`, in your dimension | | "Succeeded" means | A Stripe Refund carrying this effect's id reaches status `succeeded` | A commit on that branch, created by this write, carries `Anlyon-Effect: ` | Resend accepted the send and returned an email id | The provider answered the write with a `2xx` | | `confirmed` means | Every success is read from Stripe's own record of the refund | Every success is read from GitHub's own record of the commit | A read-back of the email id shows this effect's tag | A read-back matched your `verify` rule | | Provider idempotency | Yes. The adapter sends the effect's `Idempotency-Key` and treats it as usable for 24 hours | No. The adapter sends the reviewed blob SHA as a precondition instead | Yes. The adapter sends the effect's `Idempotency-Key`. Resend keeps a key for 24 hours | No | | Stale-state protection | Not atomic. The adapter re-checks the account before dispatch and sends no conditional write | Atomic. The write carries the reviewed blob SHA | None. Resend has no conditional send | None. Anlyon does not know the API's preconditions | | Lookup after a lost response | By refund id, or by the `anlyon_effect` metadata on the payment intent | By a commit whose message carries the effect trailer | By email id, or by a bounded scan of recent sent emails for the `anlyon_effect` tag | By repeating the read-back, when the verify rule does not need the response | | Same-key replay | Yes, within 24 hours | No | Yes, within 24 hours | No. A replay is refused before any write | | Later completion | Tracked. A refund accepted as `pending` is read back up to 12 times. After that it stays unresolved until an operator resolves it | The commit exists when GitHub answers | Not tracked. Delivery, bounces and complaints happen after acceptance | Not tracked. A `2xx` is recorded as the result | | Undo | No. A refund cannot be reversed through the Stripe API | Yes, by a new and separately reviewed file update | No. A sent email cannot be recalled | Not known to Anlyon. Define a separate governed action for the reversal | Statements about Resend's API on this page are as of 2026-10-03. ## Define the action An adapter builds the request. You give it an explicit target and a vault secret name. There is no URL template, and no default account, branch or sending domain that could change after review. ```typescript TypeScript await anlyon.secrets.put('STRIPE_TEST_KEY', { value: process.env.STRIPE_TEST_KEY!, allowedHosts: ['api.stripe.com'] }); await anlyon.actions.create({ name: 'refund', requiresApproval: true, adapter: { type: 'stripe.refund', config: { account: 'acct_1Nxyz', mode: 'test', currency: 'usd' }, credentialSecret: 'STRIPE_TEST_KEY', }, }); await anlyon.actions.create({ name: 'update-fixture', adapter: { type: 'github.file_update', config: { owner: 'acme', repo: 'fixtures', branch: 'anlyon-test', pathPrefix: 'data/' }, credentialSecret: 'GITHUB_TOKEN', }, }); await anlyon.actions.create({ name: 'send-receipt', adapter: { type: 'resend.email_send', config: { domain: 'mail.example.com', fromAddresses: ['billing@mail.example.com'] }, credentialSecret: 'RESEND_API_KEY', // a full access key: a sending access key cannot read back }, }); ``` ```python Python anlyon.secrets.put("STRIPE_TEST_KEY", value=os.environ["STRIPE_TEST_KEY"], allowed_hosts=["api.stripe.com"]) anlyon.actions.create( name="refund", requires_approval=True, adapter={ "type": "stripe.refund", "config": {"account": "acct_1Nxyz", "mode": "test", "currency": "usd"}, "credentialSecret": "STRIPE_TEST_KEY", }, ) anlyon.actions.create( name="update-fixture", adapter={ "type": "github.file_update", "config": {"owner": "acme", "repo": "fixtures", "branch": "anlyon-test", "pathPrefix": "data/"}, "credentialSecret": "GITHUB_TOKEN", }, ) anlyon.actions.create( name="send-receipt", adapter={ "type": "resend.email_send", "config": {"domain": "mail.example.com", "fromAddresses": ["billing@mail.example.com"]}, "credentialSecret": "RESEND_API_KEY", # a full access key: a sending access key cannot read back }, ) ``` - The Stripe refund adapter takes `mode: "live"` or `mode: "test"` and a Stripe key that matches it: `sk_live_` or `rk_live_` for live, `sk_test_` or `rk_test_` for test. The examples on this page use a test key. Amounts are exact integer minor units in the action's one currency. - `github.file_update` refuses the repository's default branch unless the config says `allowDefaultBranch: true`. It refuses files it cannot read as UTF-8 text or that exceed 64 KB. It changes one existing file per effect. - `resend.email_send` needs an explicit sending `domain`. Every `from` address must be on it, and `fromAddresses` narrows that to a list. It refuses a credential that is not a Resend API key (`re_...`). `to` takes 1 to 50 addresses, and `cc` and `bcc` take up to 50 each. The input needs `html`, `text` or both. A declared action is defined like any other HTTP action, with its declarations. See [Actions](/execution/actions). ## Invoke it ```typescript TypeScript const { data } = await agent.actions.invoke( 'refund', { paymentIntent: 'pi_3Pabc', amount: 1250, currency: 'usd' }, { operationKey: 'order-4411-refund' }, ); console.log(data!.status); // pending_approval, succeeded, running (pending), unknown, failed console.log(data!.effectId); // eff_... ``` ```python Python result = agent.actions.invoke( "refund", {"paymentIntent": "pi_3Pabc", "amount": 1250, "currency": "usd"}, operation_key="order-4411-refund", ) print(result.data["status"], result.data["effectId"]) ``` **Operation identity.** Repeating the same `operationKey` with the same request returns the existing operation, and nothing is sent again. Reusing it with a different request is refused with `409 OPERATION_KEY_REUSE`. When you do not pass one, the `Idempotency-Key` is used, then a fresh key. Two *different* keys do not make two requests different business intent: two refunds of the same payment under two keys are two refunds. What bounds that is an [impact limit](/execution/impact-limits), not identity. ## Stage and outcome are different things An effect has a **stage** (where it is in Anlyon's pipeline) and an **outcome** (what happened in the world): | Outcome | Meaning | | --- | --- | | `pending` | The provider accepted the work and has not finished it (a Stripe refund in `pending`). | | `succeeded` | Evidence establishes the declared result. | | `failed` | Evidence establishes that the declared result did not happen. `partialEffect` says whether some of it might have (`possible`) or did (`confirmed`): a failure is not proof of zero impact. | | `unknown` | The available evidence cannot establish the result. | `verification` says who established the outcome: `provider` (the provider's own evidence), `manual` (an operator's resolution, never shown as provider verification), `unverified`, or `not_dispatched`. Every effect also carries a **grade**, derived from the stage, the outcome and the verification: `confirmed`, `acknowledged`, `unknown`, `failed`, `refused`, `denied` or `pending`. See [Receipts and grades](/execution/outcomes). ## When the response is lost A timeout, a reset connection, a `5xx`, or a worker that dies after sending all end the same way. The outcome is `unknown`, never `failed`, and **nothing is retried**. Anlyon schedules a reconciliation with backoff. It reads and never writes, and it asks the provider: - **Stripe**: by refund id when the response arrived, otherwise by the `anlyon_effect` metadata every refund carries. Finding the refund settles the effect. Not finding it proves nothing, because metadata can be removed after the fact, so the effect stays `unknown` with its allowance held. The adapter offers a same-key replay for 24 hours, the time it treats the idempotency key as usable. After that, an operator resolves it with evidence. - **GitHub**: by a commit on the branch whose message carries the effect trailer. Finding one confirms success. Not finding one never proves the write failed: a force-push can remove a commit that landed, and the search is bounded. So a GitHub effect whose commit cannot be found stays `unknown` until an operator resolves it with evidence. Matching file content alone is never taken as proof that Anlyon made the change. - **Resend**: by email id when the response arrived. After a lost response Resend offers no lookup by idempotency key, so Anlyon lists recent sent emails and reads candidates by id for the `anlyon_effect` tag. The scan is bounded at 3 pages and 20 reads. Finding the email settles the effect. Not finding it proves nothing, so the effect stays `unknown` with its allowance held. A same-key replay inside 24 hours is the other recovery, and a send recovered that way grades `acknowledged`. - **A declared action**: by repeating the read-back you declared. A match confirms the write. No match proves nothing, so the effect stays `unknown`. The write is never repeated. A verify rule that needs the provider response cannot run after that response was lost, and an action with no verify rule has nothing to read. Both stay `unknown` until an operator resolves them. See [When the outcome is unknown](/execution/when-the-outcome-is-unknown). ```typescript TypeScript const { data: open } = await operator.actions.effects({ unresolved: true }); const { data: effect } = await operator.actions.effect('eff_01JABCDEF'); for (const item of effect!.evidence) console.log(item.observedAt, item.source, item.summary); await operator.actions.reconcileEffect('eff_01JABCDEF'); // read-only, now await operator.actions.addEffectNote('eff_01JABCDEF', 'Asked Stripe support, ticket 81233.'); await operator.actions.resolveEffect('eff_01JABCDEF', { // manual, labelled as such outcome: 'failed', reason: 'No refund exists for pi_3Pabc in the Stripe dashboard.', }); ``` ```python Python open_effects = operator.actions.effects(unresolved=True) effect = operator.actions.effect("eff_01JABCDEF") operator.actions.reconcile_effect("eff_01JABCDEF") operator.actions.add_effect_note("eff_01JABCDEF", "Asked Stripe support, ticket 81233.") operator.actions.resolve_effect( "eff_01JABCDEF", outcome="failed", reason="No refund exists for pi_3Pabc in the Stripe dashboard.", ) ``` Evidence is append-only: every observation keeps its source, time, provider ids and redacted details, and a later observation never erases an earlier one. A confirmed outcome is never rewritten, including when the file or payment changes later. ### Rehearse it: the Stripe fault drill You cannot make Stripe lose a response on demand, so `stripe.refund` can do it for you. The drill needs `mode: "test"`. Give an action `faultDrill: "drop_response_after_send"` and Anlyon sends the refund to Stripe as usual, then discards Stripe's answer, exactly as a connection lost after the write would. The effect is recorded `unknown`, its allowance stays held, nothing is sent again, and the reconciler settles it from Stripe's record on its normal schedule (the first lookup is about 30 seconds later). ```typescript await anlyon.actions.create({ name: 'refund-drill', adapter: { type: 'stripe.refund', config: { account: 'acct_1Nxyz', mode: 'test', currency: 'usd', faultDrill: 'drop_response_after_send' }, credentialSecret: 'STRIPE_TEST_KEY', }, }); ``` The drill is part of the action's versioned config and every preview of it says "Fault drill", so nobody approves a drill without seeing it is one. A replay of a drilled effect is drilled too. Keep drills on their own action: every invocation of a drilled action loses its response. **Replay** (`replayEffect`) repeats a lost write under the *same* provider identity. It needs a provider idempotency key, so two adapters offer it: Stripe and Resend, each within a 24-hour window. GitHub file updates do not. The reviewed blob SHA cannot tell a file someone restored from one that was never written, so a repeat could apply the change twice. A declared action does not either. It has no provider idempotency key, so a replay is refused with `replay_unsupported` before anything is sent. A replay is a write, and it passes every control an original dispatch passes. The reviewed preview must still be current. The approval must hold. The action must still resolve to the reviewed version. The environment must not be halted. The requesting credential must still be active, and current policy must still allow it. A refusal from these checks answers `409` `REPLAY_REFUSED`, with a message that begins `Replay refused ():`. It is recorded in the effect's evidence and sends nothing. Two refusals come earlier and are not recorded on the effect. During a halt the call answers `409` `ENVIRONMENT_HALTED`. The key that requested the effect cannot replay it and gets `403`. Replaying needs `actions:resolve`. A replay that is admitted but fails describes that attempt, not the original write. It may have failed to reach the provider, or the provider may have refused it. The effect stays `unknown` and its allowance stays held. One write may be in flight at a time, and a replay never creates a second effect. Across every adapter, after the first dispatch only the provider's own record of the operation in a failed state settles an effect `failed`. Failing to find the operation, or failing to repeat it, never does: releasing allowance for an operation that may have happened is the error Anlyon is built to avoid. Reconcile, notes and resolve stay available while the environment is halted. Replay does not. An invocation governed by an effect is resolved through its effect, never through `POST /actions/invocations/{id}/resolve`, which refuses it with `EFFECT_RESOLUTION_REQUIRED`: settling only the invocation would leave the effect unknown and its allowance held. Reconciliation, notes, resolution and replay need `actions:resolve` for keys (reconcile needs only `actions:invoke`). The key that requested an effect can never resolve or replay it. ## Dependent work Where one operation must only follow another, say so. Pending and unknown outcomes never satisfy a dependency: ```typescript TypeScript await agent.actions.invoke('update-fixture', { path: 'data/refunds.txt', content: 'order-4411: refunded\n', message: 'Record refund' }, { dependsOn: [{ effect: 'eff_01JABCDEF', outcome: 'succeeded' }], }); ``` ```python Python agent.actions.invoke( "update-fixture", {"path": "data/refunds.txt", "content": "order-4411: refunded\n", "message": "Record refund"}, depends_on=[{"effect": "eff_01JABCDEF", "outcome": "succeeded"}], ) ``` A compensation (undoing an effect) is a new governed effect with its own preview, approval, impact and outcome. Invoke the compensating action with `compensates: 'eff_...'`. ## How it behaves **`stripe.refund`** - One currency per action, and every refund carries an explicit amount. - The refundable amount in a preview is as of review. The adapter re-checks the account before dispatch. **`github.file_update`** - One existing UTF-8 text file per effect, up to 65536 bytes. - The write carries the reviewed blob SHA as its precondition. - A commit that lookup cannot find leaves the effect `unknown` until an operator resolves it with evidence. **`resend.email_send`** - One sending domain per action, and one email per effect. - The vault secret is a full access key. The read-back and the lookup use it. - Recovery after a lost response is a bounded search or a same-key replay inside 24 hours. - Confirmed is Resend's record of the send, not inbox delivery. **A declared action** - The write is sent once, with no provider idempotency key. A replay is refused before anything is sent. - A `2xx` with a matching read-back grades `confirmed`. A `2xx` with no `verify` rule grades `acknowledged`. - A verify URL that reads `{{response.*}}` needs the provider response. After a lost response an operator resolves that effect. - The declared amount is evaluated as you wrote it. - A verify rule can match a resource that already existed. Write a rule that distinguishes this write. **Every governed effect** - A lost response that lookup cannot find stays `unknown`, with its allowance held, until a replay inside the provider's window or an operator's resolution. An operator's resolution is labelled `manual` and is never shown as provider verification. - The Stripe fault drill is an injected fault. Anlyon discards a response that did arrive. - A receipt is a database record with provider evidence. - A governed effect comes from an action with an adapter or a declaration. --- # Idempotency and retries Source: https://docs.anlyon.com/execution/idempotency Two different things get confused here, so this page separates them: - **Idempotency** is how you retry safely. You control it, with a key. - **Automatic retry** is something Anlyon does for delivery and deliberately does *not* do for an action invocation. ## Idempotency keys Send an `Idempotency-Key` header on a mutation and the key is claimed atomically before the handler runs, so concurrent retries perform the side effect at most once. ```typescript TypeScript const { data } = await anlyon.actions.invoke( 'refund-order', { charge: 'ch_3P9x', amount: 12000 }, { idempotencyKey: `refund:${orderId}` }, ); ``` ```python Python result = anlyon.actions.invoke( "refund-order", {"charge": "ch_3P9x", "amount": 12000}, idempotency_key=f"refund:{order_id}", ) ``` For operations other than action invocations, the cached-response rules are: | Situation | What happens | | --- | --- | | Same key, same request | The original response replays, with an `X-Idempotent-Replay: true` header. | | Same key, different request | `409` with error code `idempotency_key_reuse`. "Same request" means the same method, path, workspace, environment, credential and JSON body. | | Duplicate arrives while the first is still in flight | It waits, then replays. If the first does not finish in time, the duplicate gets `409 idempotency_request_in_progress`. | | The original request failed | The key is freed, so you can retry it. Only successful responses are stored. | | More than 24 hours later | Stored responses are kept for 24 hours. For most operations the key is then unknown again. **Action invocations are the exception:** see below. | Choose a key derived from the operation, not from the attempt: `refund:ord_42` is the point, and `refund:${Date.now()}` defeats it. From `@anlyonhq/sdk` 2.2.0 and `anlyon` 0.5.0 the SDK sends an idempotency key on every invoke and generates one when you do not pass one. A resend is answered from the recorded invocation. Earlier versions send a key only when you pass one, so pass one. A generated key covers one call to `invoke`. When your own code can call `invoke` twice for one business operation, pass your own key. ### Action invocations keep their key On `POST /api/v2/actions/{ref}/invoke`, the key is also recorded on the invocation itself and kept for as long as the invocation exists. For gated actions, invocation creation, approval admission and approval linkage commit together. A refused admission leaves no invocation or reserved key. The action-specific rules are: | Situation | What happens | | --- | --- | | Same key, same request, any time later | Answered from the invocation **as it is now**, with `X-Idempotent-Replay: current-state`: `200`, or `202` while it still waits for approval. It is not executed, admitted or charged again. | | The invocation failed, or is `unknown` | The retry shows that outcome. The key is not freed: running the action again is a new decision. Resolve an `unknown` one first, then invoke with a new key and `retryOf`. See [Resolving an unknown outcome](/execution/outcomes#resolving-an-unknown-outcome). | | The request was refused before an invocation existed (invalid input, action disabled, quota) | Nothing was recorded, so the key is still unused. | | Same key from another credential, or with a different body | `409 idempotency_key_reuse`, without revealing the invocation. | This holds for dashboard sessions as well as API keys and OAuth, and when the replay store has lost the key. **A lost cache is not a licence to re-run** If a key's short-term record is gone and no invocation holds it, but a durable budget reservation for the same effect still exists, the retry gets `409 BUDGET_EFFECT_ALREADY_RESERVED` rather than executing again. Investigate the original outcome before choosing a new key: a new key can duplicate the work. Operations that accept an idempotency key are marked in the [API reference](/api-reference). Both SDKs accept one on every operation that supports it, and CI fails if that ever stops being true. ## Provider idempotency on governed actions The `Idempotency-Key` above is between you and Anlyon. A [governed action](/execution/governed-effects) also has an identity between Anlyon and the provider, and it differs by adapter. | Action | Key Anlyon sends to the provider | Replay of a lost write | | --- | --- | --- | | `stripe.refund` | The effect's `Idempotency-Key`, persisted before the write. The adapter treats the key as usable for 24 hours. | Offered inside 24 hours. After that a repeat could create a second refund. | | `resend.email_send` | The effect's `Idempotency-Key`. As of 2026-10-03, Resend keeps keys for 24 hours and returns its original response for the same key and payload, sending nothing. | Offered inside 24 hours. After that a repeat could send a second email. | | `github.file_update` | None. The adapter sends no idempotency key. It sends the reviewed blob SHA as a precondition instead. | Not offered. | | A declared action | None. A declared action sends no provider idempotency key. | Not offered. A replay is refused before any write. | A replay of a declared action answers `409 REPLAY_REFUSED` with the reason `replay_unsupported`, and nothing is sent: ```json { "success": false, "error": { "code": "REPLAY_REFUSED", "message": "Replay refused (replay_unsupported): ..." } } ``` Reconciliation of a declared action repeats the read-back, never the write. On a governed action you can also pass an `operationKey`. Repeating the same `operationKey` with the same request returns the existing operation, and nothing is sent again. Reusing it with a different request is refused with `409 OPERATION_KEY_REUSE`. When you pass none, the `Idempotency-Key` is used. ## Why an invocation is not retried for you When Anlyon dispatches an action and the request times out, or the socket dies after the request was written, the destination **may have processed it**. Anlyon records that as `unknown` rather than `failed`, and stops. It would be easy to retry. It would also be how one refund becomes two. So the contract is: Anlyon tells you what it knows, including when what it knows is "I cannot confirm this", and you reconcile with the destination before deciding. An `unknown` invocation carries a message saying so. See [Receipts and grades](/execution/outcomes) for how to read the statuses. An idempotency key does not make Anlyon send the request again. A retry with the same key and the same request is answered from the recorded invocation, and nothing reaches the destination. To run a `failed` invocation again, invoke with a new key and `retryOf`. An `unknown` invocation must be resolved first. See [Retrying on purpose](/execution/outcomes#retrying-on-purpose). ## Retries that do happen Event deliveries to your webhook subscriptions retry with backoff, and a delivery that exhausts its retries lands in a dead letter queue. Those retries apply to **delivery**, not to action invocations. The distinction is not an oversight in one direction or the other. A webhook delivery is safe to retry because the receiver is expected to be idempotent about it. A refund is not, and pretending otherwise would be the most expensive default we could ship. ## How it behaves - **Idempotency keys are the caller's.** Two different keys are two operations: two refunds of the same payment under two keys are two refunds. An [impact limit](/execution/impact-limits) bounds that. - **An action invocation is sent once.** When the outcome cannot be confirmed, the invocation is recorded `unknown` and you reconcile before retrying. - **A declared action sends its write once.** It sends no provider idempotency key. A replay is refused before anything is sent, and reconciliation repeats the read-back. - **A provider key is usable for 24 hours.** The Stripe and Resend adapters offer a same-key replay inside that window. - **A GitHub file update is recovered by lookup.** The adapter sends the reviewed blob SHA as a precondition, and reconciliation looks for the commit that carries the effect trailer. - **A repeated key is answered from the recorded invocation.** For an action with no declaration and no adapter, nothing is sent to the destination again. ## Where to go next **[Receipts and grades](/execution/outcomes)** Reading `succeeded`, `failed` and `unknown`, and the grade on a governed action. **[Actions](/execution/actions)** Defining and invoking the call. --- # Impact limits Source: https://docs.anlyon.com/execution/impact-limits [Budgets](/trust-control) cap what Anlyon does for you, per key. **Impact limits** cap what your agents do to the world. A limit counts one of three things: | `dimension` | What it counts | `unit` | | --- | --- | --- | | `money` | Integer minor units in one currency | The currency, for example `usd` | | `resource_mutations` | Changes to resources | `resource_mutations` | | A unit you declare, such as `emails`, `messages` or `rows` | Whatever the action's `impact.amount` evaluates to | The dimension name | Limits apply to **governed actions**. That means an action with an adapter, or an action that carries a declaration. See [Actions](/execution/actions) for the fields. A limit is keyed on things the server controls: the environment, the dimension and unit, an optional action name, an optional resource prefix, and a period (`day`, `month` or `total`). A `day` is the UTC day. Nothing in that key belongs to the caller. A new session, a new API key or a child agent does not get a new allowance. ```typescript TypeScript await operator.impactLimits.create({ name: 'Refunds per day', dimension: 'money', currency: 'usd', period: 'day', amount: 50_000, // $500.00 }); await operator.impactLimits.create({ name: 'Fixture edits', dimension: 'resource_mutations', period: 'day', amount: 20, resourcePrefix: 'github:acme/fixtures@', }); await operator.impactLimits.create({ name: 'Daily emails', dimension: 'emails', period: 'day', amount: 500, }); const { data } = await agent.impactLimits.list(); // [{ limit, unit, consumed, reserved, unknownExposure, available, ... }] ``` ```python Python operator.impact_limits.create(name="Refunds per day", dimension="money", currency="usd", period="day", amount=50_000) operator.impact_limits.create( name="Fixture edits", dimension="resource_mutations", period="day", amount=20, resource_prefix="github:acme/fixtures@", ) operator.impact_limits.create(name="Daily emails", dimension="emails", period="day", amount=500) limits = agent.impact_limits.list().data ``` A limit in a declared unit reads back with that unit and no currency: ```json { "dimension": "emails", "unit": "emails", "currency": null, "limit": 5, "available": 5 } ``` Creating and changing limits needs `impact-limits:write`. No key receives it by default, so the agent a limit constrains cannot raise it. Reading limits needs `impact-limits:read`, which is in the default set. ## Where the quantity comes from | Action | Quantity | | --- | --- | | `stripe.refund` | The refund amount in the action's one currency, plus one resource mutation. | | `github.file_update` | One resource mutation per file update. | | `resend.email_send` | One `emails` unit per recipient, counted across `to`, `cc` and `bcc`. | | A declared action | The action's `impact.amount`, evaluated over the validated input before dispatch. | A caller cannot pass a quantity apart from the request. For a declared action the quantity is whatever the declared expression gives for that input. An amount that reads an optional input the caller left out gives no quantity. That effect is unbounded for the dimension. A limit that applies refuses it with `impact_unbounded`. With no applicable limit it runs. ## How capacity moves 1. **Reserve** immediately before dispatch, after any approval. Every applicable limit is reserved in one transaction. If one has no room, none is reserved and nothing is sent. 2. **Settle** the confirmed impact exactly once, into the period the reservation was made in, even if the outcome arrives next month. 3. **Release** what is known not to have happened, and nothing else. 4. **Hold** everything else. Pending, unknown and possibly partial failures keep their capacity, across restarts and period boundaries, until evidence or an operator settles them. `reserved` includes that held exposure. `unknownExposure` is the part of it whose outcome is unknown or possibly partial. It is not an additional amount. `available` is `limit - consumed - reserved`. Repeated attempts of one effect (a replay, a duplicate submission of the same operation) never consume twice. Repeated legitimate changes to the same resource are separate operations and count separately. Lowering a limit never releases existing exposure. It blocks new work until availability recovers. Archiving a limit stops it applying to new effects while its reservations still settle. ## When the limit has no room An operation is admitted whole or refused whole. A send to four recipients with two left is refused, none of the four is sent, and the two stay available. A refused effect records why, and nothing reaches the provider: ```json { "stage": "rejected", "outcome": "not_dispatched", "rejectionReason": "impact_limit_exceeded", "grade": "refused" } ``` The invocation's `status` is `failed`, its `grade` is `refused`, and its error names `impact_limit_exceeded`. See [When the limit is hit](/execution/when-the-limit-is-hit). ## How it behaves - **A limit counts governed actions.** A governed action has an adapter or a declaration. Give an action a declaration to put it under a limit. - **A limit counts the calls routed through Anlyon.** Keep the provider key in the vault so the action is the path to the provider. - **A limit belongs to one environment and scope.** Each environment has its own. - **The amount of a declared action is your expression.** For an API you declare, you write what the action counts, and Anlyon enforces it before dispatch. An `upper_bound` set too low is an error in the definition. - **An effect with no quantity runs when no limit applies to it.** It is refused under a limit that applies, and runs otherwise. - **A limit can be changed.** An operator holding `impact-limits:write` can raise, lower or archive it. - **A limit and a budget count different things.** An impact limit caps what an action changes, such as the money it moves. A per-key budget caps the Anlyon operations a key may perform. --- # Receipts and grades Source: https://docs.anlyon.com/execution/outcomes 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. --- # Bound approvals Source: https://docs.anlyon.com/execution/previews 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. --- # Secrets Source: https://docs.anlyon.com/execution/secrets Secrets are the reason the execution boundary is worth having. An [action](/execution/actions) references a credential as `{{secret:NAME}}`. Anlyon decrypts it at the moment of dispatch, interpolates it into the request it is about to send, and sends it. The value is never returned to the caller, never written to a log, and never present in the context the model reads. ## There is no read endpoint Secrets go in. Nothing gets them out. ```typescript TypeScript await ops.secrets.put('STRIPE_KEY', { value: process.env.STRIPE_KEY!, allowedHosts: ['api.stripe.com'] }); // Metadata only. There is no method that returns a value, because there is // no endpoint behind one. const { data } = await ops.secrets.list(); ``` ```python Python ops.secrets.put("STRIPE_KEY", value=os.environ["STRIPE_KEY"], allowed_hosts=["api.stripe.com"]) # Metadata only. result = ops.secrets.list() ``` `secrets.get(name)` returns the name, description, the binding described below, and when it was last used. It does not return the value, and neither does any other operation on any surface. This is not an ACL you could misconfigure. It is an endpoint that was never built. Writing a secret needs `secrets:write`. **Referencing one needs `secrets:read`**, because a reference is a use: a key that can put `{{secret:STRIPE_KEY}}` into an action is a key that can cause that secret to be sent somewhere. ## Where a secret may go Two constraints apply, one always and one when you configure it. ### Always: a secret-bearing action has a literal destination An action that references a secret must have a fixed scheme and host. You cannot interpolate `{{input.*}}` into the host of a URL that carries a credential, because the invoker would then choose the recipient of your key. A secret placed in the URL's authority is refused outright: it would be handed to DNS resolution before the request was made. This holds for every secret-bearing action with no configuration on your part. ### Optionally: bind the secret itself A secret can also carry its own policy, independent of any action: | Field | Meaning | | --- | --- | | `allowedHosts` | Hosts this secret may be sent to. A leading dot matches subdomains, so `.stripe.com` admits `api.stripe.com`. Null or omitted means any public host. | | `allowedPlacements` | Where in the request it may appear: `url`, `header` or `body`. Null or omitted means anywhere. A credential in a header is the intended use. The same value in a URL is usually a mistake or an exfiltration. | Both are checked when an action is authored. `allowedHosts` is checked **again at fire time** against the host actually being called, so an action that predates a tightened host list stops working on its next call rather than on its next edit. `allowedPlacements` is checked again at dispatch on a declared action. On an action with no declaration, a tightened placement applies at the next edit. Omitting `allowedHosts` on a rotation leaves the existing value alone, so rotating a secret never quietly widens where it may go. **Setting the binding** Both SDKs take the two fields on `secrets.put()`: `allowedHosts` and `allowedPlacements` in TypeScript, `allowed_hosts` and `allowed_placements` in Python. They are part of the HTTP contract on `PUT /api/v2/secrets/{name}`, so a direct request works too: ```bash curl -X PUT https://api.anlyon.com/api/v2/secrets/STRIPE_KEY \ -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "'"$STRIPE_KEY"'", "allowedHosts": [".stripe.com"], "allowedPlacements": ["header"] }' ``` ## Secrets in a governed action A [governed action](/execution/governed-effects) persists the request it will send, shows it to the reviewer and hashes it into the approval's digest. None of those holds a credential. - A **declared action** keeps every `{{secret:NAME}}` reference as a placeholder in the stored request. The reviewer sees `Authorization: Bearer {{secret:MAILER_TOKEN}}`, and the digest covers that text. - Anlyon resolves the placeholders at dispatch, when the request is sent. The host and placement bindings of each secret are checked again at that point. - The read-back of a `verify` rule goes out with the action's own headers, so it uses the same secret under the same bindings. - An input that spells a secret reference is refused. An input can never cause a secret to be resolved. - An **adapter action** names one vault secret as `credentialSecret`. The adapter checks the key's shape. A binding tightened after review is enforced at dispatch. The approved effect is refused, and nothing is sent: ```json { "stage": "rejected", "rejectionReason": "credential_unavailable", "grade": "refused" } ``` ## Rotation `secrets.put()` on an existing name rotates it. The next dispatch resolves the new value. In-flight approvals resolve whatever is current when they finally execute, not what was current when the human clicked approve. An approval snapshot freezes the request that was reviewed. It does not preserve the right to use a credential that has since been revoked. ## How it behaves - **The vault covers credentials stored in it**, used by actions Anlyon dispatches. A key your own process holds stays in your process, including behind the local [approval gate](/guides/approval-gates). - **A limit, an approval and a receipt apply to calls routed through the action.** Keep the key in the vault and out of the agent's process. - **A credential is resolved at dispatch.** A secret rotated, removed or rebound after review is resolved as it stands then. - **The vault holds operator secrets, not end-user tokens.** - **The delivery-signing secret is workspace-wide.** A signed delivery names its environment inside the signature, and the key material and its rotation are shared across the environments of a workspace. Action invocations carry no Anlyon delivery signature. A notification webhook carries no `Anlyon-Signature`. It carries `X-Anlyon-Signature: sha256=` when a webhook secret is set, and nothing otherwise. - **Environments are logical isolation.** Secrets belong to an environment through an enforced column in shared storage. - **The Resend adapter takes a full access key, as of 2026-10-03.** The read-back and the lookup use it. ## Where to go next **[Actions](/execution/actions)** Define the call that references the secret. **[Environments](/trust-control/environments)** Secrets belong to an environment, like everything else a key can reach. **[Authentication and scopes](/authentication)** Why the agent key and the operator key are different keys. **[Receipts and grades](/execution/outcomes)** Responses are scrubbed before they are stored. --- # When approval expires or changes Source: https://docs.anlyon.com/execution/when-approval-expires-or-changes An approval of a governed action is an approval of one [preview](/execution/previews). The preview has an expiry and a `bindingDigest`. This page lists each way that approval can stop being valid, where the refusal happens, and what the caller sees. There are three places a call can stop: | Where | What the caller sees | Effect created | | --- | --- | --- | | **Admission**, when the invoke request arrives | HTTP 409 with an error code | No | | **Dispatch**, after approval and immediately before the write | HTTP 200, `status: "failed"`, `grade: "refused"` | Yes, `stage: "rejected"`, zero attempts | | **The provider**, after Anlyon sent the write | HTTP 200, `status: "failed"`, `grade: "failed"` | Yes, one attempt | The responses below come from Anlyon's CI test runs. ## Preview expiry and approval expiry A preview you create with `actions.preview` lasts one hour by default. Pass `ttlSeconds` to change that, from 60 seconds to 7 days. An invocation with no `previewId` builds its own preview. That preview lasts as long as the approval window, which is 24 hours by default, so it does not shorten the time a reviewer has. **The approval never outlives the preview.** When an approval is created for a governed effect, its expiry is the earlier of the approval window and the preview's expiry. A reviewer cannot approve after the reviewed evidence has expired. When an approval expires undecided: - the approval's status is `expired`, and a later attempt to decide it returns HTTP 409 `APPROVAL_ALREADY_DECIDED` - the invocation's status is `expired` - the effect is rejected with `rejectionReason: "approval_expired"`, which grades `refused` - nothing was sent Expiry is inside the digest. A preview whose stored expiry was altered no longer verifies, and dispatch refuses it. To continue after an expiry, create a new preview and a new invocation. **Refreshing** a preview re-reads the provider into a new preview that supersedes the old one. Its binding differs, so an approval of the old preview never carries over. ```typescript TypeScript const { data: fresh } = await agent.actions.refreshPreview('prv_01JABCDEF', { ttlSeconds: 600 }); console.log(fresh!.id, fresh!.supersedes, fresh!.bindingDigest); ``` ```python Python fresh = agent.actions.refresh_preview("prv_01JABCDEF", ttl_seconds=600).data print(fresh["id"], fresh["supersedes"], fresh["bindingDigest"]) ``` ```bash curl curl -s -X POST https://api.anlyon.com/api/v2/actions/previews/prv_01JABCDEF/refresh \ -H "Authorization: Bearer $ANLYON_AGENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "ttlSeconds": 600 }' ``` ## A changed request: `PREVIEW_MISMATCH` An invocation that carries a reviewed `previewId` must match the reviewed request. If the destination, resource, amount or content differs, the invoke is refused at admission. ```typescript TypeScript import { AnlyonError } from '@anlyonhq/sdk'; const { data: preview } = await agent.actions.preview('send-email', { emailId: 'welcome-1', to: ['a@example.com'], subject: 'Welcome', }); try { // One more recipient than the reviewer saw. await agent.actions.invoke( 'send-email', { emailId: 'welcome-1', to: ['a@example.com', 'b@example.com'], subject: 'Welcome' }, { previewId: preview!.id }, ); } catch (error) { if (error instanceof AnlyonError && error.status === 409) console.log(error.message); } ``` ```python Python from anlyon import AnlyonError preview = agent.actions.preview( "send-email", {"emailId": "welcome-1", "to": ["a@example.com"], "subject": "Welcome"} ).data try: # One more recipient than the reviewer saw. agent.actions.invoke( "send-email", {"emailId": "welcome-1", "to": ["a@example.com", "b@example.com"], "subject": "Welcome"}, preview_id=preview["id"], ) except AnlyonError as error: if error.status == 409: print(error) ``` ```bash curl curl -s -w '\n%{http_code}\n' -X POST https://api.anlyon.com/api/v2/actions/send-email/invoke \ -H "Authorization: Bearer $ANLYON_AGENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "emailId": "welcome-1", "to": ["a@example.com", "b@example.com"], "subject": "Welcome" }, "previewId": "prv_01JABCDEF" }' ``` The response is HTTP **409**. ```json { "success": false, "error": { "code": "PREVIEW_MISMATCH", "message": "Preview prv_01JABCDEF does not match this invocation: the destination, resource, amount or content differs." } } ``` The body also carries a `requestId`. No effect is created, so there is no grade and no `effectId`. Nothing is sent to the provider. `PREVIEW_MISMATCH` covers more than the request. The message after the colon says which part differs: | What differs | Message ends with | | --- | --- | | The request | `the destination, resource, amount or content differs.` | | The action | `it previews another action.` | | The action version | `it reviewed ; this invocation runs .` | | The adapter version | `the adapter has changed since review.` | | The stored binding | `its binding does not verify.` | Two more codes come from the same check, both HTTP 409 at admission: | Code | Message | Meaning | | --- | --- | --- | | `PREVIEW_EXPIRED` | `Preview expired at