Operate

Intents and retries

API Documentation

Every governed action your agent takes is an intent. When your agent moves money, creates a counterparty, asks a counterparty for its payment details, or requests a temporary limit increase (the console calls these temporary grants), we return an intent that records the action and its outcome. Reads, such as balances, transactions, and deposit addresses, do not return an intent.

An intent can pass through more than one status before it reaches a final one, so we recommend that your agent read the status every time instead of assuming the outcome.

Statuses and resolutions

Use status for control flow. Below are the five statuses that an intent may have, along with the matching resolution:

status resolution Meaning
completed completed The action has succeeded.
pending null The intent is waiting for a person to approve it.
processing null Catena has accepted the intent, and it is in progress.
blocked blocked, denied, or cancelled A policy has blocked it, a person has denied it, or it has been cancelled. This status is final.
failed failed or expired The intent did not complete. This status is final.

resolution is the final outcome, and it is null while the intent is open. A policy block and a denial both have the status blocked, so you can use resolution to tell them apart (a cancelled intent has resolution: "cancelled"). We may add new resolution values, so pass through any value you don't recognize.

Only completed means success. If an intent is pending or processing, read it again until it reaches a final status:

Shell
catena intents get int_...

Intent fields

In addition to status and resolution, an intent carries the following fields that explain its result:

Field Contents
reasons The policy decision or the approval route. Your agent should summarize it for the user.
expiresAt The approval deadline for a pending intent.
metadata.dataUrl A link to the approval or the result in the console. Give it to the person who approves.
data The result, once one exists: the transaction for a send or transfer, or the new counterparty for a creation. It is null until then.

To list your agent's recent intents, you can filter by status, type, and start time (e.g., --status pending):

Shell
catena intents list --status pending --status processing
catena intents list --type send --start 2026-10-01T00:00:00Z

Exit codes

catena send and catena transfer exit 0 as soon as they submit the intent, even when the intent is blocked, pending, or later fails. This means a successful exit does not tell you whether money moved, so read .status and .reasons every time.

Cancel an intent

An agent can cancel its own intent while the intent is waiting for approval, or after approval but before execution starts:

Shell
catena intents cancel int_...

The intent will end with resolution: "cancelled", and cancelling twice is safe. However, an intent that is already executing cannot be cancelled, and the API will return 409 (intent_not_cancellable).

Retry safely

By default, each request waits up to 80 seconds. If a request times out, that means the client stopped waiting, but the server may still have acted on the request.

Reads are safe to run again. Intents are different, because an intent needs an idempotency key to be retried safely. The CLI and the SDK generate a new key for every call unless you pass one, so sending identical inputs twice will not mark the second request as a retry.

Example: retrying an ACH payment

Let's say Cora's Coffee's agent is paying Pete's Packaging $125.00 by ACH for invoice 4471. To be able to retry, the agent chooses a key (here, 7c4f2a8e-invoice-4471) and saves it before the first attempt:

Shell
catena send --rail cpr_... --amount 125.00 --method ach \
  --idempotency-key 7c4f2a8e-invoice-4471

If the request times out, the agent runs the same command with the same key, and no money will move a second time. Below is how we handle a repeated key, and when to use a new one:

Case What happens
Same key, same inputs We return the original intent, with replayed: true. No money moves a second time.
Same key, different inputs We return 409.
The policy changed A retry keeps the original source account, even if the policy changed.
A block was cleared After an admin clears a block (by changing the policy or approving an override), send again with a new key. The blocked intent stays blocked and moved no money, so the new key cannot pay twice.
A new operation Use a new key for a new operation, or when an intent failed and another attempt is correct.
Important

Completed intents

Don't pay a completed intent again to recover from an error.

Payment errors and retries

The following messages can come back from a payment, and each one calls for a different response:

Message What to do
"Already prepared" (409 wallet_send_intent_not_executing) Run intents get <id> if you have the ID. Otherwise ask an admin. Do not retry with a new key.
"Created but never submitted; no money moved." Fix the problem in the message, then use a new key.
"Could not be confirmed." Read intents get <id> until it is final. Submit again only after blocked or failed.

Using the SDK

In the SDK, submitIntent returns the intent's current state (it does not wait for execution). You can poll with getIntent(id), cancel with cancelIntent(id), and pass idempotencyKey yourself when a retry must refer to the same operation.

TypeScript
const intent = await client.submitIntent({
  action: { type: "send", counterpartyRailId, amount: "125.00", method: "ach" },
  idempotencyKey,
})
if (intent.status === "pending") {
  // Waiting for a person. intent.reasons says why.
}
Updated October 29, 2026 Applies to @catena/cli 0.19.0 and @catena/sdk 0.11.0 View as Markdown