Inboxes · September 29, 2026

Why Your Agent's Email Lands in Spam

Why does my AI agent email go to spam? The three agent-specific causes, what the platform reports back, and the part no sender can see.

A customer tells you the agent’s follow-up “went to spam”. You check, and every signal you have is green: the send returned 202, the message reached accepted, and the recipient shows delivered. Search “why does my AI agent email go to spam” and the pages that rank for the question will tell you to publish SPF, DKIM and DMARC, warm up the domain, and keep your complaint rate under 0.3%. That is advice for a marketer who owns a domain and sends one campaign to a list. It barely touches an agent.

This article settles three things: what you control and what you do not when an agent sends from a platform address, the three ways agent traffic differs from the traffic spam filters were tuned on, and exactly what this product reports back (which, for spam-folder placement, is nothing, for reasons no provider can fix). If the message bounced rather than arriving in spam, that is a different problem, covered in agent email bounced 550 5.7.1.

Why does my AI agent email go to spam? The short answer

Spam filters decide from two kinds of evidence: who is sending, and how this sender’s mail has been received before. Authentication settles the first. The second is reputation, and reputation is a pattern learned over time: similar messages, at a steady rate, to people who mostly want them. Agent mail breaks every part of that pattern at once.

  1. The content is different every time. A model composes each message fresh, so there is no stable template for a filter to learn is harmless.
  2. Volume arrives in bursts. An agent sends when a trigger fires, not on a warmup curve.
  3. The reply ratio is inverted. A person’s outbox is mostly replies to people who wrote first. An agent’s is mostly first contact.

None of these is a misconfiguration you can fix with a DNS record. They are properties of the workload. And one fact frames everything else: no sender, on any provider, can guarantee inbox placement. Google says so about providers directly, in the passage where it declines allowlist requests: “We can’t guarantee messages sent by email providers will pass Gmail’s spam filters” (Email sender guidelines, read 29 September 2026).

What you control on a platform address, and what you do not

Live mail on this product is sent from agents.emailforagents.ai, a domain the platform operates. You do not publish its SPF, DKIM or DMARC records and cannot change them. You can read them, which is worth doing once so you stop suspecting them. As of 29 September 2026:

dig +short TXT _dmarc.agents.emailforagents.ai
# "v=DMARC1; p=none; rua=mailto:support@agents.emailforagents.ai; adkim=r; aspf=r; ri=86400"
dig +short TXT send.agents.emailforagents.ai
# "v=spf1 include:amazonses.com ~all"
dig +short TXT resend._domainkey.agents.emailforagents.ai
# "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDj..."

That is a DKIM key on the sending domain, an SPF record on the bounce subdomain, and a DMARC record in monitoring mode with relaxed alignment. That p=none suits a shared platform domain; why it is the wrong starting point for a fresh agent subdomain of your own is covered in DMARC for autonomous senders. Published records are configuration evidence. They are not a statement about where any message lands.

Two consequences of a shared domain matter more than the records.

Reputation is shared. Every live platform inbox sends from the same domain, and Google documents that “Gmail tracks volume, feedback, and limits per domain and IP address”. So the domain reputation your agent inherits was built by every agent on the domain, and your agent’s behaviour contributes to everyone else’s. That cuts both ways: a new agent does not start from zero, and it cannot fully insulate itself either.

The dashboard the 0.3% refers to is not yours to open. The spam-rate thresholds every guide quotes are measured in Google Postmaster Tools, which shows data only for domains you have verified you own. Its setup page says it “won’t display information about your email until your domain is verified” (read 29 September 2026). You cannot verify agents.emailforagents.ai, so you cannot see the number you are being told to keep low. The operator of the domain can.

The way out of shared reputation is your own sending domain, set up as described in agent email on a subdomain and in the domains documentation. Custom domains require a paid plan, and paid plans are not purchasable yet. Builder and Team are proposed plans with checkout disabled, and adding a domain without a paid plan returns HTTP 402 payment_required. When that changes, a domain of your own buys you a reputation that is only yours and a Postmaster Tools dashboard you can read. It does not buy placement.

Cause 1: content that never repeats

Filters learn senders by their mail. A billing system sends the same receipt template ten thousand times, and after the first few hundred are received without complaint, the ten-thousand-and-first is unremarkable. An agent never sends the same message twice. Every body is novel text, often with a different subject shape, a different length and a different set of links.

Google’s guidance for bulk senders assumes the opposite. It says: “If you change the format of your bulk emails, gradually increase the sending volume of messages with the new format.” An agent is, in that framing, changing format on every send. Nothing fixes that without making the agent less useful, but three habits make agent content look less like the mail filters exist to stop.

Send a real plain-text part. The send endpoint accepts text, html, or both, and requires at least one. If you send only html, the platform does not derive a plain-text version: the sanitised HTML string itself is handed to the sending provider as the message’s text field, so recipients on text-only clients can see raw markup. Always send text, and send html as well only if the agent actually needs formatting.

