Skip to content

Use cases · Published 2026-09-29 · AgentiSend team

Agent daily digest

An agent sends one digest a day to people who already receive it. The same digest sent again inside the window is held.

An agent sends one digest a day to people who already get that digest from you. Each recipient is one send, with that day's subject and body, from a verified domain.

The ceiling

PATCH /limits/keys/{id} for the digest key:

{
  "budget_per_period": 100,
  "period": "monthly",
  "rate_ceiling_per_minute": 5
}

One hundred is the count of digest copies in the monthly period, not a suggestion to add recipients. It is under the Free inclusion of 1,000. The rate ceiling is five a minute.

The send

/**
 * Vercel AI SDK — a `sendEmail` tool for an agent that holds its own key.
 *
 * The key in `AGENTISEND_API_KEY` here is the agent's, not yours. It was
 * minted with `POST /api-keys` as `sending_access`, given a ceiling with
 * `PATCH /limits/keys/:id`, and `POST /limits/kill-all` stops it along with
 * every other key. Those three calls run in examples/ai-agent-with-budget;
 * this file is the other half, the tool the model calls once the key exists.
 *
 * Two rules make it safe to hand to a model. The idempotency key is derived
 * from the purpose and the recipient, never from the moment, so a model that
 * retries a timed-out call replays the first send instead of mailing someone
 * twice. And a refusal is returned, not thrown: the model reads `code` and
 * `fix` and acts on them — for `agent_budget_exceeded` and `approval_required`
 * that means stopping and saying so, because the fix names a person.
 */
import { tool } from 'ai';
import { z } from 'zod';
import { AgentiSend, AgentiSendError } from 'agentisend';

/** The agent's own key: `sending_access`, with a budget on it. */
const agentisend = new AgentiSend();

function mailFrom(): string {
  const from = process.env.MAIL_FROM;
  if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
  return from;
}

/** What the model gets back: a message id, or a refusal it can act on. */
export type SendEmailResult =
  | { sent: true; id: string }
  | { sent: false; code: string; fix: string };

export const sendEmail = tool({
  description:
    'Send one email to a person who asked for it. Returns the message id, or a refusal ' +
    'with a code and a fix. When the fix says a person decides, stop and report it: ' +
    'calling again will not change the answer.',
  inputSchema: z.object({
    to: z.string().describe('The recipient, one address.'),
    subject: z.string().min(1).max(200),
    text: z.string().min(1).describe('The plain-text body.'),
    purpose: z
      .string()
      .regex(/^[a-z0-9][a-z0-9_-]{0,63}$/)
      .describe(
        'What this message is, for example "ticket-4182-update". The same purpose to the ' +
          'same recipient sends once, however many times it is called.',
      ),
  }),
  execute: async ({ to, subject, text, purpose }): Promise<SendEmailResult> => {
    try {
      const { id } = await agentisend.emails.send(
        { from: mailFrom(), to, subject, text },
        // Derived from the thing being done, not from the moment it was asked
        // for: a retry after a timeout replays the first send.
        { idempotencyKey: `${purpose}/${to}` },
      );
      return { sent: true, id };
    } catch (err) {
      if (err instanceof AgentiSendError) {
        // The API checked the address, the suppression list, the budget and
        // the loop guard, and the refusal names its fix. The model can read
        // that; it cannot read an exception.
        return { sent: false, code: err.code, fix: err.fix };
      }
      throw err;
    }
  },
});
examples/vercel-ai-sdk/send-email-tool.ts, run by the test suite against a local AgentiSend server

Derive the idempotency key from the day and the recipient, the way that file derives it from the purpose. A retry of the same digest replays the first send.

The refusal

Spending the key's budget refuses the next copy. The catalogue fix:

{
  "error": {
    "code": "agent_budget_exceeded",
    "message": "This key has used 1 of its 1 recipients for the current monthly period. Each To, CC and BCC recipient counts as one email.",
    "fix": "Wait for the period to reset — get_agent_budget and GET /limits/keys/:id both say when. Raising a budget is a person’s decision, made in the console; a key cannot raise its own.",
    "docs_url": "https://agentisend.com/what-does-agent-budget-exceeded-mean#agent_budget_exceeded",
    "retryable": false
  }
}

Honest limit

Sending the same digest again to the same address inside 60 minutes is held once it would be the 4th copy. Reusing an idempotency key with a different body is idempotency_payload_mismatch, and the fix is to send the same body or a new key.

Next