Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f034ecd
fix(skills): ship a variant's own reference folder in its bundle
Silthus Oct 22, 2026
177e741
refactor(skills): share the email sender setup as a partial
Silthus Oct 22, 2026
8169d7e
feat(skills): add the migrate-email skill with a Resend variant
Silthus Oct 22, 2026
5e019aa
fix(skills): keep migrate-email's summary values bare and define a site
Silthus Oct 22, 2026
6e902dc
fix(skills): point migrate-email at the transactional email docs and …
Silthus Oct 31, 2026
0286e23
fix(skills): check migrate-email's API access with the real tools and…
Silthus Nov 1, 2026
41ca66a
fix(skills): detect resend over smtp, stop without sends, and record …
Silthus Nov 1, 2026
0e526b9
refactor(scripts): ship variant references through the local referenc…
Silthus Nov 1, 2026
4ad8c6e
fix(skills): let migrate-email move on when DNS waits and read templa…
Silthus Nov 1, 2026
8f77354
fix(skills): keep cc, bcc, headers, tags and attachments sites on Res…
Silthus Nov 2, 2026
5c8a34b
fix(migrate-email): ask for the PostHog template ID and sender before…
Silthus Nov 2, 2026
efa4933
fix(skills): read Dirent.path when parentPath is missing in the migra…
Silthus Nov 2, 2026
8dcf6e7
fix(migrate-email): keep a site on Resend when its idempotency key do…
Silthus Nov 2, 2026
07dbf3a
fix(migrate-email): name PostHog's idempotency key rule exactly
Silthus Nov 2, 2026
5c92245
docs(migrate-email): ask for the transactional_email:write scope
Silthus Nov 5, 2026
254dbb9
fix(migrate-email): name the transactional email scope in the preflight
Silthus Nov 5, 2026
e22fa6c
Merge branch 'main' into feat/migrate-email-resend
Silthus Nov 12, 2026
487262b
fix(skills): close the migrate-email preparation gaps the QA swarm found
Silthus Nov 12, 2026
00b9aa3
fix(skills): tighten the migrate-email key check and template rules
Silthus Nov 12, 2026
56158d1
fix(migrate-email): keep marketing sends on the provider
Silthus Nov 12, 2026
04e7c84
fix(migrate-email): read the provider's report notes without the impo…
Silthus Nov 12, 2026
a2994e9
fix(migrate-email): keep a named recipient on Resend
Silthus Nov 12, 2026
1d85155
fix(migrate-email): tie the kept Resend error names to posthog-node, …
Silthus Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ longer exist — only some keep an alias.
| `wizard integrate` | `wizard` (default flow) | command removed |
| `wizard events-audit` | `wizard audit events` | moved into `audit` family |
| `wizard audit` (single) | `wizard audit <subcommand>` | now a family — see audit subcommands below |
| `wizard migrate` (single) | `wizard migrate <vendor>` | becomes a family with the wizard's family command — see migrate subcommands below |
| `wizard audit-3000` | *removed* | retired |
| `wizard revenue` | `wizard revenue-analytics` | renamed (old `revenue` removed) |
| `wizard upload-sourcemaps` | `wizard upload-source-maps` | renamed; `upload-sourcemaps` kept as alias |

**Audit subcommands** — the one family today backed by skills from this repo:
**Audit subcommands**, backed by skills from this repo:

| Subcommand | Backing skill |
|---|---|
Expand All @@ -88,6 +89,13 @@ longer exist — only some keep an alias.
| `wizard audit session-replay` | `audit-session-replay` |
| `wizard audit web-analytics` | *(wizard-native, not a skill here)* |

**Migrate subcommands**, published as `cliEntries` with `parentCommand: migrate`. They run once the wizard runs `migrate` as a family. Until then, `wizard migrate` stays a flat command that runs `migrate-statsig`, and the wizard rejects a vendor after it:

| Subcommand | Backing skill |
|---|---|
| `wizard migrate statsig` | `migrate-statsig` |
| `wizard migrate resend` | `migrate-email-resend` |

