Farther ShoreDocs
Go to Farther Shore
Use agents across the platform
Event-driven automation
Farther Shore Agent
Your coding agent
Ownership boundaries
Retries and idempotency
The retry contractsExecute a same-key operationWhat “the same attempt” meansPreviews never consume a live attemptOne-time resultsExternally delivered effectsDesired-state examples
Enable the Farther Shore Agent
Automation request schemas
Status
Docs/Work with agents/Retries and idempotency

Retries and idempotency

Repeat agent operations without duplicating effects or mistaking a replayed response for current state.

Idempotency is an attempt identity, not a freshness guarantee. A replay can prove what one earlier request returned. It cannot prove that the resource is still in that state.

Start with the installed CLI's machine-readable catalog:

bash
farthershore operations list --format json

Read each operation's retry.kind, retry.keyRequired, retry.enforcement, retry.responseSemantics, retry.reconcile, and retry.rationale before automating it. The catalog also includes commands that are registered directly instead of projected into MCP under directCliOperations; this includes remote farthershore validate --business and subscriber service-account operations.

The retry contracts

retry.kindUse an idempotency key?Safe agent behavior
read_currentNoRetry the read. Each successful response is a new observation.
convergent_writeNoAfter an ambiguous result, run retry.reconcile first. Repeat only if the intent is still current, then reread. This avoids overwriting a newer change.
same_key_replayYes, for the live requestPersist one key before the first dispatch. Reuse it only for that exact uncertain attempt, then reconcile.
intrinsic_replayNoThe stable target already names the one transition. Repeat the exact request and reconcile; Core rejects a changed payload.
no_automatic_retryNoAn ambiguous result may have caused an external effect. Stop and use retry.reconcile before deciding whether a new action is required.

retry.enforcement states what actually supplies safety: a fresh read, resource identity, Core's idempotency reservation, transactional secret replay, or a server-derived transition identity. retry.responseSemantics makes the freshness boundary explicit. In particular, original_attempt_result is historical proof about one attempt, not current resource state.

intrinsic_replay is intentionally rare. Managed service-account approval is one example: the approval ID and authenticated approver identify the transition, so adding a random caller key would not make it safer.

For agent mutations, Core accepts Idempotency-Key only when the concrete retry contract is same_key_replay. Sending one on a convergent write, preview-only route, or no_automatic_retry operation returns 400 IDEMPOTENCY_KEY_NOT_SUPPORTED. Current-state reads expose no key in the CLI/MCP contract and always execute as fresh reads; an incidental HTTP header cannot turn a read into a cached response. These boundaries prevent a caller from accidentally treating a desired-state write or observation as historical replay state.

For a CLI-session or MakerToken principal, Core also rejects a live same_key_replay request that omits the header with 400 IDEMPOTENCY_KEY_REQUIRED, before the operation handler runs. This server-side requirement protects agents that call the HTTP API directly rather than through the CLI or MCP. Shared human browser routes may remain unkeyed; the browser flow is not an automatic agent retry contract.

Execute a same-key operation

  1. Resolve the organization, business, environment, and exact payload.
  2. Run --dry-run without an idempotency key when preview is supported.
  3. Generate and record a new opaque key in durable agent task state before the first live dispatch.
  4. Send the live request with --idempotency-key <persisted-key>.
  5. If the result is ambiguous, retry the byte-equivalent intent with the same key. Do not generate another key.
  6. Run the catalog's retry.reconcile read before reporting current state.

When Core served the completed attempt from its replay store, the success envelope contains:

json
{
  "meta": {
    "idempotency": {
      "replayed": true
    }
  }
}

This metadata is outside data, so it cannot be mistaken for a resource field. Its absence means Core did not mark that response as a replay; it still does not make a write response a fresh read.

Example:

bash
# Record this value in private task state before the live call.
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')

farthershore business create quillby \
  --idempotency-key "$CREATE_ATTEMPT" \
  --format json

farthershore business show quillby --format json

Do not store attempt keys in the business repository. They are not credentials, but they are operational correlation material and do not belong in product source.

What “the same attempt” means

