# HTML Email Templates

> Three reusable HTML email templates for automated systems, built to survive the email clients that strip your CSS — a plain inline-styled notification/alert, a dark banner report wrapper, and an inline light-card digest — plus the subject-line, label and shared-footer conventions that keep a fleet of automated emails consistent and filterable. Transport-agnostic: the templates emit HTML and plain text, so any transactional email API (Resend, Postmark, SendGrid, SES) or SMTP relay can send them. Use whenever building or sending an automated email, alert, notification, digest, status report, or a generated report that gets emailed or attached.

- Skill: `ai-automation-tools/html-email-templates` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ai-automation-tools/html-email-templates`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ai-automation-tools/html-email-templates/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: ai-automation-tools (https://skillmd.com/u/ai-automation-tools)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ai-automation-tools/html-email-templates

---


# Email Template

Three templates, used together or alone. Pick based on what the email *is* and which system generates it:

| Email is... | Use | Delivery |
|---|---|---|
| A notification, alert, or short status update | **A — Notification** | inline HTML, the email body itself |
| A generated report (analysis, briefing, summary) from a script or job | **B — Report** | standalone `.html` file, attach it or link it — don't paste it as the email body |
| A markdown digest generated by a workflow tool (weekly/daily briefings, newsletters) | **C — Inline digest** | rendered to light-card HTML and inlined directly as the body (safe — inline styles only, no CSS vars) |

A report email is usually **both A and B (or C)**: template A's footer is appended to every send regardless of which body template produced the content — that's a shared footer helper's job, not a per-template choice. For script/job artifacts, template B stays an attachment/link per the note below. For workflow-tool digests, the fleet can inline template C's *full* HTML as the body **and** still attach the archived `.html` file — both, not either/or.

> **Why not inline Template B into the email body?** Template B relies on CSS custom properties (`var(--x)`), `background-clip: text`, and `backdrop-filter` — Gmail and Outlook strip or mangle all three. It renders correctly in a browser or PDF, not in an email client's DOM. Keep it as an attachment or a hosted link. Template C avoids this problem entirely by using only inline `style="..."` attributes — that's why it's safe to inline.

---

## Template A — Notification / alert

Plain inline styles only, no `<style>` block, no CSS variables — this is what survives every email client.

```html
<p style="margin:0 0 14px;font-size:15px;line-height:1.6;color:#1a1a1a;font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif">
  {body_paragraph_1}
</p>
<p style="margin:0 0 14px;font-size:15px;line-height:1.6;color:#1a1a1a;font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif">
  {body_paragraph_2}
</p>

<!-- optional: one call-to-action link, omit the block if there is none -->
<p style="margin:20px 0 0;font-size:14px;line-height:1.5">
  <a href="{cta_url}" style="color:#0891b2;font-weight:600;text-decoration:none">{cta_label} &rarr;</a>
</p>

<hr style="margin-top:24px;border:none;border-top:1px solid #ddd">
<p style="color:#888;font-size:12px;margin-top:8px">
  <strong>{sender_display_name}</strong> &middot; automation fleet &middot;
  <a href="{home_url}" style="color:#888">Browse more &rarr;</a>
</p>
```

Rules that make this the house style, not just *a* style:

- **Paragraphs, not one wrapped `<p>`.** Multi-line text loses its breaks in a single tag — split on blank lines into separate `<p>`, single newlines become `<br>`.
- **One footer, every send.** `<strong>{sender_display_name}</strong> · automation fleet · <a>Browse more →</a>` — the bold name is the literal string a Gmail filter can match on, so pick one identity name per automation and never change it later.
- **`#0891b2` for links, `#888` for footer text.** Keep both — they're the only two non-body colors in the template.
- **Subject line:** `{Report/Alert Name} — {date or week range}`, e.g. `Weekly Security Briefing — Week of 2026-08-23 to 2026-08-29`, or `{Job Name} failed — {date}` for alerts. Tag every automation send with a label matching the job slug, if your mail provider supports labels.
- **Sender identity:** one `From` display name per automation family (e.g. `My Automation Fleet <alerts@example.com>`), reused across every email that family sends — don't invent a new sender per report.
- **Footer is provider/template-agnostic.** A shared footer helper (whatever sends your mail) should append this block after the body regardless of which template (A, B, or C) produced the content above it.