**Commands vs. skills:** those audit subcommands **are** skills, promoted to
commands via `cli: role: command`. A `role: skill` skill is reachable only via
`wizard skill <id>`. Same machinery, two surfaces — so `wizard audit <subcommand>`
Expand Down
18 changes: 18 additions & 0 deletions context/skills/migrate-email/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Email migration skill group - move transactional email from another provider to PostHog, one variant per provider
type: skill
template: description.md
category: migrate
description: Migrate transactional email from {display_name} to PostHog. Use when asked to migrate from {display_name}, replace {display_name} with PostHog, or switch email providers. It finds every {display_name} send in the code, sets up the sender and DNS next to {display_name}'s records, imports {display_name}'s suppression list, hands over the PostHog secret key, and writes a migration report. It changes no send call site yet.
tags: [migration, workflows]
cli:
role: command
parentCommand: migrate
references:
preamble: "**Read ONLY this file.** Do not read any other reference file until this one tells you to."
shared_docs:
- https://posthog.com/docs/workflows/transactional-emails.md
- https://posthog.com/docs/workflows/transactional-emails/api.md
variants:
- id: resend
display_name: Resend
tags: [resend]
57 changes: 57 additions & 0 deletions context/skills/migrate-email/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Migrate transactional email from {display_name} to PostHog

Use this skill to move an app's transactional email, such as welcome emails, password resets, and receipts, from {display_name} to PostHog.

This version prepares the move and changes no send call site. It finds every {display_name} send, sets up the PostHog sender and its DNS records next to {display_name}'s, imports {display_name}'s suppression list, hands over the PostHog secret key, and writes a report. Both providers can send during the cutover.

The transactional email API is in beta. PostHog enables it per project.

## Workflow

{workflow}

Read the step files in order, one at a time. Each step names the next one. Do not read a step before the one before it tells you to.

## Provider files

The folder `references/<provider>/` holds everything specific to {display_name}. The steps tell you when to read each file:

- `sdk-reference.md`: how {display_name} looks in code, so you can find every send.
- `mapping.md`: how each send field maps to PostHog, and what keeps a call site on {display_name}.
- `account.md`: {display_name}'s DNS records and how to export its suppression list.

Everything you need is on disk. Do not fetch {display_name}'s docs.

## Plan file

The plan file `.posthog-email-migration-plan.md` lives at the project root. Step 3 writes it, the later steps update it, and the last step writes the report from it and deletes it.

## Rules

- Change no send call site. This version only prepares the move.
- Never touch DNS or the {display_name} account. Give the person the records and the links, and let them make the change.
- Never remove the {display_name} package, its client, or its API key.
- Never create, upload, or edit a template in PostHog.
- Never write a recipient's email address into a file in the repository, the plan, or the report. Count addresses instead.
- Keep keys in environment variables. Never hardcode a key, and never ask the person to paste one into the chat.
- Do not commit. The person reviews the diff and commits.

## Inside the PostHog Wizard

Read this section only when the Wizard's `wizard_ask` tool exists. Some runtimes name its tools `mcp__wizard-tools__wizard_ask` and `mcp__wizard-tools__set_env_values`: use the names the runtime shows. The Wizard has no chat, so the person answers through its tools.