Do not fake a thread. Google’s guidelines say not to “send messages with subject lines starting with Re: or Fwd: unless the messages are actual replies or forwards.” Models often write “Re:” on a follow-up that is really a new message. If the message is a reply, send it as one: pass reply_to_message_id, and the platform sets In-Reply-To and References from the parent, so the subject and the headers agree. If it is not a reply, the subject should not claim to be.

Keep the sender identity boring. Google asks that display names “reflect a consistent, clear, and accurate statement of the sender’s identity”. On this product the From display name is the inbox’s display_name, a property of the inbox rather than something the model supplies per message. The send schema is strict and has no field for arbitrary headers, so the agent cannot add its own. Pick a display name that names the agent or the team behind it, and leave it alone.

What does not work is the advice written for AI outreach tools: make each message look less templated. For an agent the wording already varies. Adding deliberate variation to get past a filter is filter evasion, and it is the same behaviour as the reword-and-resend loop that the 550 5.7.1 article tells you to forbid.

Cause 2: bursts where filters expect a curve

Warmup advice describes a curve: a small volume on day one, rising slowly. Google’s version reads “Send email at a consistent rate. Avoid sending email in bursts,” and “Avoid introducing sudden volume spikes if you do not have a history of sending large volumes.”

Agents do not produce curves. They produce whatever the trigger produces. A CRM export lands and the agent emails forty leads in two minutes. A support queue backs up overnight and the agent answers ninety tickets at 06:00. A retry loop with a bug sends the same recipient list three times. From the receiving side each of these is a spike from a sender with no history of spikes.

You cannot warm up a workload whose volume you do not decide. You can cap it, and the cap should be a platform control rather than an instruction in the prompt, because the prompt is the thing that failed when a loop runs away. Inbox policy has a daily_recipient_limit (an integer from 1 to 1,000,000, or null for none), set by an owner or admin with PUT /v1/projects/{project_id}/inboxes/{inbox_id}/policy. Once it is reached, further sends fail with 403 policy_denied and the message “The daily recipient limit has been reached.” The bucket is keyed by UTC date. A cap does not smooth a burst inside the day, so if the agent is fed batches, meter them in your own queue: a batch of forty sent over an afternoon looks nothing like forty sent in a minute.

Separate the two kinds of agent traffic while you are at it. Replies to people who wrote in are the safest mail an agent sends. Unsolicited first contact is the riskiest. If one inbox does both, the outreach sets the pattern the replies are judged by.

Cause 3: a reply ratio no person has

Google’s guidance keys on engagement throughout. It recommends starting “with a low sending volume to engaged users”, warns that people who did not sign up for your mail “might mark your messages as spam”, and suggests “unsubscribing recipients who don’t open or read your messages.” It also says Google does not track open rates, and this product has no open or click tracking either. So the one engagement signal you can measure yourself is the plainest one: did anyone write back?

A person’s sent folder is dominated by replies. An agent’s often is not: it starts threads, and many of them never hear back. That ratio is visible in the data you already have. Every message in the listing carries a direction of inbound or outbound and a thread_id, so you can compute, per inbox, how many threads the agent started and how many of those got any answer:

const API = "https://api.emailforagents.ai/v1";

