Skip to content
Browse the docs

Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend

Send email with RedwoodJS

A RedwoodJS service function that sends one note with the Node SDK and returns the message id or the refusal.

To send email with RedwoodJS, put the send in a service function. Redwood's services guide says a service is an ordinary function, and that GraphQL resolvers are those functions. The function below takes { input }, checks the address, calls emails.send with an idempotency key taken from the address in input, and returns the message id or the refusal's code and fix. The suite calls the function against a local AgentiSend server. It does not boot Redwood.

Store the id the function returns. GET /emails/{id}/explain names the check that held the message instead of delivering it.

Install

pnpm add agentisend

Name the GraphQL field sendReceipt so Redwood calls the exported function. The services guide's contact form submits input:

mutation SendReceipt($input: SendReceiptInput!) {
  sendReceipt(input: $input) {
    id
  }
}

SendReceiptInput is the SDL type you declare for email and note. The page calls create({ variables: { input: data } }) in that guide; pass the same input here.

Environment

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The route

/**
 * RedwoodJS — a service function. Redwood's GraphQL resolvers are services,
 * and a service is a function that receives `{ input }`.
 *
 * There is no Redwood package for this API. The function posts through the
 * Node SDK. The suite calls it on Node and does not boot Redwood.
 *
 * AGENTISEND_API_KEY, MAIL_FROM, and optionally AGENTISEND_BASE_URL.
 */
import { AgentiSend, AgentiSendError } from 'agentisend';

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;
}

/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function sendReceipt(args: {
  input: { email?: string; note?: string };
}): Promise<{ status: number; body: Record<string, unknown> }> {
  const email = String(args.input.email ?? '').trim();
  const note = String(args.input.note ?? '');
  if (!ADDRESS.test(email)) {
    return { status: 400, body: { error: 'email must be one address, like you@example.com' } };
  }

  try {
    const { id } = await agentisend.emails.send(
      {
        from: mailFrom(),
        to: email,
        subject: 'We received your note',
        text: `Note: ${note}`,
      },
      // The person the mutation names, not the moment it ran.
      { idempotencyKey: `redwood/${email}` },
    );
    return { status: 200, body: { id } };
  } catch (err) {
    if (err instanceof AgentiSendError) {
      return { status: err.status, body: { code: err.code, fix: err.fix } };
    }
    return { status: 500, body: { error: 'send failed' } };
  }
}
examples/redwoodjs/send.ts, run by the test suite against a local AgentiSend server

Try it

Call the service with { input: { email: 'you@example.com', note: 'hello' } }. The GraphQL field returns body.id from that result.

What comes back when it goes wrong

Every refused request carries code, message, fix and docs_url. Return those two fields from the service so the mutation error can show fix without a person reading the prose. The error catalogue lists every code.

Next