send-email-to-mailing-list
Send Email to Mailing List
This skill teaches Claude how to send an email to a mailing list, either (a) from any server-side TypeScript/JavaScript project that imports @schemavaults/send-email, or (b) directly from a Claude Code session — for example, to send a "I just finished this workflow" notification at the end of a task. In both cases the sendEmailToMailingList() helper wraps the SchemaVaults mail-server POST /api/send route and automatically resolves the API key from environment variables. The skill is self-contained and portable — drop it into any project's .claude/skills/ folder and you're done.
When to use this skill
There are two distinct use cases. Either fits this skill:
(a) Application code needs to send a notification to a mailing list audience. For example:
- New user signup / first-purchase events
- Unhandled errors in background jobs or cron tasks
- Billing / subscription lifecycle events (trial ending, payment failed)
- Ops alerts (deploy succeeded, rate-limit tripped, healthcheck failed)
- Any ad-hoc "FYI, this just happened" message intended for a mailing list audience
(b) Claude Code itself wants to notify a mailing list at the end of a workflow. For example:
- Claude just finished implementing a feature and pushed the branch.
- Claude finished reviewing a PR and posted comments.
- A long-running build, migration, or CI task finished (success or failure).
- A scheduled maintenance script Claude was orchestrating completed.
For use case (b), see the "Usage -- Claude Code post-workflow notification" section below.
Do not use it for:
- Sending to individual end users (use
sendEmail()with an email string instead) - Client-side / browser code (the API key is a secret)
- High-volume broadcasts beyond 50 recipients per send (the mail-server caps each send call at 50 recipients)
Prerequisites
-
Install the helper package in the target project:
bun add @schemavaults/send-email # or: npm install @schemavaults/send-email -
Set two environment variables wherever the code runs (local dev, CI, production):
SCHEMAVAULTS_MAIL_API_KEY-- Bearer token issued from the mail-server'sapi_keystable. Always starts withsvlts_mail_pk_. Treat it like any other secret; never commit it, never ship it to browsers.SCHEMAVAULTS_MAILING_LIST_ID-- UUID of the target mailing list from the mail-server'sMAILING_LISTStable.
Both are mandatory when not passing values directly -- the helper throws
Error("Failed to load ... from environment variable ...")if either is missing. -
Optional third env var:
SCHEMAVAULTS_APP_ENVIRONMENT="production"|"development"|"staging". If unset, the helper falls back toproductionand targets the production mail-server. Only set this when you explicitly want to hit a non-prod environment. -
Call only from server-side code -- API routes, server actions, cron handlers, background workers. Never from a React client component or browser bundle.
Usage -- template form (preferred)
When a React Email template already exists in the mail-server catalog, reference it by template_id so the rendering (HTML + plain text) happens on the mail-server. Use listEmailTemplates() from @schemavaults/send-email (or see the list-email-templates skill) to discover available template IDs.
import { sendEmailToMailingList } from "@schemavaults/send-email";
export async function notifyMailingListOfSignup(userName: string): Promise<void> {
await sendEmailToMailingList({
body: {
subject: `New signup: ${userName}`,
message: {
template_id: "<template-id-from-GET-/api/templates>",
template_props: {
/* prop shape per the template's description field */
},
},
},
});
}
If none of the registered templates fits your notification, use the raw text/html form below instead of trying to bend a mismatched template.
Usage -- raw HTML/text form (ad-hoc)
For one-off notifications where spinning up a dedicated React Email template is overkill, supply text and html directly. Both fields are required.
import { sendEmailToMailingList } from "@schemavaults/send-email";
export async function notifyMailingListOfError(err: Error, context: string): Promise<void> {
const subject = `[alert] ${context}: ${err.message}`;
const text =
`An error occurred in ${context}.\n\n` +
`Message: ${err.message}\n\n` +
`Stack:\n${err.stack ?? "(no stack)"}\n`;
const html =
`<p>An error occurred in <code>${context}</code>.</p>` +
`<p><strong>Message:</strong> ${err.message}</p>` +
`<pre>${err.stack ?? "(no stack)"}</pre>`;
await sendEmailToMailingList({
body: { subject, message: { text, html } },
});
}
Escape user-supplied values before embedding them in html if they can contain < / > / & -- the mail-server does not sanitize this for you.
Usage -- passing a mailing list ID explicitly
By default sendEmailToMailingList reads the mailing list UUID from the SCHEMAVAULTS_MAILING_LIST_ID env var. You can override this per-call:
await sendEmailToMailingList({
mailingListId: "00000000-0000-0000-0000-000000000000",
body: {
subject: "Hello from a specific list",
message: { text: "Hello", html: "<p>Hello</p>" },
},
});
Usage -- CLI (preferred for one-off / ad-hoc sends)
@schemavaults/send-email ships a schemavaults-send-email binary that wraps the same helper. For any one-off send -- a manual notification, a quick smoke test, a bash cron entry, or Claude Code firing off a single end-of-workflow email -- the CLI is the simplest path. No /tmp/ script, no bun run.
# Raw text/html (both required)
bunx schemavaults-send-email send-to-mailing-list \
--subject "[ops] nightly backup finished" \
--text "Backup completed at $(date -u +%FT%TZ). 0 errors." \
--html "<p>Backup completed at $(date -u +%FT%TZ). <strong>0 errors.</strong></p>"
# Template-based
bunx schemavaults-send-email send-to-mailing-list \
--subject "Welcome aboard, Alice" \
--template-id welcome-email \
--template-props '{"name":"Alice"}'
# Override the mailing list per-call
bunx schemavaults-send-email send-to-mailing-list \
--mailing-list-id 00000000-0000-0000-0000-000000000000 \
--subject "..." --text "..." --html "..."
# Long bodies: read from files instead of inline strings
bunx schemavaults-send-email send-to-mailing-list \
--subject "weekly digest" \
--text-file /tmp/digest.txt \
--html-file /tmp/digest.html
# Or supply the entire request body as a JSON file (validated server-side)
bunx schemavaults-send-email send-to-mailing-list --body-file /tmp/payload.json
(Substitute npx for bunx if bun is unavailable.) The CLI reads SCHEMAVAULTS_MAIL_API_KEY and SCHEMAVAULTS_MAILING_LIST_ID from the environment exactly like the helper, exits non-zero with a one-line error on failure, and exits 0 on a successful 200 from the mail-server.
Run bunx schemavaults-send-email send-to-mailing-list --help for the full flag reference.
Validating before sending (--dry-run)
Because a mailing-list send fans out to every subscriber, never use a real send to validate your request. Use --dry-run to round-trip Zod body + template_props shape + API-key/mailing-list scoping through the server without dispatching anything:
bunx schemavaults-send-email send-to-mailing-list --dry-run \
--subject "Welcome aboard, Alice" \
--template-id welcome-email \
--template-props '{"name":"Alice"}'
A successful dry-run prints [dry-run] mailing-list request validated; no email sent. and exits 0. A malformed request exits non-zero with the server's validation error.
Usage -- Claude Code post-workflow notification
Claude itself can use this skill to send a one-shot notification to a mailing list at the end of a workflow in any repo that depends on @schemavaults/send-email (this repo already does).
Preferred: invoke the CLI directly
For most end-of-workflow notifications (a few sentences plus a short bullet list) the CLI is the right tool -- one shell command, no scratch file:
bunx schemavaults-send-email send-to-mailing-list \
--subject "[claude-code] workflow finished: <short description>" \
--text "$(printf 'Claude just finished a workflow.\n\nSummary:\n- <bullet 1>\n- <bullet 2>\n- <bullet 3>\n')" \
--html "$(printf '<p>Claude just finished a workflow.</p><p><strong>Summary:</strong></p><ul><li><bullet 1></li><li><bullet 2></li><li><bullet 3></li></ul>')"
Replace the <short description> and bullet placeholders with a concrete summary. Keep the subject under ~70 characters and the body scannable (3-5 bullets is usually enough). A non-zero exit means the helper threw -- surface the error in your summary to the user rather than retrying silently.
Fallback: write a /tmp/ script
Reach for the script form only when the body is large enough or templated enough that string-quoting in shell is awkward (e.g. multi-paragraph HTML, dynamic data assembly, conditional content):
// /tmp/send-notification-after-workflow.ts
import { sendEmailToMailingList } from "@schemavaults/send-email";
async function main(): Promise<void> {
await sendEmailToMailingList({
body: {
subject: "[claude-code] workflow finished: <short description>",
message: {
text:
"Claude just finished a workflow.\n\n" +
"Summary:\n" +
"- <bullet 1>\n" +
"- <bullet 2>\n" +
"- <bullet 3>\n",
html:
"<p>Claude just finished a workflow.</p>" +
"<p><strong>Summary:</strong></p>" +
"<ul>" +
"<li><bullet 1></li>" +
"<li><bullet 2></li>" +
"<li><bullet 3></li>" +
"</ul>",
},
},
});
console.log("[notify] sent");
}
main().catch((err) => {
console.error("[notify] failed:", err);
process.exit(1);
});
Run from the repo root so Bun resolves @schemavaults/send-email through the repo's node_modules/:
bun run /tmp/send-notification-after-workflow.ts
When to trigger this
Send exactly one notification at the end of a workflow, after all commits and pushes have landed, so the email reflects the final state.
Cautions
- The env vars
SCHEMAVAULTS_MAIL_API_KEYandSCHEMAVAULTS_MAILING_LIST_IDmust be set in Claude's process. If they're missing, the helper (and CLI) throws a clear error -- report it to the user instead of retrying blindly. - One notification per workflow, not per step. If a workflow had no meaningful outcome (e.g. "user asked a question, Claude answered"), skip the notification entirely. The inbox should not become chatty.
- Do not send the notification before the work is finished. Push first, notify second.
- Ask before sending if the user hasn't explicitly opted in to post-workflow notifications. Sending email is a side effect visible to other humans; don't do it silently on tasks where the user hasn't asked for it.
- Never send a blank or "test" email to a mailing list to validate a request. Use
--dry-run(CLI) ordryRun: true(helper body). The mail-server validates the full request -- includingtemplate_propsshape -- without dispatching to the audience. A misfired real test reaches every subscriber.
Request body shape
sendEmailToMailingList accepts Omit<SendEmailRequestBody, "to" | "cc" | "bcc"> -- the audience is the mailing list, so to, cc, and bcc are intentionally not allowed. Allowed fields:
type MailingListNotificationBody = {
subject: string;
message:
| { template_id: string; template_props?: unknown }
| { text: string; html: string };
from?: string; // defaults to the mail-server's configured sender
replyTo?: string; // optional reply-to override
dryRun?: boolean; // server validates without dispatching
};
// Full call signature:
type ISendEmailToMailingListOpts = {
body: MailingListNotificationBody;
mailingListId?: string; // override SCHEMAVAULTS_MAILING_LIST_ID
bearerToken?: string; // override SCHEMAVAULTS_MAIL_API_KEY; rarely needed
mailServerUrl?: string; // override the server origin; rarely needed
environment?: "production" | "development" | "staging";
dryRun?: boolean; // convenience; sets body.dryRun
};
Error handling
The helper throws on any non-200 response -- wrap the call in try/catch whenever a failed notification should not break the caller's primary flow:
try {
await sendEmailToMailingList({
body: {
subject: `New signup: ${userName}`,
message: { text, html },
},
});
} catch (notifyErr) {
console.error("[notify] failed to send mailing list notification", notifyErr);
}
Common failure modes:
| Error | Cause |
|---|---|
Failed to load API key from environment variable 'SCHEMAVAULTS_MAIL_API_KEY' |
Env var not set (or empty string) in the runtime environment. |
Failed to load mailing list ID from environment variable 'SCHEMAVAULTS_MAILING_LIST_ID' |
Env var not set (or empty string) in the runtime environment. |
Bad request body to send email with! |
Your body does not match the schema -- typically a missing subject, missing text/html pair, or unknown fields. |
Invalid or revoked API key. (HTTP 401) |
SCHEMAVAULTS_MAIL_API_KEY is wrong, expired, or revoked. |
This API key is not permitted... (HTTP 403) |
The API key is allowlisted to a different mailing list than the one targeted. |
Failed to parse request body! (HTTP 400) |
Server-side Zod parsing failed; usually a template template_props shape mismatch. |
Environment targeting
By default the helper resolves the mail-server URL for the production environment. To hit staging or development explicitly:
await sendEmailToMailingList({
environment: "development",
body: {
subject: "dev smoke test",
message: { template_id: "my-test-email", template_props: { name: "test" } },
},
});
Or set SCHEMAVAULTS_APP_ENVIRONMENT at the process level -- the helper reads it via getAppEnvironment() from @schemavaults/app-definitions when opts.environment is not passed in.
Adding this skill to another project
- Copy this file into the target project's
.claude/skills/folder. - In the target project, install the helper package:
bun add @schemavaults/send-email - Populate the two environment variables via the project's normal secret management (e.g.
.env.localfor local dev, your hosting provider's secret store for production):SCHEMAVAULTS_MAIL_API_KEY=svlts_mail_pk_... SCHEMAVAULTS_MAILING_LIST_ID=00000000-0000-0000-0000-000000000000 - Commit the skill file. The next Claude Code session in that repo will automatically discover the skill.
Reference
Source files inside the installed package (node_modules/@schemavaults/send-email/dist/) -- read these when you need ground truth:
send-email-to-mailing-list.{d.ts,js}-- thesendEmailToMailingList()helper and itsISendEmailToMailingListOptsinterface.send-email.{d.ts,js}-- the underlyingsendEmail()implementation, includinggetSchemaVaultsMailApiKey()and server-URL resolution.send-email-request-body-schema.{d.ts,js}-- the Zod schema (createSendEmailRequestBodySchema) that both the client helper and the mail-server route use to validate bodies.index.d.ts-- package entry point; lists every exported symbol.