Source: https://cli.nylas.com/guides/ai-agent-email-bounce-retry

# AI Agent Email Bounce Detection and Retry

Your agent sent 500 emails overnight and 23 bounced. Whether it retries them is not a judgment call to hand a language model: classify each bounce by DSN class digit, back off the soft ones, suppress the hard ones, and escalate the leftovers to a human.

Written by [Pouya Sanooei](https://cli.nylas.com/authors/pouya-sanooei) Software Engineer

Updated June 9, 2026

> **TL;DR:** Subscribe to bounces with `nylas webhook create --triggers message.bounce_detected,message.send_failed`, then classify by the DSN class digit: a `4.x.x` soft bounce gets up to 3 backoff retries, a `5.x.x` hard bounce goes on a suppression list forever. These rules live outside the agent's decision loop, and the backoff schedule needs no cron daemon — one send flag handles it, covered below.

Command references used in this guide: `nylas webhook create`, `nylas webhook server`, and `nylas email send`.

## Why should bounce retry logic live outside the AI agent's decision loop?

AI agent email bounce retry must be deterministic code the agent calls, not a question the model answers. A sampled decision gives different verdicts on identical bounces, and one wrong verdict re-sends to a dead address. Classification, backoff, and suppression are table lookups: the agent can't prompt its way past a rule it doesn't control.

The stakes are reputation, not convenience. Since February 2024, Google's [email sender guidelines](https://support.google.com/a/answer/81126) require bulk senders to keep spam rates below 0.3% (with under 0.1% recommended), and mailbox providers read repeated sends to hard-bounced addresses as the same list-hygiene neglect. An agent that decides retries in its prompt will eventually hammer a `550` address 10 times in an hour. A rule table never will, because the same input always produces the same verdict. Wrap the rules around the send tool, give the model only the wrapper, and the worst a confused agent can do is request a send the wrapper refuses.

## How does an AI agent detect email bounces with webhooks?

The `message.bounce_detected` and `message.send_failed` webhook triggers cover both failure paths: an asynchronous bounce after the message was accepted, and a send that failed at submission. Subscribing to them pushes failures to your handler instead of making the agent poll daemon mail, and the Message category exposes 8 triggers in total.

The `nylas webhook create` command registers the subscription with a URL and a trigger list; it requires an API key. During development, `nylas webhook server --no-tunnel` runs a loopback-only HTTP listener so you can inspect event payloads from local tooling before deploying a real endpoint. Run `nylas webhook triggers` to list every available trigger by category.

```bash
# Subscribe the bounce handler to both failure paths
nylas webhook create \
  --url https://agent.example.com/webhooks/bounces \
  --triggers message.bounce_detected,message.send_failed \
  --description "Agent bounce feed"

# Inspect payloads locally before deploying (loopback only, no prompt)
nylas webhook server --no-tunnel
```

Treat each delivery as untrusted input and verify its HMAC signature before acting; `nylas webhook verify --payload-file --signature --secret` checks one offline. The handler's only job is to extract the recipient and status code, then hand both to the classifier below. No model call happens anywhere in this path.

## How do you classify a soft vs hard bounce from SMTP codes?

The first digit of the enhanced status code decides everything. [RFC 3463](https://datatracker.ietf.org/doc/html/rfc3463), published in January 2003, defines `4.XXX.XXX` as “Persistent Transient Failure” (a soft bounce worth retrying) and `5.XXX.XXX` as “Permanent Failure” (a hard bounce that never recovers).

That single digit has been stable across providers for over 20 years, unlike the human-readable bounce prose, so a shell `case` statement is the whole classifier. A `452 4.2.2` mailbox-full reply earns a retry; Gmail's no-such-user reply is documented as `550 5.1.1` and earns permanent suppression. Watch one edge: `5.2.2` marks a mailbox permanently over quota, hard despite sounding temporary — the [soft vs hard bounce guide](https://cli.nylas.com/guides/soft-bounce-vs-hard-bounce) covers it in depth.

```bash
#!/usr/bin/env bash
# classify <recipient> <enhanced-status-code>  e.g. classify bob@x.com 5.1.1
classify() {
  case "$2" in
    4.*) echo "soft"  ;;  # transient: schedule a backoff retry
    5.*) echo "hard"  ;;  # permanent: suppress forever
    *)   echo "human" ;;  # unparseable: escalate, never guess
  esac
}
```

The fallthrough branch matters as much as the first two. A bounce the classifier can't parse goes to a human, not back to the agent — guessing on 1 ambiguous code out of 100 is how suppression lists rot.

## How do you retry soft bounces with exponential backoff?

Retry a soft bounce at most 3 times on a widening schedule: 30 minutes, 2 hours, then 8 hours. If the third attempt bounces, suppress the address. The 30-minute floor comes from [RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321) section 4.5.4.1, which says the retry interval “SHOULD be at least 30 minutes.”

Here is the payoff teased in the TL;DR: the `--schedule` flag on `nylas email send` accepts durations like `30m` or `8h`, so each retry is queued at send time and no cron daemon or sleep loop exists anywhere. Tagging the attempt number with `--metadata` makes the count durable on the message itself, so the next bounce webhook knows whether the cap of 3 is reached.

```bash
# retry <recipient> <attempt-number> — called on each 4.x.x bounce
retry() {
  case "$2" in
    1) DELAY=30m ;;
    2) DELAY=2h  ;;
    3) DELAY=8h  ;;
    *) echo "$1" >> suppressed.txt; return ;;  # cap reached: suppress
  esac
  nylas email send \
    --to "$1" \
    --subject "$SUBJECT" \
    --body "$BODY" \
    --schedule "$DELAY" \
    --metadata attempt="$2" \
    --yes
}
```

The widening gaps are deliberate: a mailbox full at 09:00 is often empty by lunch, and 3 attempts spread across 10.5 hours give it that chance without pestering the receiving server. The same bounded-attempt discipline applies to plain network failures, covered in the [reliable email automation guide](https://cli.nylas.com/guides/build-reliable-email-automation).

## When should an email bounce escalate from the agent to a human?

Escalate when the rules run out: a soft bounce that survives all 3 retries, a status code the classifier can't parse, or a batch where more than 5% of recipients bounce, which usually means a stale list rather than 25 individual dead mailboxes. Everything else resolves silently through retry or suppression.

Suppression is the gate that makes the whole system safe: before any send, the wrapper checks the recipient against `suppressed.txt` and refuses matches. One `5.1.1` is enough to add an address — Microsoft's [Exchange Online NDR documentation](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/non-delivery-reports-in-exchange-online/non-delivery-reports-in-exchange-online) describes these codes as final verdicts, not requests to try harder. The escalation itself is one more send, this time to a person.

```bash
# Gate every agent send through the suppression list
grep -qxF "$RECIPIENT" suppressed.txt && { echo "suppressed, skipping"; exit 0; }

# Escalate a rule-exhausted bounce to a human
nylas email send \
  --to ops@example.com \
  --subject "Bounce escalation: $RECIPIENT" \
  --body "3 retries exhausted (last code: $STATUS). Verify the address manually." \
  --yes
```

Notice what the agent never sees: the suppression file, the attempt counter, the schedule table. It calls a send wrapper and gets back “sent,” “queued for retry,” or “refused.” That interface is the entire containment story, and it costs about 30 lines of shell.

## Next steps

- [Handle email bounces from the CLI](https://cli.nylas.com/guides/email-bounce-handling-cli) — the webhook-to-suppression workflow for non-agent senders
- [Email bounce codes reference](https://cli.nylas.com/guides/email-bounce-codes-reference) — decode 5.1.1, 5.2.2, 4.2.2, and the other RFC 3463 codes
- [Soft bounce vs hard bounce](https://cli.nylas.com/guides/soft-bounce-vs-hard-bounce) — retry semantics, greylisting, and the over-quota edge case
- [Build reliable email automation](https://cli.nylas.com/guides/build-reliable-email-automation) — exit codes, bounded retries, and idempotent sends
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted vs hosted infrastructure for mailbox automation
- [Twilio vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — outbound-only sending vs bidirectional mailbox access
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
