Darkmind / Issue 006

Retry AI Actions With One Operation ID

Give every consequential AI action a stable operation ID so a retry resumes the same intent instead of creating a second side effect.

Oct 13, 20264 min readReliable automation

An AI workflow can do the right thing and still create the wrong result. Imagine an assistant has prepared a customer refund, created a support ticket, or submitted a purchase request. It calls the API, then the connection times out before it receives a response. Did the action fail—or did it succeed and only the reply get lost?

If the system simply asks the model to try again, it may create a second refund, ticket, email, or order. The practical guardrail is an operation ID: generate one stable identifier for one intended real-world action, store it before making the write, and reuse it on every retry. The receiving service must recognize that ID and return the original outcome rather than performing the effect again.

This is idempotency in a form a working AI system can use. Temporal defines an idempotent activity as one where multiple executions do not change state beyond the first, and warns that activities may run more than once when retried.[1] Stripe’s API documentation describes the complementary interface: a client supplies an idempotency key so later retries of the same request can be recognized.[2] The point is not to make models less fallible. It is to make an uncertain network result safe to retry.

Give the action a durable identity

Create the operation ID at the boundary where a proposed action becomes eligible for execution—not inside a model response and not after the API call. Save a compact record such as:

The ID identifies the intent, not an individual HTTP attempt. A retry after a timeout must reuse the same ID. A genuinely new request—another email to the same person tomorrow, or a corrected charge for a different amount—needs a new ID.

Use a UUID or another server-generated identifier. Do not ask the model to invent it, and do not derive it only from mutable prose such as an email body. If the model revises the intended effect after review, that is a new intent and should get a new operation ID.

Copy this retry-safe action checklist

Put this beside any tool that can send, create, charge, publish, or update:

BEFORE THE WRITE
1. Validate the approved action against an allowlist and its exact target.
2. Create operation_id once; persist it with the normalized payload.
3. If operation_id already has status=succeeded, return the stored result.
4. If a prior request is still pending, look up its outcome before retrying.

ON EVERY PROVIDER CALL
- Send the same operation_id in the provider's idempotency field,
  request metadata, or your own deduplication table.
- Send the same normalized payload for that operation_id.
- Record the provider request/result ID and the response status.

AFTER AN ERROR OR TIMEOUT
- Do not create a new operation_id.
- Query the provider or local action log using the existing ID.
- Retry only if the outcome remains unknown and policy permits it.
- Escalate conflicting payloads or ambiguous results for review.

The check in step three prevents a common mistake: an agent loop may restart from an earlier state and call the same tool again even after the first attempt succeeded. A local record lets your executor return the first result, while a provider-supported idempotency key protects the last mile. Use both when you can. Your local record covers coordination among your workflow workers; the provider key covers duplicate delivery to its API.

Keep the payload fixed too

An operation ID is a promise: this key means this action. Do not reuse one with altered recipients, amount, target record, or content. Stripe documents that it compares later requests’ parameters with the original request and errors if they differ, which is a useful model for your own executor.[2]

Normalize the request before saving it. Sort an email recipient list if order is irrelevant; convert currency values to a canonical integer amount; pin the record ID rather than repeatedly looking it up by name. Then hash or store that normalized payload beside the operation ID. If a retry arrives with different material values, stop rather than deciding which version is “close enough.” Create a new proposal and new operation ID if the change is intentional.

This practice also improves debugging. Instead of reading an agent transcript to answer “did we send it?”, an operator can search one ID and see the approval, payload, attempts, provider result, and final state.

Failure mode: treating “exactly once” as a network guarantee

An operation ID does not make a distributed system magically execute code once. A worker can complete a write and crash before recording success; a provider may retain keys only for a limited window; and two workers may race on the same pending action. Temporal explicitly notes that a workflow may observe an activity as completed once even though the activity itself can execute or partly complete multiple times.[1]

Design for that uncertainty. Make the downstream effect idempotent, persist status transitions atomically where possible, use a provider lookup when the result is unknown, and put an expiry and reconciliation process around old pending operations. For consequential actions, the safe response to an ambiguous result is often “needs review,” not an eager second attempt.

The habit to keep

retries are normal in AI workflows. Give each real-world action one durable identity, then make every retry prove it is continuing that same action—not starting another one.

Sources

  1. Temporal, “Activity Definition”
  2. Stripe, “Idempotent requests”