# Idempotency

> Source: https://openwop.dev/spec/v2/core/idempotency.html · OpenWOP, the open-source protocol for multi-agent workflow orchestration. CC BY 4.0.

> **Status: Stable.**
> **Normative home:** `idempotency`.

## Why this exists

A retried request MUST NOT create a second run, and a retried node MUST NOT issue a second external effect. A host MUST implement Layer 1 for every mutating endpoint; a host advertising `idempotency` MUST implement Layer 2 for every node executor that performs an external side effect ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)).

## Layer 1: `Idempotency-Key`

The header keeps its standard name and applies to every mutating operation in `api/v2/openapi.yaml`; `GET` operations MUST NOT honor it.

### Key and record

- **Grammar.** The value MUST match `^[A-Za-z0-9._~-]{22,128}$` and MUST carry at least 128 bits of entropy (a UUIDv4 in canonical or base64url form satisfies it). A host MUST reject a value outside the grammar with `400 idempotency_key_invalid`.
- **Record key.** A record MUST be keyed by `(authenticatedTenantId, canonicalEndpointId, callerIdempotencyKey)`; the tenant MUST come from the credential, never the body.
- **Retention.** A record MUST be retained for at least 24 hours.
- **Keyspace.** Host-minted identifiers MUST NOT share the caller idempotency store. Logs and spans MUST NOT expose keys.

### Outcomes

- **Final.** A host MUST cache `2xx` and non-retryable `4xx` responses (status, headers, body) and MUST return the cached response to a same-key duplicate.
- **Retryable.** `429` and `5xx` MUST NOT be replayed from cache; a same-key retry MUST re-execute, and a later final outcome replaces the record.
- **Not cached.** `400 idempotency_key_invalid`, `400 validation_error`, `401` and `403` MUST NOT be cached.
- **Digest mismatch.** A different request digest under the same record key MUST fail with `409 idempotency_key_mismatch` and MUST NOT return the cached body. This is the only mismatch code.
- **Replay marker.** A response served from cache MUST carry `OpenWOP-Idempotent-Replay: true`.

### Concurrency

Of two concurrent same-key requests a host MUST process exactly one to completion and MUST NOT process both.

- The other MAY wait, bounded by the host's request timeout, and receive the winner's response only if it is a final outcome (one this record caches), marked `OpenWOP-Idempotent-Replay: true`.
- Otherwise the host MUST answer `409 idempotency_in_flight` with no retry timing in `details`, and SHOULD set `Retry-After`.

The code's registry `retriable: false` means not retryable without waiting.

## Layer 2: effect identity

Layer 2's unit is the **effect**, identified once and stable across every transport or provider retry.

- **Keying.** An effect MUST be keyed on its business identity (`keying: business-identity`): derived from the business operation, stable across every entry point, containing no `runId`, `nodeId` or ordinal. The activity recipe (`keying: activity-recipe`: tenant, run, node, ordinal, `providerKey`) is the fallback for a provider with no business key.
- **Attempts.** The retry counter MUST NOT participate in the identity. Two distinct logical invocations MUST receive different identities.
- **Claim.** The persist that guards the effect MUST be an atomic claim (compare-and-set or insert-if-absent) that at most one executor can win, so at most one concurrent duplicate performs the effect.
- **Provider key.** When the provider accepts an idempotency key, the host MUST inject the effect identity (or a documented deterministic derivative), stable across retries. A host that cannot use the provider's convention MUST still persist the outcome.
- **Streaming.** A streamed body MUST NOT be cached in the ledger; the host SHOULD record the request and its final outcome.
- **Retention.** An effect record MUST be retained for at least 14 days.

### Witness: `GET /runs/{runId}/effects`

A host that advertises `idempotency` MUST serve `schemas/v2/effect-ledger-projection.schema.json` at `GET /runs/{runId}/effects` (`getRunEffects`): `{ runId, effects[] }`. Each record carries:

- `effectId` (tenant-bound, `schemas/v2/ids.schema.json`), `nodeId`, `attempt`, `keying`, `at`;
- `state`: `claimed` | `completed` | `released` | `escaped`;
- optional `invocationId` and a redaction-safe `providerKey`.

The projection MUST be content-free of provider payloads and credential material.

## Composition

Layer 1 deduplicates the caller's request; Layer 2 deduplicates the run's effects. A retried provider call inside a run MUST resolve to the same effect record. Effects under replay and fork are in [replay.md](https://openwop.dev/spec/v2/core/replay.html); identifier grammars are in [identity.md](https://openwop.dev/spec/v2/core/identity.html).

## Multi-region

The two region facets of `idempotency`, `multiRegion` and `crossRegion`, claim different things.

- `crossRegion` names the host's deployment posture for this axis and MUST be held constant for the life of an advertisement.
- `multiRegion` is the behavioural claim. When a host advertises it, both layers MUST hold across regions: an `Idempotency-Key` replayed into a second region MUST resolve to the first region's response rather than starting new work, and effect identity MUST collapse a duplicate effect wherever it is observed.
- A host that does not advertise `multiRegion` makes no cross-region promise, and a client MUST NOT infer one from `crossRegion` alone.

*Sources: RFCs 0170, 0171, 0173, 0213.*
