Newsletter draft
Writes one email for a list of people who asked to hear from the business,
from the owner's facts only, in the shape the CAN-SPAM guide asks for. The
owner approves and sends; the agent never sends to a list. The rules are
summarised in reference/can-spam-summary.md.
Inputs
facts.mdfrom the owner (seeexamples/facts.md) with these lines:Sender name:,Postal address:,List:(which consented list), the shop or booking link, then the month's facts, allergen or safety wording copied from labels, and topics never to mention.- Past issues the owner liked, for tone and length.
- For a seasonal email to past customers: an export the owner made with only the people who gave contact permission. Work on it on the owner's machine and never paste it into a hosted model unless the owner agreed.
Steps
- [ ] 1 Confirm the list is a consented list
- [ ] 2 Draft subject, From and body from the facts
- [ ] 3 Add the footer: why they get it, unsubscribe, postal address
- [ ] 4 Run the pre-send check and fix
- [ ] 5 Hand the draft to the owner through the gateway
-
List. Ask which list it goes to and how people joined it. If anyone on it did not agree (bought once, gave a card at a show without ticking a box), stop and tell the owner; that is their call, not yours.
-
Draft. Start with
Subject:andFrom:lines. The subject says what is inside, no "Re:" or "Fwd:", no false urgency. The body: a short greeting, one short section per fact, the link, a sign-off. Dates, times and prices exactly as in the facts. Allergen or safety wording copied from the label, word for word. Notes to the owner go on lines starting with>. -
Footer. Why they are getting it, how to stop ("unsubscribe"), and the postal address line exactly as in the facts. The owner's email tool usually adds its own unsubscribe link too; keep yours.
-
Check. Run:
node scripts/presend-check.mjs facts.md newsletter.mdexamples/newsletter-first-draft.mdfails it (examples/expected-check-first-draft.txt: fake reply subject, wrong sender, no address, no unsubscribe, invented numbers). The correctedexamples/newsletter-ready.mdpasses (examples/expected-check-ready.txt). Fix and re-run until it prints READY. -
Hand over. Send the draft to the owner as a draft through the AHL Action Gateway (
gmail.draftormanual.draft, see the gateway-call skill) or save it in the shared folder. The owner loads it into their email tool, chooses the list and presses send. The agent never holds the email tool's login and never sends to a list itself; a one-to-one test copy to the owner is a heldgmail.sendthe owner approves.
Output
The newsletter file (Subject, From, body, footer) and the READY check output,
with > notes listing what you removed or changed and why.
What the check misses
It reads text only. It cannot see the list, so it cannot tell whether everyone agreed or whether opt-outs were removed. It does not judge whether the subject is honest, and it does not catch claims without numbers: in the example, "healthy keto snack" passes the script and is removed by reading the never-mention list. Read the whole email against the facts every time.
Edge cases
- Offers and prices: only the owner's numbers and terms, with an end date; re-check on the day the owner sends.
- Customer stories or photos: only with the customer's permission, which the owner confirms.
- Health, diet or results claims: never, unless the owner wrote them and can back them up.
- A transactional message (an order update) is not a newsletter; do not mix promotions into it without the owner's say.
Never
- Never send to a list, import contacts, or re-add someone who opted out.
- Never write a subject that misleads about the content or pretends to be a reply.
- Never invent numbers, sales results, scarcity ("only 3 left") or quotes.
- Never ask for or store the email tool's password.
Hand over
The owner checks the list, reads the email next to the facts and the >
notes, sends a test to themselves, and sends. Keep the facts, the final text
and the check output with the month's campaign record.
The skill folder
Every file in the folder. Download each one into a folder with the same layout, or copy it.
newsletter-draft/ SKILL.md examples/expected-check-first-draft.txt examples/expected-check-ready.txt examples/facts.md examples/newsletter-first-draft.md examples/newsletter-ready.md reference/can-spam-summary.md scripts/presend-check.mjs
SKILL.md
---
name: newsletter-draft
description: Drafts a small business's email newsletter or seasonal customer email from the owner's facts for the month, with an honest subject line, the sender's name, the postal address and an unsubscribe line, then runs a local pre-send check for those items and for numbers the facts do not support. Use when an owner wants a monthly newsletter, a new-arrivals or event email, a seasonal reminder to past customers who agreed to hear from them, or a check of an email before it goes to a list.
metadata:
ahl-category: marketing
ahl-jobs: campaigns, content, nurturing
ahl-industries: specialty-food, cafe-bakery, boutique, hvac
ahl-workflows: marketing--05-weekly-news-to-posts-and-newsletter, marketing--08-seasonal-email-to-past-customers, marketing--2109-specialty-food-new-arrivals-and-tastings-email, marketing--1427-cafe-bakery-lapsed-regulars-win-back
ahl-human-check: The owner approves the final text and the recipient list, and sends it from their own email tool; only people who agreed to receive it are on the list.
ahl-proposed-status: approved
ahl-version: 1
ahl-sources: |
FTC — CAN-SPAM Act: A Compliance Guide for Business — https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business — 2026-10-06
---
# Newsletter draft
Writes one email for a list of people who asked to hear from the business,
from the owner's facts only, in the shape the CAN-SPAM guide asks for. The
owner approves and sends; the agent never sends to a list. The rules are
summarised in `reference/can-spam-summary.md`.
## Inputs
- `facts.md` from the owner (see `examples/facts.md`) with these lines:
`Sender name:`, `Postal address:`, `List:` (which consented list), the
shop or booking link, then the month's facts, allergen or safety wording
copied from labels, and topics never to mention.
- Past issues the owner liked, for tone and length.
- For a seasonal email to past customers: an export the owner made with only
the people who gave contact permission. Work on it on the owner's machine
and never paste it into a hosted model unless the owner agreed.
## Steps
```
- [ ] 1 Confirm the list is a consented list
- [ ] 2 Draft subject, From and body from the facts
- [ ] 3 Add the footer: why they get it, unsubscribe, postal address
- [ ] 4 Run the pre-send check and fix
- [ ] 5 Hand the draft to the owner through the gateway
```
1. **List.** Ask which list it goes to and how people joined it. If anyone on
it did not agree (bought once, gave a card at a show without ticking a box),
stop and tell the owner; that is their call, not yours.
2. **Draft.** Start with `Subject:` and `From:` lines. The subject says what is
inside, no "Re:" or "Fwd:", no false urgency. The body: a short greeting,
one short section per fact, the link, a sign-off. Dates, times and prices
exactly as in the facts. Allergen or safety wording copied from the label,
word for word. Notes to the owner go on lines starting with `>`.
3. **Footer.** Why they are getting it, how to stop ("unsubscribe"), and the
postal address line exactly as in the facts. The owner's email tool usually
adds its own unsubscribe link too; keep yours.
4. **Check.** Run:
```bash
node scripts/presend-check.mjs facts.md newsletter.md
```
`examples/newsletter-first-draft.md` fails it
(`examples/expected-check-first-draft.txt`: fake reply subject, wrong
sender, no address, no unsubscribe, invented numbers). The corrected
`examples/newsletter-ready.md` passes (`examples/expected-check-ready.txt`).
Fix and re-run until it prints READY.
5. **Hand over.** Send the draft to the owner as a draft through the AHL Action
Gateway (`gmail.draft` or `manual.draft`, see the gateway-call skill) or
save it in the shared folder. The owner loads it into their email tool,
chooses the list and presses send. The agent never holds the email tool's
login and never sends to a list itself; a one-to-one test copy to the owner
is a held `gmail.send` the owner approves.
## Output
The newsletter file (Subject, From, body, footer) and the READY check output,
with `>` notes listing what you removed or changed and why.
## What the check misses
It reads text only. It cannot see the list, so it cannot tell whether everyone
agreed or whether opt-outs were removed. It does not judge whether the
subject is honest, and it does not catch claims without numbers: in the
example, "healthy keto snack" passes the script and is removed by reading the
never-mention list. Read the whole email against the facts every time.
## Edge cases
- **Offers and prices:** only the owner's numbers and terms, with an end date;
re-check on the day the owner sends.
- **Customer stories or photos:** only with the customer's permission, which
the owner confirms.
- **Health, diet or results claims:** never, unless the owner wrote them and
can back them up.
- **A transactional message** (an order update) is not a newsletter; do not mix
promotions into it without the owner's say.
## Never
- Never send to a list, import contacts, or re-add someone who opted out.
- Never write a subject that misleads about the content or pretends to be a reply.
- Never invent numbers, sales results, scarcity ("only 3 left") or quotes.
- Never ask for or store the email tool's password.
## Hand over
The owner checks the list, reads the email next to the facts and the `>`
notes, sends a test to themselves, and sends. Keep the facts, the final text
and the check output with the month's campaign record.
examples/expected-check-first-draft.txt
NOT READY (6) - Subject "Re: your October order" pretends to be a reply or forward. - From "The Deli Team" does not name the sender "Little Oak Deli". - The postal address "12 Sample Street, Exampleton, ST 00000" is not in the body. - No unsubscribe line in the body. - Body line 6: "3pm" is not in the facts file. - Body line 8: "3" is not in the facts file.
examples/expected-check-ready.txt
READY FOR THE OWNER: subject, sender, postal address, unsubscribe line and numbers check out. Still for a person: the list "newsletter sign-ups (people who ticked "send me the monthly email")" holds only people who agreed, and recent opt-outs are removed.
examples/facts.md
# October newsletter facts: Little Oak Deli (invented) Sender name: Little Oak Deli Postal address: 12 Sample Street, Exampleton, ST 00000 List: newsletter sign-ups (people who ticked "send me the monthly email") Booking or shop link: https://shop.example.com/little-oak - New this month: aged cheddar from Test Valley Creamery; smoked almonds; two new olive oils. - Tasting: Saturday 2026-10-17, 11am to 2pm, in the shop, no booking needed. - Holiday pre-orders open 2026-11-01 (owner will confirm the cut-off date later). - Staff pick from Jo: the smoked almonds with the aged cheddar. - Allergens: the almonds are packed in a facility that also handles peanuts (from the label). - Never mention: diets, health claims, other shops.
examples/newsletter-first-draft.md
Subject: Re: your October order From: The Deli Team Hello neighbours, New on the shelves: aged cheddar from Test Valley Creamery, smoked almonds and two new olive oils. The almonds are a healthy keto snack. Come and taste them on Saturday 2026-10-17, 11am to 3pm. No booking needed. Holiday pre-orders open 2026-11-01. Order early: last year we sold out in 3 days. Jo's pick: the smoked almonds with the aged cheddar.
examples/newsletter-ready.md
Subject: New cheese, smoked almonds and a Saturday tasting From: Little Oak Deli Hello neighbours, New on the shelves this month: aged cheddar from Test Valley Creamery, smoked almonds and two new olive oils. (The almonds are packed in a facility that also handles peanuts.) Come and taste them on Saturday 2026-10-17, 11am to 2pm, in the shop. No booking needed. Holiday pre-orders open 2026-11-01. We will send the cut-off date as soon as it is set. Jo's pick: the smoked almonds with the aged cheddar. See you soon, Little Oak Deli https://shop.example.com/little-oak > Note for the owner: removed "healthy keto snack" (never mention diets) and "sold out in 3 days" (not in the facts). Changed the tasting end time to 2pm to match the facts. You are getting this because you signed up for our monthly email. To stop, click unsubscribe below or reply "unsubscribe". Little Oak Deli, 12 Sample Street, Exampleton, ST 00000
reference/can-spam-summary.md
# CAN-SPAM in one page (for drafting a business newsletter) A summary, not legal advice. Source opened 2026-10-06: FTC — CAN-SPAM Act: A Compliance Guide for Business — https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business The guide applies to commercial messages (ones that promote a product or service). Transactional or relationship messages (an order confirmation, an account update) are mostly exempt, but a newsletter that promotes the business is commercial. | Requirement (from the guide) | What the drafter does | |---|---| | Header information (From, To, Reply-To, routing) must be accurate and identify who sent it. | `From:` names the business as the owner wrote it in the facts file. | | The subject line must reflect the content. | No "Re:" or "Fwd:", no subject about something the body does not contain. | | The message must be identified as an advertisement (the guide allows flexibility in how). | Keep the business clearly the sender; ask the owner how they identify promotional mail. | | Include a valid physical postal address (street address, PO box or registered private mailbox). | The owner's address line goes at the foot of every issue. | | Tell recipients clearly how to opt out. | An unsubscribe line in plain words, plus the email tool's own unsubscribe link. | | Honor opt-outs within 10 business days; subscribers keep the right to opt out. | The owner removes opt-outs in their email tool; the agent never re-adds anyone. | | You stay responsible even if someone else sends for you. | The owner sends from their own tool and account. | The guide also states that each separate violating email can bring a civil penalty; the amount changes over time, so check the page rather than quoting a number. What a script cannot check: whether everyone on the list agreed to receive it, whether opt-outs were removed, and whether the subject honestly matches. A person checks those every time.
scripts/presend-check.mjs
#!/usr/bin/env node
// Pre-send check for a newsletter draft. Local files only, no network.
// Usage: node scripts/presend-check.mjs <facts.md> <newsletter.md>
// facts.md must contain lines "Sender name: ...", "Postal address: ..." and "List: ...".
// newsletter.md starts with "Subject: ..." and "From: ..." lines, then the body.
// Checks: subject present, not a fake reply/forward, under 80 characters (a readability
// choice, not a legal rule); From names the sender; the body has the postal address and
// an unsubscribe line; numbers in the body appear in the facts file.
// It cannot check: that the list holds only people who agreed, that opt-outs were
// removed, or that the subject is honest about the content. A person checks those.
import { readFileSync } from "node:fs";
const [factsFile, draftFile] = process.argv.slice(2);
if (!factsFile || !draftFile) { console.error("Usage: node scripts/presend-check.mjs <facts.md> <newsletter.md>"); process.exit(2); }
const read = (f) => { try { return readFileSync(f, "utf8"); } catch (e) { console.error(`Cannot read ${f}: ${e.message}`); process.exit(2); } };
const facts = read(factsFile);
const draft = read(draftFile);
const field = (text, name) => (text.match(new RegExp(`^${name}:\\s*(.+)$`, "mi")) || [])[1]?.trim() ?? "";
const problems = [];
const sender = field(facts, "Sender name"), address = field(facts, "Postal address"), list = field(facts, "List");
if (!sender || !address) problems.push("facts.md needs 'Sender name:' and 'Postal address:' lines; ask the owner.");
if (!list) problems.push("facts.md needs a 'List:' line saying which consented list this goes to.");
const subject = field(draft, "Subject"), from = field(draft, "From");
if (!subject) problems.push("No Subject line.");
else {
if (/^(re|fwd?):/i.test(subject)) problems.push(`Subject "${subject}" pretends to be a reply or forward.`);
if (subject.length > 80) problems.push(`Subject is ${subject.length} characters; keep it under 80 so it reads in an inbox.`);
}
if (!from || (sender && !from.toLowerCase().includes(sender.toLowerCase()))) problems.push(`From "${from}" does not name the sender "${sender}".`);
const body = draft.split(/\r?\n/).filter((l) => !/^(subject|from):/i.test(l)).join("\n");
if (address && !body.includes(address)) problems.push(`The postal address "${address}" is not in the body.`);
if (!/unsubscribe|opt out/i.test(body)) problems.push("No unsubscribe line in the body.");
const NUMBER = /\$?\d[\d,.:]*(?:%|am|pm)?/gi;
const clean = (t) => t.replace(/[.,:]+$/, "").toLowerCase();
const known = new Set([...facts.matchAll(NUMBER)].map((m) => clean(m[0])));
body.split(/\r?\n/).forEach((line, i) => {
if (/^\s*>/.test(line)) return; // notes for the owner
for (const m of line.matchAll(NUMBER)) if (!known.has(clean(m[0]))) problems.push(`Body line ${i + 1}: "${clean(m[0])}" is not in the facts file.`);
});
if (problems.length) { console.log(`NOT READY (${problems.length})\n- ${problems.join("\n- ")}`); process.exit(1); }
console.log(`READY FOR THE OWNER: subject, sender, postal address, unsubscribe line and numbers check out.\nStill for a person: the list "${list}" holds only people who agreed, and recent opt-outs are removed.`);
Use it in Claude Code
Put the folder at .claude/skills/newsletter-draft/ in your project (or ~/.claude/skills/newsletter-draft/ to use it in every project). Claude Code finds SKILL.md there and uses the skill when a request matches its description.
Other agents that read the Agent Skills format take the same folder.
Sources
- FTC: CAN-SPAM Act: A Compliance Guide for BusinessChecked 6 Oct 2026