Email sending architecture when the sender improvises
Conventional email infrastructure architecture assumes a deterministic caller: your code decides to send, and the question is how to make that send reliable. An agent changes the assumption at the front. The caller now decides for itself who to write to and what to say, based partly on text someone else wrote.
That adds a stage. A traditional pipeline is compose, enqueue, dispatch, observe. An agent pipeline needs a decision boundary before any of that — a point where the proposed send is checked against constraints the agent cannot alter.
| Stage | Responsibility | Failure if missing |
|---|---|---|
| 1. Propose | Agent produces recipient, subject, body | — |
| 2. Constrain | Scope, allowlist, rate ceiling, approval checked | Agent emails anyone it was persuaded to |
| 3. Persist | Intent written durably, in your own transaction | Sends lost on crash or deploy |
| 4. Dispatch | Worker sends, retries, respects idempotency | Duplicates, or silent drops |
| 5. Reconcile | Delivery events fed back to suppression and context | Repeatedly mailing dead addresses |
Stage 2 is the one that does not exist in normal pipelines
Constraint has to sit between the decision and the side effect, and it has to live somewhere the agent cannot reach. A rule in the system prompt is a request — the attacker writing into the agent context is making requests too, further down the same window.
In practice that means the credential carries the ceiling: scopes narrow enough that the key cannot do what you never intended, a recipient allowlist enforced server-side, a rate limit that is counted rather than documented, and an approval mode for anything whose blast radius justifies a human.
The test is simple. If a perfectly persuaded agent asked for the worst thing you can imagine, what stops it? If the answer is a line in a prompt, stage 2 is missing.
Stage 3: where the durability boundary goes
The agent should not be waiting on a provider API, and the send must not be lost if the process dies between deciding and dispatching. Both are solved by writing the intent to your own database in the same transaction as whatever caused it, and letting a worker drain the table.
This is the transactional outbox, covered in depth elsewhere in this series. Architecturally the point is narrow: the durability boundary belongs inside your transaction, not at the provider. Once the row is committed the send will happen eventually, whatever crashes next.
// The agent's tool returns once the intent is durable — not once mail is sent.
export async function proposeSend(ctx: AgentContext, draft: Draft) {
const verdict = await constrain(ctx, draft); // stage 2
if (!verdict.allowed) return { refused: verdict.reason };
await db.transaction(async (tx) => { // stage 3
await tx.agentActions.insert({ agentId: ctx.agentId, draft });
await tx.outbox.insert({ payload: draft, idempotencyKey: draft.id });
});
return { queued: draft.id };
}Stage 5: closing the loop back into the agent
The stage most agent pipelines never build. Bounces, complaints and suppressions arrive asynchronously by webhook, long after the tool call returned, and if nothing consumes them the agent will cheerfully mail a hard-bounced address again tomorrow.
Two consumers are needed. Suppression is mechanical: a hard bounce or complaint suppresses the address account-wide, immediately and permanently, and every later send checks it. Context is the agent-facing half: the fact that an address is undeliverable belongs in what the agent knows, so it stops proposing sends that will be refused.
What belongs to the agent, and what does not
A useful division when designing this: the agent owns judgement, the pipeline owns guarantees. The agent decides what is worth saying and to whom. It should never be responsible for retrying, deduplicating, respecting a rate limit, or knowing whether an address bounced last week.
Every one of those is a property the pipeline can guarantee deterministically and the agent can only approximate. Leaving them to the model is how you get an agent that retries a permanent failure forty times, or sends three copies because the first response was slow.
Building AI agents that send email?
Join the SadaSend early access waitlist to get scoped API keys, recipient allowlists, and Model Context Protocol (MCP) servers upon launch.