---
title: "Governed effects"
description: "One persisted business operation per request: reserved against shared limits, bound to its approval, graded, and recoverable when a response is lost."
---

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: <effect id>` | 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 (<reason>):`. 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.
