Skip to content

feat(skills): add the migrate-email skill with a Resend variant - #1

Draft
Silthus wants to merge 4 commits into
feat/send-transactional-email-skillfrom
feat/migrate-email-resend
Draft

Silthus wants to merge 4 commits into
feat/send-transactional-email-skillfrom
feat/migrate-email-resend

Conversation

@Silthus

@Silthus Silthus commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Moved here from PostHog#435, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.

Problem

A team on Resend that wants its transactional email on PostHog has no skill for the move. Its coding agent would have to find every send, decide which ones PostHog can take, set up the sender without breaking Resend's DNS, and carry the suppression list over, all by hand.

With this skill, migrate-email-resend does the preparation and changes no send call site: it finds every Resend send, classifies each one as ready to move or staying on Resend with a reason, sets up the PostHog sender next to Resend's records, imports Resend's suppression list, hands over the secret key, checks the sending tier, and writes posthog-migrate-email-resend-report.md. The next slice moves the sites.

Important

Stacked on PostHog#433. Merge PostHog#433 first. This PR's own commits start after 161a6fd: 161a6fd...0a48cff.

Tracking: Silthus/posthog#539, slice 14 of the Resend parity spec. Contract: PostHog/posthog#120921.

Impact

Pays into Grow Workflows through Switched orgs: teams that move their transactional email from Resend to PostHog. Not measured yet: it needs the migration marker on each send (PostHog/posthog#120628) and the Wizard's migration summary. Expected move: large, up, once the cutover slice and wizard migrate resend ship. This slice alone moves no mail.

Merge Danger

Warning

🟡 Merge risk: medium · two-way door (merge-risk rubric)

  • skill-menu.json changes for the Wizard: migrate-statsig moves from command: migrate to parentCommand: migrate, command: statsig, and migrate-email-resend joins as migrate resend. Today's Wizard runs bare wizard migrate from a hardcoded skill id, so Statsig keeps working. The family command comes with the Wizard PR.
  • Two generator changes reach every bundle: a variant's references/<variant-id>/ folder now ships (only migrate has one today), and a partial takes its directive's indentation (every existing {{> }} line is unindented, so their output is unchanged).
  • Like feat(skills): add send-transactional-email skill PostHog/context-mill#433, a clean build needs transactional-email-api.md on posthog.com (PostHog/posthog.com#21306). A revert removes the skill from the next release.

Summary

A new skill group, context/skills/migrate-email/, with one variant per provider. Resend is the first. The seven steps are provider-neutral; references/resend/ holds everything specific to Resend, so a SendGrid or Postmark variant is mostly three reference files.

  1. Detect. Package, import, client, api.resend.com, or smtp.resend.com. React Email or Nodemailer alone is not Resend: [ABORT] no email provider sends found before any change.
  2. Preflight. The transactional email API is on (the upload and send MCP tools exist), and the person compares their busiest hour and day with the project's sending allowance.
  3. Inventory. Every site with its file, line, content (template, react, html, text) and fields, written to .posthog-email-migration-plan.md. mapping.md decides field by field: cc, bcc, headers, tags and attachments move within the contract's limits; scheduledAt, topicId, batch sends, SMTP, more than one to, last_event reads, and error names PostHog lacks stay on Resend with a reason.
  4. Sender and DNS. One verified sender per from domain, set up with the sender setup of send-transactional-email. Resend's records stay; PostHog's MAIL FROM label must not be Resend's send.
  5. Suppressions. Resend's hard bounces, complaints and manual entries go to PostHog's suppression list through email-suppressions-add, from the dashboard CSV or GET /suppressions. Addresses never land in a file of the repository, the plan, or the report. That tool does not exist yet (see Open below): until it does, the step records not imported and the report tells the person to add them before cutover.
  6. Key. The person creates a phs_ key with hog_flow:write for POSTHOG_PROJECT_SECRET_API_KEY. The report lists every other place the Resend key lives, such as CI and docs.
  7. Report. Ends with a summary block the Wizard parses:
from: resend
sites_found: 3
sites_rewritten: 0
sites_left: 2
dns_ready: false
suppressions_ready: false

Other changes:

  • send-transactional-email's sender setup moves into context/shared/email-sender-setup.md, included by both skills. The built send skill is unchanged byte for byte.
  • fix(skills): the generator copied only top-level reference files, so migrate-statsig shipped without references/statsig/mapping.md and sdk-reference.md that its steps tell the agent to read. Both now ship.
  • AGENTS.md lists the migrate subcommands.

Evidence

Bundle and generator tests (vitest, red then green):

  • migrate-email-skill.test.js: migrate-email-resend and migrate-statsig build with every references/... path they name present in the bundle, the API page and the Resend files ship without the send skill, and both publish as migrate subcommands. Red before the skill: 3 of 4 fail (expected undefined to be defined).
  • skill-generator-references-folder.test.js: a variant ships its own folder and no other variant's. Red before the fix: ENOENT ... references/statsig/mapping.md.
  • partial-includes.test.js: an indented directive indents every partial line. Red before: the first bullet lost its indent.
  • Full suite: 30 files, 226 tests pass. pnpm build passes with the docs page stubbed in .docs-cache (it answers 404 until PostHog/context-mill#21306).

Ranking in the MCP's SkillCatalog (its skill-catalog.ts from PostHog/posthog master over every built bundle):

Query Before (PostHog#433) After
migrate from Resend migrate-statsig (126) migrate-email-resend (1162)
replace resend with posthog feature-flags-ios (138) migrate-email-resend (990)
switch email provider omnibus-send-transactional-email (133) migrate-email-resend (1011)

Agent runs on fixture apps. Claude Code on Sonnet 5.5 with only this skill installed, headless, the PostHog MCP not connected, and the person's answers given in the prompt. Every run changed no send call site: the diff is one commented POSTHOG_PROJECT_SECRET_API_KEY line in the env example, plus the report.

Fixture Runs Result
SvelteKit app on the resend SDK (PostHog/wizard-workbench#2221) 3 2 sites, both ready to move; the one-recipient list traced through sendUserEmail. Summary parses every time
Node.js app on fetch('https://api.resend.com/emails'), no resend package 4 3 sites: the order confirmation moves with its tag and idempotency key, the digest stays (scheduled_at), the invoice stays (attachment by path). The Resend key in the deploy workflow is listed
Node.js app with React Email and Nodemailer, no Resend 2 Stops at step 1, no diff, no report

The first two REST runs found two skill gaps, fixed in 0a48cff: a run wrote reasons into a summary value, and runs disagreed whether a helper that only forwards its callers' payload is one site. The reruns agree and parse.

Happy fixture report, excerpt
| File | Line | Content | Status |
|------|------|---------|--------|
| src/lib/mailer.ts (`sendAdminEmail`) | 30 | text | Ready to move |
| src/lib/mailer.ts (`sendTemplatedEmail`) | 173 | html and text, rendered in the app with Handlebars | Ready to move |

Keep all of Resend's records until cutover is done: `resend._domainkey` (DKIM), `send` (MX and SPF), and the shared `_dmarc`. PostHog's MAIL FROM label is `feedback` by default. Do not give it `send`.

## Before cutover

1. Confirm that the transactional email API is enabled for your PostHog project. ...
4. Export Resend's suppression list and import it into PostHog.
  • Test seams: the bundle build (files a skill ships and the paths it names), the cliEntries the Wizard reads, the MCP SkillCatalog ranking, and the files an agent run leaves in a fixture.
  • Verify: pnpm exec vitest run scripts/lib/tests/migrate-email-skill.test.js → 4 passed.
  • Gaps: no run inside the Wizard (wizard migrate resend needs the Wizard PR), no run with the PostHog MCP connected, so sender creation, DNS publish, and sender verification on a deployed project are unproven. The suppression import has no tool to run against yet.

Decisions

Question Answer Who Where
Suppression list Import the provider's list before cutover 👤 Human lane brief, 2026-10-22
Providers One migrate-email group, one variant per provider; Resend parts stay in its folder 👤 Human lane brief, 2026-10-22
Import path Write the step against a new bulk tool, email-suppressions-add, and record not imported until it exists; the existing one-address route has no MCP tool 🤖 Agent this PR
Unsubscribed contacts Left out: Resend's transactional sends reach them, and PostHog's opt-outs cover marketing only 🤖 Agent this PR
Summary block Adds suppressions_ready after the spec's five keys 🤖 Agent this PR
Sending tier The person compares their volume with the Reputation page; no tool reads the allowance 🤖 Agent this PR
Statsig entry Nest it under migrate now; today's Wizard ignores the entry 🤖 Agent this PR

Open: the server half of the suppression import, a bulk route and the email-suppressions-add MCP tool in PostHog/posthog. Recommended as its own slice.

Involvement: no grilling rounds, no prototypes, one Opus 5.5 review of the strand (PostHog#435 and PostHog#436), proof captured. Its findings were on rules the cutover uses (suppressed recipients, subjects, fallback quoting, idempotency across providers), and the fixes landed in PostHog#436, where those rules apply.


🤖 Generated with Claude Code (Claude Opus 5.5)

Silthus and others added 4 commits October 22, 2026 06:26
The migrate group keeps Statsig's mapping and SDK reference under
references/statsig/, and its step files point there, but the generator
copied only the top-level reference files. Copy references/<variant-id>/
into each variant's bundle, and nothing of the other variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the sender setup of send-transactional-email's STEP 3 into
context/shared/email-sender-setup.md, so the email migration skill
includes the same text. A partial now takes the indentation of its
{{> name}} line on every line, so it nests under a step. The built
send skill is unchanged byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0 prepares a move off Resend and changes no send call site. It finds
every Resend send, classifies each one as ready to move or staying on
Resend with a reason, sets up the PostHog sender next to Resend's DNS
records, imports Resend's suppression list when the import tool exists,
hands over the project secret key, checks the sending tier, and writes
posthog-migrate-email-resend-report.md with a summary block.

Steps are provider-neutral; references/resend/ holds what is specific
to Resend. Statsig and Resend now publish as wizard migrate subcommands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixture runs wrote reasons into the summary block, which the Wizard
can't parse, and counted a forwarding helper as one site or as each
caller. The report step now shows bare values, and a site is the place
that decides a send's content and fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant