Execution and recovery
Find out whether an action happened before trying it again.
Your application asks a provider to make a payment. The request times out. Did the payment fail, or did the provider complete it and lose the response? Trying again immediately could pay twice.
Use this guide when an application crashes, a provider response is lost or an Action has an unknown outcome. The first task is to establish what happened to the original request.
AuthRail keeps the permission decision and the application's execution reports separately. An allowed decision tells you the request had permission. The application must check the provider or its own saved business records to establish the effect.
Investigate the original request
The developer or operator responsible for the protected application should:
- Stop automatic retries for this request. Keep its saved request, Action and execution-attempt identifiers. Keep the original arguments and provider request key too.
- Open its Action in AuthRail. Read the request decision and execution
reports. An attempt marked
startedmay have reached the provider. - Look up the original operation. Use the trusted provider account or the application's saved effect record. Match the request key, resource and relevant amount or other business details.
- Report the established outcome through the SDK. Report success when the source confirms the effect. Report failure only when the source confirms that no effect occurred.
- Leave it unknown if the source cannot establish either answer. Keep the request for an operator to investigate.
This is called reconciliation: checking the original effect and updating its reported outcome. A timeout, HTTP error or the agent's recollection is not enough to establish that nothing happened.
Expected result: the same Action has an established outcome or remains explicitly unknown. The investigation itself does not repeat the payment.
Save the attempt and report what you establish
The code below continues the Enforce setup in the SDK guide. client,
operation and payment come from your protected application. The operation
keeps the original reviewed version, authenticated identity and stable business
Action key. Run reconciliation in a worker that exclusively claims that task.
recoveryStore / recovery_store is an application-owned helper: encrypt and
persist the recovery context, binding it to the original organisation, user,
deployment and task. Its load function must check the same binding. Save the
context before calling the provider. Keep the original payment arguments
and business request with it; do not reconstruct them from agent messages.
provider.pay / Pay performs your actual protected operation using the SDK's
idempotency key. lookupOriginal / LookupOriginal / lookup_original must
query that same trusted provider account and original request. It returns a
validated state (succeeded, failed or unknown) and a bounded provider
reference. Here failed means the lookup confirmed no effect, not a timeout.
These provider and storage methods are your code, not AuthRail SDK APIs.
import { EnforceOutcomeUnknownError, type ExecutionContext } from '@authrail/sdk-node';
async function executePayment() {
try {
return await client.enforce(operation, payment, async (checked, execution) => {
await recoveryStore.saveEncrypted(binding, JSON.stringify(execution));
return provider.pay(checked, execution.providerIdempotencyKey);
});
} catch (error) {
if (error instanceof EnforceOutcomeUnknownError && error.recovery) {
try {
await recoveryStore.saveEncrypted(binding, JSON.stringify(error.recovery));
} catch {
console.error('Recovery storage unavailable; keep this task unresolved');
}
}
throw error; // Keep the task unresolved; do not pay again here.
}
}
async function reconcilePayment() {
const recovery: ExecutionContext = JSON.parse(await recoveryStore.loadEncrypted(binding));
const found = await provider.lookupOriginal(recovery.providerIdempotencyKey);
if (found.state === 'unknown') return; // Leave the saved task for investigation.
await client.reconcile({ ...recovery,
providerReference: found.reference, outcome: found.state });
}A recovery-storage failure must also keep the task held for investigation, not mark it failed or authorize another attempt.
A returned reconciliation receipt updates the original attempt; it does not repeat the payment. Update your task after checking that response. An unknown lookup stays unresolved, even though the example worker returns without error. A result-blocking error can also mean an effect exists: inspect and reconcile that original effect instead of treating every SDK error as a failed payment.
Retry only after confirming no effect
A confirmed no-effect reconciliation can allow another execution attempt. The application still needs a fresh decision, any required approval, and fresh business facts used by the rule.
Keep the same business arguments and provider request key. Keep the same AuthRail Action key—the value your application assigned to this business request. Changing that key creates a different request; it does not resolve the uncertainty around the first one. Do not reuse an Action key with different arguments.
A provider idempotency key is a request key it can use to recognize a repeat of the same operation. Pass the SDK's provider key to services that support it, and keep it in your application's saved records. Confirm the provider's own idempotency behavior before relying on it to prevent duplicate effects.
More about saving recovery information
The protected application keeps provider credentials and performs the business
function. Before that function runs in Enforce, the adapter sends a signed
started receipt to AuthRail. Your application must also save the recovery
context with its business request so it survives a process restart.
Keep the Action ID, attempt ID, original input, provider idempotency key and
any approval permit reference. The Enforce callback receives a provider key
of the form ar-<Action ID>. Use the recovery context supplied by your adapter,
rather than reconstructing a different attempt after a crash.
Node's reconcile, Go's Reconcile and Python's reconcile report an outcome established by the
protected application's lookup. They accept recovery context, a provider
reference and a known outcome. The resulting receipt is
reconciled_succeeded or reconciled_failed. Neither method automatically
repeats the business function.
A signed receipt establishes what the registered executor key reported. It does not independently retrieve or verify the provider record.
Check whether the result was withheld
An operation can finish even if AuthRail stops its result from being returned to the agent. For example, the provider may create an invoice, then the result check may prevent the agent from seeing sensitive invoice fields.
Inspect the execution reports and provider record before retrying. A withheld response does not delete the invoice. If the business operation needs to be reversed, use your application's supported reversal process.
Restore report delivery without repeating the action
The sidecar may have a report waiting to reach AuthRail. Read its recording status before deciding what to do:
| Status | Meaning | What to do |
|---|---|---|
retained | AuthRail saved the report. | Keep its record references with the application task. |
queued | The sidecar saved the report locally for delivery. | Restore connectivity and keep the complete state directory. |
unavailable | Durable recording could not be established. | Investigate storage and delivery; preserve the application's recovery information. |
Enforce holds a new operation when its required call report has not been retained. Later report delivery does not start that operation automatically. A reporting problem after execution cannot undo an effect that already happened. Observe remains advisory during a reporting outage.
Keep the entire sidecar state directory through restarts, including queues, saved session history and recording-gap files. Deleting those files can remove the information needed to recover. See monitoring to inspect connection status and local logs.