---
title: "When the outcome is unknown"
description: "What unknown means for a governed action, why Anlyon does not send the request again, why the allowance stays held, and the four ways an unknown outcome settles."
---

Sometimes a write leaves Anlyon and no usable answer comes back. The provider may have applied it. Anlyon records that outcome as `unknown`. It does not record `failed`, and it does not send the request again.

The responses below come from Anlyon's CI test runs.

## What `unknown` means

`unknown` means the available evidence cannot establish the result. It happens when:

- the request timed out or the connection was reset after the request was sent
- the provider answered with a `5xx`
- the worker handling the dispatch stopped before it recorded an outcome

It is not a failure. A request that provably never left Anlyon is `failed`, and a provider's `4xx` is usually `failed`. The exception is a `409` to `stripe.refund` or `resend.email_send`, which is `unknown`: it can mean the idempotency key is still in use, which proves nothing either way. `unknown` is reserved for a write that may have happened.

```typescript TypeScript
const { data } = await agent.actions.invoke('send-email', {
  emailId: 'welcome-1',
  to: ['a@example.com', 'b@example.com'],
  subject: 'Welcome',
});

if (data!.status === 'unknown') {
  // Do not invoke again. Keep the effect id and read it later.
  console.log(data!.grade, data!.effectId, data!.error);
}
```

```python Python
result = agent.actions.invoke(
    "send-email",
    {"emailId": "welcome-1", "to": ["a@example.com", "b@example.com"], "subject": "Welcome"},
)

if result.data["status"] == "unknown":
    # Do not invoke again. Keep the effect id and read it later.
    print(result.data["grade"], result.data["effectId"], result.data["error"])
```

```bash curl
curl -s -X POST https://api.anlyon.com/api/v2/actions/send-email/invoke \
  -H "Authorization: Bearer $ANLYON_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-1" \
  -d '{ "input": { "emailId": "welcome-1", "to": ["a@example.com", "b@example.com"], "subject": "Welcome" } }'
```

The HTTP status is 200.

```json
{ "status": "unknown", "grade": "unknown" }
```

The invocation's `error` carries this text:

```text
The provider may have applied effect <effect id>; Anlyon is reconciling it. Do not retry; inspect the effect.
```

The effect, read while unknown:

```json
{
  "outcome": "unknown",
  "stage": "reconciling",
  "verification": "unverified",
  "unresolvedExposure": true
}
```

On the same effect `reconciliation.nextAt` is set: a lookup is already scheduled.

From the CLI, an invoke whose outcome is unknown exits 13.

## A request is sent once

Nothing in the effect lifecycle retries a write. A dispatch is claimed once, and the database refuses a second dispatch attempt for one effect. A worker that dies after sending leaves the effect `unknown`. The effect is reconciled, never dispatched again.

The reason is the cost of being wrong. A repeated refund is a second refund. A repeated send is a second email. A lookup costs one read.

## The allowance stays held

The effect reserved its impact before dispatch. While the outcome is `unknown`, that reservation is neither consumed nor released. It is held.

```json
{ "consumed": 0, "reserved": 2, "unknownExposure": 2 }
```

Two emails are held against the limit. `available` is `limit - consumed - reserved`, so other callers have that much less room. The hold survives restarts and period boundaries. Releasing allowance for an operation that may have happened is the error this design avoids. See [When the limit is hit](/execution/when-the-limit-is-hit).

A dependent effect does not start either. `unknown` and `pending` never satisfy a `dependsOn`.

## How it settles

There are four ways out of `unknown`.

### 1. The scheduled read-back

Anlyon schedules a lookup when it records the unknown outcome. The lookup is a read. It asks the provider what happened to this operation and never writes.

- **A declared action** repeats its verify rule: one `GET`, compared with the declared status and checks.
- **`stripe.refund`** looks the refund up by id, or by the `anlyon_effect` metadata every refund carries.
- **`github.file_update`** looks for a commit on the branch whose message carries the effect trailer.
- **`resend.email_send`** reads the email by id, or scans recent sent emails for the `anlyon_effect` tag. The scan is bounded at 3 pages and 20 reads.

A match settles the effect. For the declared action above:

```json
{
  "stage": "settled",
  "outcome": "succeeded",
  "grade": "confirmed",
  "verification": "provider",
  "attempts": [{ "kind": "dispatch" }, { "kind": "reconcile" }]
}
```

The destination saw one write and one read. The limit then read:

```json
{ "consumed": 2, "reserved": 0, "unknownExposure": 0 }
```

The settled amount goes into the period the reservation was made in, even when the answer arrives in a later period.