A key is scoped to the authenticated organization and principal. Within that scope it names one semantic request across every endpoint. The following must remain identical on a retry:

  • HTTP method and concrete path, including the target resource;
  • environment selection;
  • query values other than the preview control;
  • canonical request body, including array order;
  • any route-specific semantic target.

Reusing the key for a different intent returns 422 IDEMPOTENCY_KEY_REUSED; the second mutation does not run. A request still executing returns 409 IDEMPOTENCY_KEY_IN_FLIGHT; back off and retry the same key. If an attempt has remained in progress long enough that Core can no longer prove whether its handler committed, it returns 409 IDEMPOTENCY_RESULT_INDETERMINATE with retryable: false. Core retains the attempt and does not execute it again. Run the reconciliation read; use a new key only after current state proves that a new mutation is still required. Keys are retained for 24 hours.

A 5xx emitted after a keyed handler starts is also indeterminate: a server error cannot prove that no database or provider effect committed. Core retains the attempt instead of freeing the key for an automatic rerun. A 3xx/4xx response is stored as the terminal historical result of that attempt too, because a handler may have partially applied before rejecting. Reconcile first; if corrected input or a fresh action is required, give that new intent a new key.

Previews never consume a live attempt

Never send an idempotency key with --dry-run. The CLI and Core reject that combination. Preview again from current state, then create a separate key for the live operation. This prevents a preview response from being replayed as if the write happened.

Only operations whose catalog entry has retry.preview: true expose preview. Core returns 400 DRY_RUN_NOT_SUPPORTED for any other mutating route instead of silently running a live mutation or returning a false preview.

Some APIs expose a dedicated POST preview route and reuse the live operation's authorization permission. The route-level OpenAPI x-farthershore-agent-retry contract is authoritative: dedicated agreement preview routes are fresh observations and reject Idempotency-Key, while the separate confirmed agreement create or amend route requires one.

One-time results

Token and credential mints use same_key_replay so an ambiguous first response can recover the exact secret without minting another. The encrypted secret recovery window is 15 minutes. After it expires, the completed attempt remains tombstoned and returns IDEMPOTENCY_RESULT_EXPIRED; Core does not rerun the mutation. Use the reconcile read to locate the created credential, then rotate under a new persisted key if a new secret is actually required.

auth context-token is deliberately different. It mints a short-lived, point-in-time authorization credential, so replay could return a token whose expiry or embedded authorization snapshot is no longer useful. Its contract is no_automatic_retry: after an ambiguous response, request a new token rather than attaching an idempotency key or treating an older token as current state.

Externally delivered effects

webhook test and webhook trigger send a real signed HTTP request to the configured receiver. Persist a key before the first send so an uncertain retry cannot deliver the same test twice:

bash
WEBHOOK_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
farthershore webhook trigger quillby <webhookId> \
  --type payment.failed \
  --idempotency-key "$WEBHOOK_ATTEMPT" \
  --format json
farthershore webhook deliveries quillby <webhookId> --format json

webhook listen remains no_automatic_retry because it is a process-owning, multi-step session (tunnel, temporary endpoint, optional send, tail, cleanup), not one server mutation that response replay can make atomic.

Desired-state examples

Backend binding is convergent and environment-specific, so it deliberately has no replay key. After an ambiguous bind, inspect the current binding before deciding whether to repeat it; do not blindly restore an older URL over a newer human or agent change:

bash
farthershore backend list quillby --format json
# Select the row whose environmentId matches the resolved preview environment.
# If it is not already correct and the original intent is still current:
farthershore backend bind quillby api \
  --env preview \
  --origin-url https://preview-api.example.com \
  --format json
farthershore backend list quillby --format json

Usage-limit create is naturally identified by subscription, subject, and quantity. An exact repeated create returns the current matching row; if that row changed after the first request, the retry returns conflict instead of stale success. Updates target a stable limit ID, but an absolute count can still be newer than the ambiguous request. Both paths therefore run limit list before any repeat and again after a write.

Never add a key merely because a request uses POST. Use the advertised retry contract and the observed operation semantics.

PreviousOwnership boundariesNextEnable the Farther Shore Agent

On this page

The retry contractsExecute a same-key operationWhat “the same attempt” meansPreviews never consume a live attemptOne-time resultsExternally delivered effectsDesired-state examples