OpenWOP openwop.dev

Status: Stable · v2.1.0 (2026-09-11) · RFC 0170 §D.3, RFC 0171 §B.2, RFC 0173 §B.

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 and Layer 2 for every node executor that performs an external side effect.

Layer 1: Idempotency-Key

The header keeps its standard name (RFC 0171 §C.1) and applies to every mutating operation in api/v2/openapi.yaml; GET operations MUST NOT honor it.

RuleRequirement
GrammarThe 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 keyA record MUST be keyed by (authenticatedTenantId, canonicalEndpointId, callerIdempotencyKey); the tenant MUST come from the credential, never the body.
Final outcomesA host MUST cache 2xx and non-retryable 4xx responses (status, headers, body) and MUST return the cached response to a same-key duplicate.
Retryable outcomes429 and 5xx MUST NOT be replayed from cache; a same-key retry MUST re-execute, and a later final outcome replaces the record.
Not cached400 idempotency_key_invalid, 400 validation_error, 401 and 403 MUST NOT be cached.
Digest mismatchA 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.
ConcurrencyOf two concurrent same-key requests a host MUST process exactly one to completion and MUST NOT process both. Retry timing travels in Retry-After only.
Replay markerA response served from cache MUST carry OpenWOP-Idempotent-Replay: true.
RetentionA record MUST be retained for at least 24 hours.
KeyspaceHost-minted identifiers MUST NOT share the caller idempotency store. Logs and spans MUST NOT expose keys.

Layer 2: effect identity

Layer 2 is bound by advertising idempotency (security-defaults.md). Its unit is the effect, identified once and stable across every transport or provider retry.

RuleRequirement
KeyingAn 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.
AttemptsThe retry counter MUST NOT participate in the identity. Two distinct logical invocations MUST receive different identities.
ClaimThe 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 keyWhen 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.
StreamingA streamed body MUST NOT be cached in the ledger; the host SHOULD record the request and its final outcome.
RetentionAn 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 carrying effectId (tenant-bound, schemas/v2/ids.schema.json), nodeId, attempt, keying, state (claimed | completed | released | escaped), at, 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; identifier grammars are in identity.md.