### 2. The on-demand reconcile endpoint

You do not have to wait for the schedule. Ask for the same lookup now.

```typescript TypeScript
const { data: effect } = await operator.actions.reconcileEffect('eff_01JABCDEF');
console.log(effect!.outcome, effect!.grade, effect!.verification);
```

```python Python
effect = operator.actions.reconcile_effect("eff_01JABCDEF").data
print(effect["outcome"], effect["grade"], effect["verification"])
```

```bash curl
curl -s -X POST https://api.anlyon.com/api/v2/actions/effects/eff_01JABCDEF/reconcile \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
```

This needs `actions:invoke`. It stays available while the environment is halted, so a halt never strands held exposure.

```json
{ "outcome": "succeeded" }
```

That call returned HTTP 200 during a halt. An effect that is already `succeeded` or `failed` answers HTTP 409: a settled outcome is not reconciled again. If another lookup or write for the effect is in flight, the call also answers 409 and you try again shortly.

### 3. A same-key replay, where the adapter supports it

A replay repeats the lost write under the same provider identity. It is safe when the provider deduplicates on an idempotency key, and inside that key's window.

- **`stripe.refund`** supports it for 24 hours. The adapter sends the same idempotency key again and treats that key as usable for 24 hours.
- **`resend.email_send`** supports it for 24 hours. Resend keeps an idempotency key for 24 hours, as of 2026-10-03. A send recovered that way grades `acknowledged`.
- **`github.file_update`** does not.
- **A declared action** does not. It sends no provider idempotency key, so a repeated write could apply twice.

A replay is a write. It passes every control a dispatch passes, and it needs `actions:resolve`. The key that requested the effect cannot replay it.

```typescript TypeScript
await operator.actions.replayEffect('eff_01JABCDEF');
```

```python Python
operator.actions.replay_effect("eff_01JABCDEF")
```

```bash curl
curl -s -X POST https://api.anlyon.com/api/v2/actions/effects/eff_01JABCDEF/replay \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
```

On a declared action the replay is refused before anything is sent. The response is HTTP 409.

```json
{
  "success": false,
  "error": {
    "code": "REPLAY_REFUSED",
    "message": "Replay refused (replay_unsupported): ..."
  }
}
```

The message names the reason. The hosted API does not return `error.details`. The refusal is recorded in the effect's evidence, and the outcome stays `unknown`. In a test run the destination still showed one write.

A replay that is admitted and then fails says nothing about the original write. The effect stays `unknown` and its allowance stays held.

### 4. An operator's resolution, labelled `manual`

When no lookup can settle it, a person checks the provider and records what they found.

```typescript TypeScript
await operator.actions.addEffectNote('eff_01JABCDEF', 'Asked the mailer support desk, ticket 81233.');

const { data: resolved } = await operator.actions.resolveEffect('eff_01JABCDEF', {
  outcome: 'succeeded',
  reason: 'Checked the mailer dashboard: the message was sent.',
});
console.log(resolved!.grade, resolved!.verification);
```

```python Python
operator.actions.add_effect_note("eff_01JABCDEF", "Asked the mailer support desk, ticket 81233.")

resolved = operator.actions.resolve_effect(
    "eff_01JABCDEF",
    outcome="succeeded",
    reason="Checked the mailer dashboard: the message was sent.",
).data
print(resolved["grade"], resolved["verification"])
```

```bash curl
curl -s -X POST https://api.anlyon.com/api/v2/actions/effects/eff_01JABCDEF/resolve \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "succeeded", "reason": "Checked the mailer dashboard: the message was sent." }'
```

```json
{ "outcome": "succeeded", "grade": "acknowledged", "verification": "manual" }
```

A manual `succeeded` grades `acknowledged`, never `confirmed`. It keeps `verification: "manual"` everywhere it is shown. A manual `failed` releases the allowance.

Resolving and adding notes are for admins in the console, or a key holding `actions:resolve`, which no key receives by default. The `reason` must be at least 10 characters. The key that requested an effect can never resolve it. A confirmed outcome is not rewritten: resolving an effect that is already settled is refused.

## Not finding it never becomes `failed`

A lookup that finds nothing is absence of evidence. Metadata can be removed, a commit can be force-pushed away, and a provider's list can lag. So a lookup that does not find the operation leaves the effect `unknown`, with its allowance held.

In a test run a declared write timed out and three read-backs then found nothing:

```json
{ "outcome": "unknown", "grade": "unknown", "unresolvedExposure": true }
```

```json
{ "consumed": 0, "reserved": 1, "unknownExposure": 1 }
```