## Template C — Inline digest

For a workflow tool (n8n, Zapier, a scheduled script, etc.) that turns a fixed-structure Markdown briefing (`# title` / `**Week of:**` / `## sections` / `### per-story headings` / `- bullets` / `---` rules) into an inlined HTML email. One conversion function (`mdToHtml`) can be reused across every digest workflow, each supplying its own `accentColor`.

```html
<!DOCTYPE html><html><body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;max-width:720px;margin:0 auto;padding:24px;background:#fafaf9;color:#1f2937;">
<div style="background:#ffffff;padding:32px;border-radius:12px;border:1px solid #e5e7eb;">
  {body_html — h1/h2/h3/ul/p produced by mdToHtml(markdown)}
  <div style="margin-top:32px;padding-top:16px;border-top:1px solid #e5e7eb;color:#9ca3af;font-size:12px;">Generated by {tool} &middot; {date} &middot; Items considered: {n}</div>
</div>
</body></html>
```

`mdToHtml` splits the Markdown on blank lines (`\n{2,}`) into blocks and converts each block by its **first line only**: `# `→`<h1>`, `## `→`<h2 style="color:{accentColor}...">`, `### `→`<h3>`, a block whose first line is `- `→`<ul><li>` (splits every `- ` line in the block into an `<li>`), anything else→`<p>` with `\n`→`<br>`. Links `[text](url)` and `**bold**` are regex-replaced after block conversion.

**Two known bugs in this converter shape — worth fixing at the source, not just documenting:**

- **Per-story detail lines don't listify.** If a prompt template puts `### {headline}` and its `- **Source:** ... / - **Link:** ... / - **What happened:** ...` bullets in the *same* block (no blank line between them), the block is classified by its first line and matches the `### ` branch — `.replace(/^### (.+)$/m, '<h3>...</h3>')` only replaces that first line, so the `- ` bullets underneath fall through unconverted: raw text with literal `- ` prefixes and no `<br>` between them (the `<br>` conversion only happens in the `else`/plain-paragraph branch). Fix: after the heading match, split the block's remaining lines and run them through the same `- ` → `<li>` logic, or handle "heading + bullets" as its own block type.
- **`---` rules render as literal text.** A lone `---` block doesn't match `# `, `## `, `### `, or `/^- /` (no space after the dashes), so it falls into the plain-`<p>` branch and renders as `<p>---</p>` instead of an `<hr>`. Fix: add a check for `/^-{3,}$/` before the final `else` and emit `<hr style="border:none;border-top:1px solid #e5e7eb;margin:20px 0;">`.

Delivery can be two-stage rather than a single send: the workflow tool posts the inline HTML to your mail provider's API and separately archives the `.html` somewhere durable (a repo, a bucket); a downstream sender can then re-send the archived artifact as the actual email, with `--html` set to the same generated body and `--file` set to the archived copy as an attachment — appending Template A's footer at send time. That gets you **both** an inlined report body **and** an attached `.html` copy, not either/or.

## Template B — Generated report

Full standalone HTML document: dark card on a soft radial-gradient page, markdown-shaped content styling. Same skeleton for every report — the CSS never changes, only the four banner strings and the body content.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{Report Title}</title>
<style>

