Skip to content

Questions · Published 2026-10-27 · Updated 2026-10-27 · AgentiSend

Stop an AI agent sending emails in a loop

AgentiSend stops an AI agent that sends emails in a loop by holding near-identical repeated sends for a person to release or reject.

AgentiSend stops an AI agent that sends emails in a loop by holding near-identical repeated sends for a person to release or reject. If the loop is already running, pause the key first: the hold queues one message, and the pause stops the next ones.

1. Pause the key that is sending

POST /limits/keys/{id}/kill stops one key on the next request. POST /limits/kill-all stops every key on the account. A paused key's send is refused with kill_switch_active. The catalogue message is "This API key has been paused with the kill switch." The catalogue fix is "Read GET /trust/standing for why it was paused. Only a person signed in to the console can resume it; the paused key cannot resume itself."

Turning it back on is POST /limits/keys/{id}/resume for one key and POST /limits/resume-all for the account. Both summaries say a person signed in to the console does this, and that budgets and ceilings stay as they were.

curl -sS -X POST https://api.agentisend.com/limits/keys/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42/kill \
  -H "Authorization: Bearer $AGENTISEND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
200
{
  "api_key_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
  "budget_per_period": -9007199254740991,
  "consumed_in_period": -9007199254740991,
  "consumed_in_window": -9007199254740991,
  "paused": false,
  "paused_at": "2026-09-04T09:14:00.000Z",
  "paused_reason": "example",
  "period": "hourly",
  "period_started_at": "2026-09-04T09:14:00.000Z",
  "rate_ceiling_per_minute": -9007199254740991,
  "rate_window_started_at": "2026-09-04T09:14:00.000Z",
  "updated_at": "2026-09-04T09:14:00.000Z"
}
Response

2. Read what was held

The loop guard does not delete the message. It writes a row you can list:

curl -sS -X GET https://api.agentisend.com/agent-actions \
  -H "Authorization: Bearer $AGENTISEND_API_KEY"
200
{
  "data": [],
  "has_more": false,
  "next_cursor": "example",
  "object": "list"
}
Response

GET /agent-actions summary: "Every held agent action, newest first — nothing waits invisibly. Filter by state to read the inbox or the audit trail." Filter with state=pending. The refusal the sender receives is approval_required. On a loop, the message names the count and the window; the catalogue sentence underneath is "This action requires human approval before it executes."

{
  "error": {
    "code": "approval_required",
    "message": "Blocked: this agent has sent 3 near-identical emails to customer@example.com within 60 minutes, which matches a retry or script loop; review the agent before sending more.",
    "fix": "Do not send it again: a person approves or rejects it in the console under Agents → Approvals, and approving sends it — the message then appears in GET /emails. action_id in this error names the held send; the key that asked cannot approve itself, and a retry waits on the same approval.",
    "docs_url": "https://agentisend.com/what-does-approval-required-mean#approval_required",
    "retryable": false
  }
}

retryable is false. Sending the same body again waits on the same row. action_id in the error is the row.

3. Release or reject each hold

POST /agent-actions/{id}/approve runs the held send through the normal accept path. Its summary: "Execute the held send through the normal accept path. A person signed in to the console decides; the action row records who and when." POST /agent-actions/{id}/reject refuses it and keeps the reason. The key that asked cannot approve itself.

What the guard treats as a loop

Comparison uses this key's own recent sends, a window of 60 minutes. A previous send counts when the recipient matches and both subject and body reach 85%. The candidate is held when accepting it would make 4 such messages. On a key whose loop guard is full, a second signal watches recipient collapse: once the window holds at least 20 sends and the effective recipient count falls below 3, the next message to one of those busy inboxes is held. A message to a different address is not held for that signal. A new key watches repetition and not collapse. How that comparison is defined, and how to turn the second signal on, is Agent email loop detection.

What will not stop it

An Idempotency-Key is stored for 7 days. Repeating one request with the same key and the same body replays the first result, which is how a crashed retry avoids a second copy. A loop that edits the subject, picks a new key, or walks a list is a different request, so the header does not catch it. The page for that header is How do I stop my code from sending the same email twice?.

POST /emails/batch judges each item on its own. One item can be held while the items around it are accepted. Read the status on each row of data and repair only the rejected ones. A batch is not a way past the pause: a paused key refuses every item.

A budget caps how many messages the key may send. An agent that loops underneath that cap stays under it. Setting the cap is How do I stop an AI agent from sending too many emails? and the spending-limit page is Email API with spending limits for AI agents.

FAQ

How do I stop the loop before the window ends?

Pause the key with POST /limits/keys/{id}/kill, or pause every key with POST /limits/kill-all. The next send from a paused key is refused with kill_switch_active.

Does the hold delete the email?

No. The message waits on GET /agent-actions until a person approves it, which sends it, or rejects it, which does not.