The evidence for each lookup reads:

```text
The read-back did not match the declared verify rule. That does not prove the write failed, so the outcome stays unknown.
```

After dispatch, one thing settles an effect `failed` without a person: the provider's own record of this operation in a failed state. A Stripe refund that Stripe reports as failed is that. Failing to find the operation is not. Failing to replay it is not.

## A verify URL that needs the response

Some APIs choose the resource id and return it in the response. The verify URL then uses `{{response.id}}`. That rule works when the response arrives. It cannot run when the response is lost, because the id was in the lost response.

In a test run that action confirmed a normal send. Then a send lost its response, and reconciliation was requested:

```json
{ "outcome": "unknown", "grade": "unknown" }
```

Reconciliation sent no request at all. The evidence reads:

```text
The verify rule needs the provider response, which was lost. The outcome stays unknown until an operator resolves it.
```

This is stated before the write. The preview's `limitations` block carries an `unsupported` item about read-back after a lost response. An operator's resolution is the way out for such an effect.

To avoid it, let the caller choose the id. An action that writes to `/v1/emails/{{input.emailId}}` and verifies at the same URL can be read back after a lost response. An action with no verify rule cannot be read back at all, and a lost response on it also waits for an operator.

## Backoff, the attempt cap, then `reconcile exhausted`

Scheduled lookups back off. The delay starts at 30 seconds and doubles after each attempt, up to one hour. After 12 attempts automatic reconciliation stops.

```json
{ "reconciliation": { "exhausted": true, "nextAt": null } }
```

The effect's evidence gains an entry that begins "Automatic reconciliation stopped". The outcome is still `unknown` and the allowance is still held. Nothing times out into `failed`.

An exhausted effect needs a person. An on-demand reconcile still works and runs one more lookup. Otherwise resolve it with evidence. Find these effects with the unresolved filter:

```typescript TypeScript
const { data: open } = await operator.actions.effects({ unresolved: true });
const { data: unknown } = await operator.actions.effects({ grade: 'unknown' });

for (const effect of open ?? []) {
  console.log(effect.id, effect.outcome, effect.reconciliation.attempts, effect.reconciliation.exhausted);
}
```

```python Python
open_effects = operator.actions.effects(unresolved=True).data
unknown = operator.actions.effects(grade="unknown").data

for effect in open_effects:
    print(effect["id"], effect["outcome"], effect["reconciliation"]["attempts"], effect["reconciliation"]["exhausted"])
```

```bash curl
curl -s "https://api.anlyon.com/api/v2/actions/effects?unresolved=true" \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY"

curl -s "https://api.anlyon.com/api/v2/actions/effects?grade=unknown" \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
```

From the CLI: `anlyon effects list --unresolved` and `anlyon effects list --grade unknown`.

## What the caller should do

1. **Do not retry.** A new invocation under a new key is a new operation. If the lost write landed, a retry is a duplicate.
2. **Keep the `effectId`.** It is in the invoke response.
3. **Read the effect.** Poll `GET /actions/effects/{id}`, or ask for a reconcile now. Wait for `outcome` to leave `unknown`.
4. **Repeat the same call if you must repeat something.** The same `Idempotency-Key` returns the existing invocation in its current state. The same `operationKey` with the same request returns the existing effect. Neither sends anything.
5. **Do not start dependent work.** Pass `dependsOn` and Anlyon refuses the dependent call with `DEPENDENCY_UNRESOLVED` until the outcome is settled.
6. **Escalate when `reconciliation.exhausted` is true.** An operator resolves it with evidence.

An invocation governed by an effect is resolved through its effect. `POST /actions/invocations/{id}/resolve` refuses it with `EFFECT_RESOLUTION_REQUIRED`.

## How it behaves

- **Governed means an adapter or a declaration.** A governed action has an effect, a read-back and a held allowance. The invocation of an action with no declaration can still be `unknown`, and you reconcile that one yourself.
- **A declared action recovers by the read-back or an operator.** Its write is sent once.
- **`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. This matters most after a lost response, when the read-back is the evidence.
- **A lookup is bounded.** Past its bounds the effect stays `unknown` until a person resolves it.
- **The Stripe replay window is 24 hours.** That is how long the adapter treats Stripe's idempotency key as usable.
- **A limit belongs to one environment.** Held exposure in one environment leaves the others unchanged.
- **A receipt is a database record with provider evidence.** A manual resolution is a person's statement, labelled as one.
- **Any workspace member can decide an approval.** Resolving an effect is a separate permission, `actions:resolve`.
