For agents

Agent email loop detection

To stop an AI agent from sending the same email in a loop, send through a key whose recent history is checked before every send. The guard refuses the repeat, records it for a human, and reports the key's loop state.

To stop an AI agent from sending the same email in a loop, send through a key whose recent sends are read and compared before every new one. The loop guard runs on every send from an API key, refuses the repeat with approval_required, records the held message so a human can decide, and reports the key's standing loop state on GET /limits/keys. You configure nothing to get it.

The two patterns it refuses

Repetition. The guard compares the candidate against this key's sends from the last 60 minutes. A prior send counts as near-identical when it shares the recipient and both subject and body similarity are at least 85%. The candidate is refused once it would be the 4th such message.

Recipient collapse. Content that changes slightly each time defeats similarity matching, so recipients are measured too. Once the window holds at least 20 sends, a window whose effective number of distinct recipients has fallen below 3 is refused as well.

Both signals run on every send. Either one alone is enough to hold the message.

What the agent gets back

{
  "error": {
    "code": "approval_required",
    "message": "Blocked: this agent has sent 3 near-identical emails to [email protected] within 60 minutes, which matches a retry or script loop; review the agent before sending more.",
    "fix": "Review this key's recent sends via GET /emails?api_key_id=:id to find the loop, then fix the agent before sending again.",
    "docs_url": "https://agentisend.com/docs/errors",
    "retryable": false
  }
}

retryable is false. Retrying is the behaviour that produced the loop, and it will be refused again. The refusal carries the evidence the guard used: the counts, the window, and the entropy reading.

Nothing is dropped

The held message is written to the approval queue, so a person can send it after looking at it: GET /agent-actions lists it with held_reason naming which signal fired, and POST /agent-actions/{id}/approve sends it. See /agent-email-approvals.

Watching a key before it trips

GET /limits/keys returns loop_state for every key, one of:

  • clear — the window shows no repetition and no recipient collapse.
  • watch — the pattern is forming but no send has been refused yet.
  • tripped — the guard is refusing sends from this key now.

loop_evidence on the same row carries the numbers behind the state. The middle state is the useful one: it is where an agent can be fixed before anything is held.

Testing without sending

POST /emails/preflight runs every send gate, the loop guard included, and reports what POST /emails would do without sending anything.

What a budget will not do here

A budget caps volume, so an agent looping under its ceiling stays under its ceiling. That is why the loop guard is a separate check rather than a smaller number. Both apply: /agent-email-budgets.