From gtm-skills
Renders cold-outreach email sequences from fixed templates and contact CSV data using deterministic variant routing and placeholder substitution without per-row LLM calls. Useful for executing predefined campaign templates against pre-filled contact data.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gtm-skills:email-generationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Render a sequence from a **fixed template set** (`sequence.md` from `email-prompt-building`) and a contact CSV whose `{{input_fields}}` are already populated. This skill is **mechanical**: route variants, substitute placeholders, render to the target format, write the output CSV.
Render a sequence from a fixed template set (sequence.md from email-prompt-building) and a contact CSV whose {{input_fields}} are already populated. This skill is mechanical: route variants, substitute placeholders, render to the target format, write the output CSV.
There is no LLM call here. All strategic reasoning happened at design time (email-prompt-building); all per-row intelligence happened upstream in normalization + enrichment, which filled the input fields. If an email is wrong, the template or the input data is wrong — fix the source, re-render.
This skill is a renderer, not a reasoner and not a runner.
template set (sequence.md) ─┐
├──▶ route variant ─▶ substitute ─▶ render to target ──▶ emails CSV
contact CSV (fields filled) ┘
| Input | Source | Required |
|---|---|---|
| Template set | The campaign's sequence file from email-prompt-building | yes |
| Contact CSV | Input fields already filled by normalization + enrichment | yes |
The contact CSV must carry every field in the template's Input Fields contract, plus the routing key(s). If a declared field is missing for a row, apply that field's declared fallback; if the row can't be salvaged, drop it and report.
Rendering is where this pipeline actually breaks. "Substitute the placeholders" hand-waves the hard part — newlines and escaping. This contract is mandatory.
The template uses two break types and the renderer MUST preserve the distinction:
\n\n (blank line) = paragraph break\n (single) = soft line break inside a paragraph (used deliberately)| Target | Newline handling | Escaping |
|---|---|---|
review (emails.md / CSV body column) | keep \n\n and \n literal | none — but the CSV writer MUST quote the multiline cell |
html (Instantly / sequencer) | \n → <br /> (so \n\n → <br /><br />) | HTML-escape & < > in the content |
plain (plain-text channels) | keep literal | none |
customer_segment).{{fields}} into the plain-text body.Why this order: if you HTML-escape the template before substitution, a field value like Johnson & Johnson slips in unescaped and breaks the HTML. Escaping the final assembled string covers both template copy and substituted values.
import html
def render(body: str, target: str) -> str:
"""body: assembled plain-text (variant chosen, {{fields}} substituted).
target: 'review' | 'html' | 'plain'."""
if target in ("review", "plain"):
return body
if target == "html":
# escape content per line, THEN join with <br /> — never escape the tags
return "<br />".join(html.escape(line) for line in body.split("\n"))
raise ValueError(target)
\n → <br>: the email arrives as one wall of text. Every HTML sequencer needs the conversion.& < > break the HTML.&amp;. Escape exactly once, as the last step.body column shift rows/columns. Use a real CSV writer.{{ remain in any output before writing. A leftover {{company_name}} means the field wasn't passed (e.g. Touch 3 not receiving company_name).{{field}} and every routing key.For each contact, for each touch:
review, html).{{ remains.Subject lines: first touch only; follow-ups carry an empty subject (thread continuation).
email, first_name, company_name, touch, variant, subject, body, body_html, gaps. The gaps column records which fields fell back (e.g. industry=fallback).Write both into the GTM project's campaign-assets location. See references/render_emails.py for the reference renderer.
{{ across all rows\n\n and \n both survive into the HTML target as <br /><br /> / <br />& / < renders escaped (not raw, not double-escaped)gaps populated where fallbacks firedWhen the user flags a bad email:
email-prompt-building) or a data problem (→ fix normalization/enrichment so the input field is correct).Before writing the output CSV, drop any contact whose email is on the GTM project's suppression list (unsubscribes, bounces, retired addresses). The project owns where that list lives; this skill only honors it.
If no sequence.md exists for the campaign, use the email-prompt-building skill to design one. Do not improvise copy in this skill — this skill only renders.
npx claudepluginhub extruct-ai/gtm-skills --plugin gtm-skillsDesigns a cold-outreach email sequence as a fixed template set with placeholders and variant-routing rules. Useful when committing to human-approved copy at design time rather than using per-row LLM generation.
Authors and edits PostHog email templates with Liquid personalization, Unlayer design JSON, and round-trip editing over MCP. Useful when building campaign or workflow email templates.
Composes high-converting emails using copy frameworks (PAS, AIDA, BAB, FAB, 4Ps). Generates scored subject lines, responsive HTML templates with dark mode, plain-text fallback, preheaders for cold outreach, newsletters, launches.