:root {
  --bg-page: #07090f;
  --bg-wrapper: #0e1320;
  --bg-elevated: #131a2c;
  --bg-elevated-2: #182135;
  --bg-deep: #050810;
  --border: rgba(255,255,255,0.06);
  --border-strong: rgba(255,255,255,0.10);
  --text-primary: #e6edf7;
  --text-secondary: #b3bdd1;
  --text-muted: #7d899f;
  --accent: #7dd3fc;
  --accent-soft: #38bdf8;
  --accent-warm: #f5c97a;
  --gradient-banner: linear-gradient(135deg, #0a0f1f 0%, #14224a 40%, #1f3870 75%, #2a4f9c 100%);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
  background:
    radial-gradient(1200px 600px at 15% -10%, rgba(56,189,248,0.07), transparent 60%),
    radial-gradient(900px 500px at 110% 10%, rgba(125,211,252,0.05), transparent 60%),
    var(--bg-page);
  color: var(--text-primary);
  line-height: 1.6;
  font-size: 16px;
  padding: 28px 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.wrapper {
  max-width: 880px;
  margin: 0 auto;
  background: var(--bg-wrapper);
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.04) inset,
    0 30px 80px rgba(0,0,0,0.55),
    0 8px 24px rgba(0,0,0,0.35);
}
.banner {
  position: relative;
  background: var(--gradient-banner);
  color: var(--text-primary);
  padding: 44px 40px 32px;
  border-bottom: 1px solid var(--border-strong);
  overflow: hidden;
}
.banner::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(600px 200px at 50% 0%, rgba(125,211,252,0.18), transparent 70%),
    radial-gradient(400px 150px at 85% 100%, rgba(245,201,122,0.07), transparent 70%);
  pointer-events: none;
}
.banner > * { position: relative; z-index: 1; }
.banner-eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 2px;
  color: var(--accent);
  text-transform: uppercase;
  margin-bottom: 10px;
}
.banner-title {
  font-size: 28px;
  font-weight: 800;
  line-height: 1.22;
  margin-bottom: 8px;
  letter-spacing: -0.4px;
  background: linear-gradient(180deg, #ffffff 0%, #cfe1ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.banner-subtitle {
  font-size: 14px;
  color: var(--text-secondary);
  font-weight: 400;
}
.banner-date {
  display: inline-block;
  margin-top: 16px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 20px;
  padding: 5px 14px;
  font-size: 12px;
  color: var(--text-secondary);
  backdrop-filter: blur(6px);
}
.content {
  padding: 32px 40px 26px;
}
.content h2 {
  color: var(--text-primary);
  font-size: 20px;
  font-weight: 700;
  margin: 30px 0 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  letter-spacing: -0.2px;
}
.content h2:first-child { margin-top: 0; }
.content h3 {
  color: var(--accent);
  font-size: 15px;
  font-weight: 600;
  margin: 20px 0 8px;
  letter-spacing: 0.1px;
}
.content p { margin: 10px 0; color: var(--text-secondary); }
.content ul, .content ol { margin: 10px 0 10px 24px; color: var(--text-secondary); }
.content li { margin: 5px 0; }
.content li::marker { color: var(--accent); }
.content a {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid rgba(125,211,252,0.3);
  transition: color 0.15s, border-color 0.15s;
}
.content a:hover { color: #bae6fd; border-bottom-color: var(--accent); }
.content code {
  background: var(--bg-deep);
  border: 1px solid var(--border);
  padding: 1px 6px;
  border-radius: 4px;
  font-family: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em;
  color: var(--accent-warm);
}
.content pre {
  background: var(--bg-deep);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  padding: 14px 18px;
  border-radius: 8px;
  overflow-x: auto;
  margin: 14px 0;
  font-size: 13px;
}
.content pre code { background: none; border: none; padding: 0; color: inherit; }
.content blockquote {
  border-left: 3px solid var(--accent);
  background: var(--bg-elevated);
  padding: 10px 18px;
  margin: 14px 0;
  color: var(--text-secondary);
  font-style: italic;
  border-radius: 0 6px 6px 0;
}
.content hr { border: none; border-top: 1px solid var(--border); margin: 22px 0; }
.content table {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  margin: 16px 0;
  font-size: 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.content table th {
  background: linear-gradient(180deg, #1c2540 0%, #131a2c 100%);
  color: var(--text-primary);
  text-align: left;
  padding: 10px 12px;
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  border-bottom: 1px solid var(--border-strong);
}
.content table td { padding: 9px 12px; border-bottom: 1px solid var(--border); color: var(--text-secondary); }
.content table tr:last-child td { border-bottom: none; }
.content table tr:nth-child(even) td { background: rgba(255,255,255,0.015); }
.content table tr:hover td { background: rgba(125,211,252,0.04); }
.content strong { color: var(--text-primary); font-weight: 600; }
.content em { color: var(--text-secondary); }
.content img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 20px auto;
  background: #ffffff;
  padding: 12px;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.45);
}
.footer {
  background: var(--bg-deep);
  border-top: 1px solid var(--border);
  padding: 18px 40px;
  font-size: 11px;
  color: var(--text-muted);
  text-align: center;
  letter-spacing: 0.5px;
}
@media (max-width: 640px) {
  body { padding: 0; }
  .wrapper { border-radius: 0; box-shadow: none; border-left: none; border-right: none; }
  .banner, .content, .footer { padding-left: 20px; padding-right: 20px; }
  .banner-title { font-size: 24px; }
  .content table { font-size: 12px; }
}

</style>
</head>
<body>
  <div class="wrapper">
    <header class="banner">
      <div class="banner-eyebrow">{Eyebrow — e.g. the generating agent/product name}</div>
      <div class="banner-title">{Report Title}</div>
      <div class="banner-subtitle">{One-line subtitle}</div>
      <div class="banner-date">{Date or date range}</div>
    </header>
    <main class="content">
{report_body_html — h2/h3/p/ul/ol/table/blockquote/hr/img, exactly what a markdown-to-HTML render of the report produces}
    </main>
    <footer class="footer">{Footer disclaimer — e.g. "For Educational Purposes Only"}</footer>
  </div>
</body>
</html>
```

Rules:

- **Only four things change per report:** `<title>`, `banner-eyebrow`, `banner-title`, `banner-subtitle`, `banner-date`, and the `.content` body. The `<style>` block is copy-pasted verbatim — don't redesign it per report.
- **`.content` is markdown output, not custom markup.** Render the report body as plain `h2`/`h3`/`p`/`ul`/`ol`/`table`/`blockquote`/`hr`/`img` — the CSS already styles all of them. Don't hand-build stat cards or custom divs inside `.content`; the template doesn't have classes for them.
- **`.banner-eyebrow`** is the product/agent name generating the report; swap it for the automation's own name.
- **Footer is a one-line disclaimer or attribution**, not the fleet footer from Template A — this document stands alone (opened directly, attached, or hosted), it doesn't need the "Browse more →" link.

---

## Putting them together

**Script/job reports (Template B):**
1. Generate the report as its own `.html` file using **Template B**, named for what it is (e.g. `Weekly_Security_Briefing.html`), saved next to the automation's other outputs.
2. Send the notification email using **Template A** as the body — one or two sentences on what's in the report, a CTA link if the report is hosted somewhere, or send the report file as an email attachment.
3. Keep the subject line and label conventions from Template A even when the body is otherwise custom — that's what makes an inbox filter and a fleet-wide tag keep working.

**Workflow-tool digests (Template C):** the agent/job emits Markdown → a shared render step turns it into Template C's light-card HTML → that HTML is optionally archived somewhere durable → the sender delivers it as **both** the inline HTML body and a `.html` attachment, picking up Template A's footer automatically via the shared footer helper. Don't build a separate short Template A body for these — inline the full digest, unlike the Template B report flow.

Regardless of which body template: subject line and label conventions come from Template A, and the footer is always appended by a shared helper — it's not something you hand-write per template.

---

## Sending it

These templates only produce HTML and text — something still has to call an email API. The
templates are deliberately transport-agnostic, so any of these works:

| Mechanism | Use when | Notes |
|---|---|---|
| **A transactional email API** (Resend, Postmark, SendGrid, SES…) | Any new automation email | The normal choice. All of them take an HTML body, a plain-text alternative, attachments, and a tag/label for filtering — which is exactly what these templates emit. Verify a sending domain first, or your mail lands in spam. |
| **A mail MCP server** | Searching, labeling, or filtering existing mail | Most read-only mail integrations can't send. Check before you wire one in as a delivery path. |
| **Raw SMTP** | A one-off script, or a host that already has a relay | Fine for low volume. You give up delivery tracking, bounce handling, and per-message tagging. |

> [!IMPORTANT]
> **Write one sender and reuse it.** The single biggest source of drift in an automation fleet
> is every job growing its own send function — each with slightly different footer handling,
> retry behavior, and API-key resolution. Put one small module behind the templates:
> it takes `(subject, html, text, attachments, tag)`, appends the shared footer, and is the
> only thing in the codebase that holds the API key.

**Key resolution is the trap.** A sender that works interactively fails under a scheduler,
because the scheduled task runs as a different user and never sees your shell environment.
Decide explicitly where the key comes from — process env, a secrets file, the scheduler's own
secret store — and document it next to the sender, not in your head.

