Skip to content

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

Closed
Silthus wants to merge 9 commits into
mainfrom
feat/migrate-email-resend
Closed

Silthus wants to merge 9 commits into
mainfrom
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.

This revision

Restacked on main on 2026-10-31. What changed:

  • The base is main. feat(skills): add send-transactional-email skill #3 (the send skill) is closed, because feat(messaging): add a skill that offers and sets up transactional emails PostHog/posthog#125415 adds sending-transactional-emails with references/moving-from-other-providers.md.
  • 6e902dc bundles the posthog.com transactional email pages (transactional-emails.md and transactional-emails/api.md) and names the sending-transactional-emails skill for the general move.
  • 0286e23 (review fixes): the preflight looks for the workflows-make-email-template-transactional-prepare and -execute tools. When they are missing, it names both causes (a missing hog_flow:write scope, or transactional email off for the project), records api: not confirmed, and continues; it never probes with a mutating tool. The [ABORT] transactional email api not enabled line is gone. The mapping keeps a replyTo with a display name, more than 10 entries, or a value built at run time on Resend, and lists the reserved DKIM-Signature and Received headers and the header and tag limits.
  • 41ca66a (QA swarm round 3): step 1 also admits Resend's SMTP host and a framework mailer and needs at least one send; step 3 stops when it finds no site. A suppression import that stops partway stays unticked and lands in "Before cutover". A template site needs its content from the person, or stays with template content not checked. The sender setup is inlined into step 4, which keeps the existing from domain and records an unverified domain instead of waiting.
  • 0e526b9 (QA swarm round 3): the selected variant's references/<variant-id>/ files go through the same loop as the root reference files, so the separate copy helper is gone, and the partial expander drops the indentation it gained for the sender partial. A shadow build of every bundle shows no output change outside the rewritten step files.
  • 4ad8c6e (QA swarm fixes round): step 4 records a domain as not verified, with its records or Domain Connect link, when the person can't publish them during the run, and step 3 asks the person to save each template from the dashboard to a file outside the repository and reads it.
  • What this skill adds beyond moving-from-other-providers.md. That reference maps any provider's sends to a PostHog template. This skill adds the Resend-specific preparation: send detection for the SDK, REST, and SMTP; the sender and DNS setup next to Resend's records; the suppression list import; the key handover; the sending-tier check; and a report the Wizard parses.

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