- The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>`.
- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Step 2 says how to tell.
- Ask every question with `wizard_ask`, never in plain text. Batch the questions of one step into one call.
- Emit the `[STATUS]` line each step names when you start it, and the `[ABORT]` line a step names when it stops the run.
- Ask for a key with `wizard_ask` and `sensitive: true`, then pass the answer to `set_env_values` with the variable name the step gives. The answer is a reference that only `set_env_values` can read, so the key never shows in the run.

Outside the Wizard, skip the `[STATUS]` lines, and tell the person why you stopped instead of an `[ABORT]` line.

## Reference files

{references}

`transactional-emails.md` is the transactional email overview. It covers how to create a transactional email and its API key, idempotency, and the error codes. `api.md` covers the send route, including content from your code, and the checks of a send.

When the `sending-transactional-emails` skill is available, read it too. Its `moving-from-other-providers.md` moves any provider's sends to a PostHog template. This skill adds what is specific to {display_name}: it finds every send, sets up the sender next to {display_name}'s DNS records, imports {display_name}'s suppression list, and hands over the key.
41 changes: 41 additions & 0 deletions context/skills/migrate-email/references/1-detect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Find the email provider in the code
next_step: 2-preflight.md
---

# Step 1, find the email provider in the code

Discovery only. Do not change any file.

## Status

```
[STATUS] Looking for the email provider
```

## Procedure

Read the provider's `sdk-reference.md`. It lists the provider's packages, imports, clients, send calls, HTTP host, SMTP host, and framework mailers, and the packages that look related but are not the provider.

The provider is in this project when at least one of these is true:

- Its package is in a dependency manifest, such as `package.json`, `requirements.txt`, `pyproject.toml`, `Gemfile`, `composer.json`, or `go.mod`.
- Source code imports it or creates its client.
- Source code calls its HTTP host.
- Source code, mail config, or an example env file names its SMTP host or its framework mailer.

Search the source code, not only the manifest. Skip `node_modules`, build output, and lockfiles.

The migration needs at least one send: a send call, a request to the send route, or mail through the SMTP host or the framework mailer. A package without a send is not a migration, and neither are calls that only read contacts, domains, or an email's status.

A rendering package alone, such as `@react-email/components`, is not the provider. It only tells you how some emails are rendered.

When you find no send, stop before any change:

```
[ABORT] no email provider sends found
```

Do not widen the search to other email providers. That outcome is correct, not a failure to work around.

When you find a send, note each file that uses the provider for Step 3, and continue.
38 changes: 38 additions & 0 deletions context/skills/migrate-email/references/2-preflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Check that the project can send
next_step: 3-inventory.md
---

# Step 2, check that the project can send

Do not change any file.

## Status

```
[STATUS] Checking the PostHog project
```

## The transactional email API

Check which PostHog MCP tools exist.

- When there are no PostHog tools at all, the PostHog MCP is not connected or not signed in. Ask the person to connect it, and wait.
- When `workflows-make-email-template-transactional-prepare` and `workflows-make-email-template-transactional-execute` exist, the API is on. Record `api: on`.
- When other PostHog tools exist but that pair doesn't, either the login lacks the `transactional_email:write` scope (a login with `hog_flow:write` works too), or transactional email is not on for the project yet. A missing tool cannot tell these apart. Tell the person both causes, record `api: not confirmed`, and continue. The report asks them to confirm it before cutover.

Do not call a tool that changes anything to find out.

Those tools confirm sends by template. PostHog enables sends with content from the code separately, and no tool shows it. The report asks the person to confirm it before cutover when a site that moves sends its content from the code.

## The sending tier

PostHog gives each project a sending allowance: how many emails it sends per hour and per day. A new project starts on a low tier, and the tier grows with low bounce and complaint rates. Emails above the allowance are not dropped, they are sent later. So a cutover with more mail than the tier allows delays email, such as password resets.

- Ask the person how many emails the app sends in its busiest hour and on its busiest day.
- Ask them to compare these numbers with the "Sending allowance" in PostHog: Workflows, then Messaging, then Reputation.
- Record the answer as `tier: fits`, `tier: too low`, or `tier: not checked` when they don't know. Do not stop on `too low`: the report tells the person to move the sends in parts, or to ask PostHog about a higher tier, before cutover.

## Write down

Keep `api` and `tier` for Step 3, which writes them into the plan file.
76 changes: 76 additions & 0 deletions context/skills/migrate-email/references/3-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: List and classify every send
next_step: 4-sender.md
---

# Step 3, list and classify every send

List every place the code sends email through the provider, and decide for each one whether PostHog can send it. Change no source file. The only file you write is the plan file.

## Status

```
[STATUS] Listing every send
[STATUS] Writing the migration plan
```

## Find the sends

Read the provider's `sdk-reference.md` for the call shapes, and its `mapping.md` for the fields.

A **site** is one place in the code that decides what a send contains: its content and its fields. Usually that is the `emails.send(...)` call or the HTTP request itself.

- When a helper builds the send from a few parameters, such as a subject and a template name, the helper is one site, however many places call it. Read its callers anyway: they decide values such as how many recipients a send has.
- When a helper only forwards a payload that its callers build, each caller is a site, and the helper is not.

For each site, read the surrounding code and record:

- The file and the line of the call.
- How it sends: `sdk`, `http`, or `smtp`.
- The content: `template` for a template stored at the provider, `react` for a component the provider renders, `html`, or `text`. A site that sets both `html` and `text` is `html`.
- Every field the site sets, including fields set on the payload object after it is created and fields set through options, such as an idempotency key.
- Where `from` comes from: the literal address, or the name of the environment variable or config value that holds it.

Also list every other call to the provider that is not a send, such as reading an email's status, contacts, or domains. These calls stay on the provider. Note what each one does, because a site whose code reads the status back stays on the provider too.

When you find no site, stop before you write the plan file:

```
[ABORT] no email provider sends found
```

## Classify each site

Check what each site sends first. PostHog's transactional send ignores marketing opt-outs, so it is only for an email that a person needs, such as a password reset, a receipt, or an invite. A site that sends marketing, such as a newsletter, a promotion, or a product announcement, stays, and the reason is `marketing email`. When neither the code nor the template's name tells you, ask the person once for every such site. A site they call marketing, or don't answer for, stays with that reason.

Then go through `mapping.md` field by field. A site **moves** when PostHog can send every field it sets. A site **stays** on the provider when any one field or feature keeps it there: record that field or feature as the reason. Never plan to drop a field.

When the code passes a value you can't read, such as a list of recipients built at run time, follow it to its callers. When you still can't tell, the site stays, and the reason says what you could not check.

A `template` site moves only to a template made in PostHog, and the person makes that template. Check the site's other fields first: a site that another field keeps on the provider stays with that reason, and needs no question about its template. Ask the person once, for every other template the sites send, whether to move it to PostHog or keep it on the provider for now. A template they keep stays, and the reason is `kept on <provider name> for now`. For each template they move, ask them to copy it from the provider's dashboard into a file outside the repository, and to tell you where they saved it. Read those files to classify the site, and never write the content into the plan. When the person can't give the content, the site stays, and the reason is `template content not checked`.

A template whose content passes the checks moves only once the person has made it in PostHog. Ask them once, for every such template, to make it in PostHog from the provider's template and give you its template ID and the address it sends from, usually the provider template's sender. PostHog makes a template transactional only once a verified sender covers its address: ask them to make it transactional now when the project has one, and otherwise once Step 4 verifies the sender. When the email carries a login code, a reset link, or another secret, ask them to mark the template sensitive, as `mapping.md` says. The report lists both before cutover either way. Tell them how its placeholders change, as `mapping.md` says under "Content". Record each template ID with its site under the sites table, and write the address in the site's `from` column, so Step 4 sets up a sender for its domain. A template without an ID stays, and the reason is `PostHog template not made yet`.

## Write the plan file

Write `.posthog-email-migration-plan.md` at the project root with three sections. Never write an email address of a recipient into it. A `from` address is fine.

A phase checklist, in this order. Tick `preflight` with what Step 2 recorded, and `inventory` once the file is written. Later steps tick the rest.

```
- [x] preflight — api: <on|not confirmed> — tier: <fits|too low|not checked>
- [x] inventory — <sites found> sites, <sites that stay> stay on the provider
- [ ] sender
- [ ] suppressions
- [ ] key
```

A sites table, one row per site, in source tree order:

```
| site | file | line | sends | content | from | moves | reason |
```

`moves` is `yes` or `no`. `reason` is empty when the site moves.

A list of the other provider calls, each with its file, line, and what it does.
Loading
Loading