async function replyRate(projectId: string, inboxId: string, since: Date) {
  // thread_id -> direction of the oldest message seen, and inbound count
  const threads = new Map<string, { first: string; inbound: number }>();
  const cutoff = since.toISOString();
  let cursor: string | undefined;
  let done = false;
  while (!done) {
    const url = new URL(`${API}/projects/${projectId}/inboxes/${inboxId}/messages`);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.EFA_API_KEY}` },
    });
    if (!res.ok) throw new Error(`list failed: ${res.status}`);
    const page = await res.json();
    for (const m of page.data) {
      if (m.created_at < cutoff) { done = true; break; }
      // an outbound send that was canceled or failed never reached anyone
      if (m.direction === "outbound" && ["canceled", "failed"].includes(m.state)) continue;
      const t = threads.get(m.thread_id) ?? { first: m.direction, inbound: 0 };
      t.first = m.direction; // newest first, so the last one seen is the oldest
      if (m.direction === "inbound") t.inbound += 1;
      threads.set(m.thread_id, t);
    }
    if (!page.has_more) done = true;
    cursor = page.next_cursor;
  }

  const started = [...threads.values()].filter((t) => t.first === "outbound");
  const answered = started.filter((t) => t.inbound > 0).length;
  const rate = started.length ? answered / started.length : null;
  return { started: started.length, answered, rate };
}

The key needs messages:read. The listing is newest first, which is why the loop stops at the first message older than since; a thread that began before the window is counted from its first message inside it, so choose a window of a few weeks. Auto-replies and out-of-office notices arrive as ordinary inbound messages and inflate the rate, so treat the number as a trend, not a score.

There is no universal good value and this article will not invent one. What the number is good for is comparison: between inboxes, between the week before and after a prompt change, between the agent that answers tickets and the agent that does outreach. An inbox whose started-thread reply rate falls sharply is sending mail its recipients did not want, and that is the pattern that produces spam reports, long before anyone tells you.

What you are told, and what nobody can tell you

The honest answer to “did it land in spam” is that the sender cannot know. A receiving server that accepts a message and files it in a spam folder reports the same thing to the sender as one that files it in the inbox: accepted. On this product that surfaces as message.delivery_updated with a status of delivered, and nothing after it. The ladder of what each status proves is laid out in accepted vs delivered email.

Here is what each outcome looks like from your side:

What happened at the recipientWhat you receive hereWhat else changes
Accepted, filed in inboxdelivered on message.delivery_updatedNothing
Accepted, filed in spamdelivered on message.delivery_updatedNothing. Indistinguishable from the row above
Recipient reports spam, provider sends a complaint reportcomplained on message.delivery_updatedAddress suppressed at workspace scope
Gmail user reports spamNothing per messageCounted in an aggregate spam rate only the domain owner sees
Provider suppresses the addresssuppressed on message.delivery_updatedAddress suppressed, recipient unit reversed
Permanent bouncebounced on message.delivery_updatedAddress suppressed

Two rows need explaining.

Gmail does not send complaint reports for individual messages. Its Feedback Loop is aggregate by design: reports are “generated only if a given Identifier is present in a certain volume of mails as well as in distinct user spam reports”, and the data sits in Postmaster Tools for the verified owner of the signing domain (Feedback Loop, read 29 September 2026). AWS states it more bluntly for its own email service: “Gmail does not send spam complaint events because their priority is to protect the privacy of their users” (AWS Messaging blog, 19 July 2024). So a complained status reaches you only from mailbox providers that send per-message reports. The absence of complaints tells you very little about Gmail recipients.

The spam rate can look better as things get worse. Google defines the Postmaster Tools spam rate as “the percent of your messages that are delivered to engaged recipient’s Inbox and then marked as spam by the recipient,” and adds: “If Gmail automatically sends a significant number of your messages to spam, the rate shown in the dashboard might seem low, because recipients get fewer of your messages in their Inbox.” It also warns that dashboards “might not include all data on days when your outgoing email volume is low” (Postmaster Tools dashboards, read 29 September 2026). A low-volume agent whose mail is already being filed as spam can therefore look clean on the one metric every guide tells you to watch. This is the detail the ranking pages leave out, and it is why the reply rate above is a better early signal for agent mail than any complaint count.

When an address is suppressed, you are not sent a separate suppression event. The change shows up on your next send to that address, which fails at request time with 422 recipient_suppressed, marked non-retryable. There is no automatic complaint-rate threshold on a workspace in this build: a complaint suppresses one address, and the workspace keeps sending. Two declared event types sound relevant and are not emitted by this build: project.sending_paused and message.quarantined appear in the event vocabulary but nothing fires them, and message.quarantined concerns inbound processing in any case. If sending is stopped at the workspace level, you learn it from a 403 permission_denied on the send (“Sending is paused or the resource is inactive.”), not from an event. And message.failed is not a spam signal at all: it fires when the provider refuses a submission, or with a reason of provider_retry_window_expired or reply_parent_headers_not_available.

A diagnostic order when someone says it went to spam

Work in this order, cheapest check first.

  1. Confirm it was sent live and delivered. Check environment and simulated on the message and the recipient’s delivery_status. A test inbox cannot reach an internet recipient, so “it went to spam” about a test send means it went nowhere. My agent says it sent it, nobody received it covers this branch in detail.
  2. Get the headers from the recipient. Ask them to open the message in their spam folder and view the original. The Authentication-Results header shows whether SPF, DKIM and DMARC passed at their server. On a platform address they should; if they did not, report it to support@agents.emailforagents.ai with the message ID, because it is not a record you can change.
  3. Ask them to mark it not spam. Google’s own guidance says recipients “can mark valid messages as not spam, so future messages from the sender should be delivered to their inbox.” For a recipient who wants the agent’s mail, this is the step most likely to help.
  4. Read the message the agent wrote. Check for a missing text part, a fake “Re:”, links whose visible text does not match their target, and claims of urgency. Google asks that links “be visible and easy to understand” and that content not be hidden with HTML and CSS.
  5. Check the shape of the day. Was it one of forty messages sent in a minute? Was the recipient cold? Compare the inbox’s started-thread reply rate with its recent history.
  6. Change the workload, not the wording. Split outreach from replies, set a daily_recipient_limit, or set the inbox’s approval_mode to "always" so a person reads every send from that inbox before it goes. Do not have the agent resend a reworded copy to the same person to see if it gets through.

The inboxes overview lists the controls this relies on. None of them places mail in an inbox. What they do is keep an agent’s traffic close to the shape receiving systems were built to trust: consistent, expected, and answered.


Keep reading

All guides · Documentation · Inboxes