Based on main now. The send skill it used to stack on (#3) is closed, because PostHog/posthog#125415 adds the sending-transactional-emails skill. This skill names that skill for the general move and keeps only the Resend-specific preparation.
A clean build needs the transactional email pages of PostHog/posthog.com#21564 on posthog.com (transactional-emails.md and transactional-emails/api.md). Until they publish, pnpm build stops on a 404 for those shared_docs.

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 registers wizard migrate natively with the hardcoded skill id migrate-statsig and never reads these entries, so Statsig keeps working; wizard migrate statsig and wizard migrate resend run once the Wizard turns migrate into a family.
  • One generator change reaches every bundle: the selected variant's references/<variant-id>/ folder now ships through the reference loop (only migrate has one today). A shadow build of all 446 bundles without example projects is byte-identical outside this skill.
  • A clean build needs the transactional email pages of PostHog/posthog.com#21564 on posthog.com. 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, smtp.resend.com in code or mail config, or a framework's resend mailer, with at least one send. React Email or Nodemailer alone, or a Resend client used only for contacts or domains, is not a migration: [ABORT] no email provider sends found before any change. Step 3 stops the same way when it finds no site.
  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. A template site needs a copy of the template from the person, or stays with template content not checked. 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, on the domain the app already sends from. Resend's records stay; PostHog's MAIL FROM label must not be Resend's send. Pending DNS, a 403, or missing sender tools record the domain as not verified, and the run goes on.
  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. The phase is ticked only when every entry is imported; a partial import keeps its count and lands in "Before cutover". 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:

  • description.md names the sending-transactional-emails skill (feat(messaging): add a skill that offers and sets up transactional emails PostHog/posthog#125415) for a general move off any provider, and mapping.md points at transactional-emails/api.md.
  • 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, through the same loop as the root files.
  • AGENTS.md lists the migrate subcommands and says they run once the Wizard runs migrate as a family.

Evidence

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

  • migrate-email-skill.test.js: migrate-email-resend builds with every references/... path it names present in the bundle, the API page and the Resend files ship without the send skill, and Resend and Statsig publish as migrate subcommands. Red before the skill: 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. Green before and after the 0e526b9 refactor.
  • Lane gate on 4ad8c6e: vitest run scripts/plugins/tests scripts/lib/tests, 29 files, 224 tests pass. Fork CI runs no workflows on pull requests.
  • Bundle shadow diff (0286e23 against 0e526b9): every skill without example projects, 446 bundles, built with generateSkill and stubbed docs on both commits. Only the six migrate-email-resend step files whose text 41ca66a changed differ; migrate-statsig with its statsig/ folder and every other bundle are byte-identical. Saved in proof storage. pnpm build stops on the 404 for the posthog.com pages until docs(workflows): add the transactional email API page PostHog/posthog.com#21564 publishes them. The earlier revision built with the page stubbed in .docs-cache.

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

Query Before (on the old send skill, earlier revision) 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 passes.
  • Gaps:
    • Live run: no agent rerun on a fixture app after 41ca66a. 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.
    • Prod shape: the skill reads the person's own code, mail config, and Resend exports, which PostHog does not store, so there is nothing to measure. No fixture app sends only over SMTP or through a framework mailer yet.
    • Cross-version: not run against a built manifest, because pnpm build needs docs(workflows): add the transactional email API page PostHog/posthog.com#21564's pages. From the Wizard's main source: wizard migrate keeps running Statsig, wizard migrate statsig|resend fails with an unknown-argument error, and wizard skill list prints the subcommand paths before they run.

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
Base after the send skill moved upstream Rebase onto main without #3, and name sending-transactional-emails instead 🤖 Agent Silthus/posthog#461
migrated_from The Wizard sets it on its existing setup wizard finished event; no new event 🤖 Agent Silthus/posthog#461
Transactional tools missing in preflight Record api: not confirmed and continue, instead of aborting; never probe with a mutating tool 🤖 Agent review round 1
replyTo the API can't take Keep that site on Resend: a display name, more than 10 entries, or a value built at run time 🤖 Agent review round 1
Statsig as the migrate default leaf No default: true: it would hide resend from the family picker and not save CI scripts, which need a subcommand either way; the release notes belong to the Wizard PR that makes migrate a family 🤖 Agent pending approval
Sender setup Inlined into step 4 and the shared partial deleted: one includer, and its "pick a subdomain" and "wait until verified" rules contradicted step 4 🤖 Agent pending approval
Variant reference files Ship through the existing reference loop instead of a second copy helper 🤖 Agent pending approval

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.

Open: accept the cross-version gap (prod-shape is accepted by the standing rule until 2026-11-02). Recommended: accept; the check runs once the posthog.com pages publish and the manifest builds.

Follow-up: the Wizard PR that turns migrate into a family names the bare wizard migrate change in its release notes (CONTRIBUTING.md, flat to family, step 2).

Involvement: no grilling rounds, no prototypes, one Opus 5.5 review of the strand (PostHog#435 and PostHog#436), and one GPT-6.1 Sol (high) round on this revision (two findings, both fixed) with a clean read of the fixes. QA swarm 2026-11-01: round 3, the first two-family panel (Opus 5.5 fresh eyes, GPT-6.1 Sol on nine lenses), round 4 on the fixes (six lenses), and one clean final read: 12 findings, 11 fixed, 1 rejected (Statsig default leaf), 1 follow-up. 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

Silthus and others added 5 commits October 31, 2026 09:51
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…the sending skill

The send-transactional-email skill is gone from this stack, so migrate-email
bundles the transactional email pages and names the sending-transactional-emails
skill for the general move.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus
Silthus force-pushed the feat/migrate-email-resend branch from 0a48cff to 6e902dc Compare October 31, 2026 09:53
@Silthus
Silthus changed the base branch from feat/send-transactional-email-skill to main October 31, 2026 09:53
Silthus and others added 4 commits November 1, 2026 13:02
… PostHog's reply-to rules

The preflight looked for two MCP tools that don't exist. It now looks
for the make-transactional pair, and a missing pair records the API as
not confirmed instead of stopping, because it can also mean a missing
scope. The mapping now keeps a site on Resend for reply-to addresses
with a name or more than 10, and lists every reserved header and the
header and tag length limits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…partial imports in migrate-email

Step 1 now admits the SMTP host and a framework mailer and needs at least one
send; step 3 stops when it finds no site. A suppression import that stops
partway stays unticked and lands in the report's "Before cutover" list. A
template site needs its content from the person or stays on Resend. The
sender setup is inlined into step 4, which keeps the existing `from` domain and
records an unverified domain instead of waiting. AGENTS.md says the migrate
subcommands run once the wizard runs migrate as a family.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e loop

The selected variant's `references/<variant-id>/` files now go through the same
loop as the root reference files, so the separate copy helper is gone. The
partial expander drops the indentation it gained for the sender partial, which
nothing includes any more, and the bundle test drops the migrate-statsig case
that guarded nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tes from files

Step 4 records a domain as not verified, with its records or Domain Connect
link, when the person can't publish them during the run. Step 3 asks the person
to save each template from the dashboard to a file outside the repository and
reads it, since the Wizard can't take pasted HTML.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Moved to PostHog#450.

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