Skip to content
Closed
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
fa8aeb1
feat(skills): add send-transactional-email skill
Silthus Oct 17, 2026
5a1f8e1
fix(skills): tighten send-transactional-email after review
Silthus Oct 17, 2026
3d85758
fix(skills): never rerun a test send to read its output
Silthus Oct 17, 2026
711998d
fix(skills): verify an unknown sender instead of asking
Silthus Oct 17, 2026
965a656
fix(skills): route a refused verify to the person
Silthus Oct 17, 2026
5127da7
feat(skills): record a template's file on upload and change it in the…
Silthus Oct 17, 2026
99eaa9a
fix(skills): cover undo, early publish and no-repository changes
Silthus Oct 17, 2026
c128595
fix(skills): scope undo to templateFile and order the by-key upload
Silthus Oct 17, 2026
8b236c3
fix(skills): name a published revert in STEP 8 and split the fallback
Silthus Oct 17, 2026
c626ddf
feat(skills): teach the flat send contract in send-transactional-email
Silthus Oct 22, 2026
05f2f10
feat(skills): add a Wizard section to send-transactional-email
Silthus Oct 22, 2026
db4baf8
fix(skills): treat missing tools as inconclusive inside the Wizard
Silthus Oct 22, 2026
161a6fd
fix(skills): let only a send's 404 stop a Wizard run in STEP 6
Silthus Oct 22, 2026
35e917c
fix(skills): ship a variant's own reference folder in its bundle
Silthus Oct 22, 2026
bd7d535
refactor(skills): share the email sender setup as a partial
Silthus Oct 22, 2026
45ae5d5
feat(skills): add the migrate-email skill with a Resend variant
Silthus Oct 22, 2026
0a48cff
fix(skills): keep migrate-email's summary values bare and define a site
Silthus Oct 22, 2026
8ba31c3
refactor(skills): share the posthog-node client setup as a partial
Silthus Oct 22, 2026
a4916b9
feat(skills): move Resend sends to posthog-node in migrate-email
Silthus Oct 22, 2026
c3274f5
fix(skills): keep migrate-email from splitting sends or faking the ol…
Silthus Oct 22, 2026
c1383c1
fix(skills): handle suppressed recipients and dynamic subjects in mig…
Silthus Oct 22, 2026
3d51135
fix(skills): keep a suppressed copy address from passing as a sent email
Silthus Oct 22, 2026
4499957
fix(skills): warn that retrying a send with a suppressed copy address…
Silthus Oct 22, 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`. Until the wizard runs `migrate` as a family, bare `wizard migrate` keeps running `migrate-statsig`:

| 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
7 changes: 7 additions & 0 deletions context/shared/email-node-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Configure the posthog-node client for email
---

- Reuse the app's existing `posthog-node` client when it has one, and add `secretKey`. Keep `enableLocalEvaluation: false` unless the key also has `feature_flag:read` and the app uses local evaluation.
- When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell.
- When `host` is a reverse proxy or a self-hosted instance, also set `appHost` to the PostHog app URL, such as `https://us.posthog.com`. posthog-node never sends the secret key to a proxy, and without `appHost` the send fails with `missing_app_host`.
15 changes: 15 additions & 0 deletions context/shared/email-sender-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Set up an email sender
---

- First check that the PostHog MCP has the sender tools: `integrations-email-create`, `integrations-email-verify-create`, `integrations-domain-connect-check-retrieve`, and `integrations-domain-connect-apply-url-create`. When they are missing, go to the last bullet of this step.
- When the `setting-up-an-email-domain` skill is available, follow it. It covers the details.
- Ask which domain to send from. Recommend a subdomain such as `mail.example.com`. Use the person's own domain, never a free mailbox domain such as gmail.com.
- Never create a sender for an address that already has one: creating it again marks it unverified. For an existing unverified sender, skip to its records.
- Create the sender with `integrations-email-create`, then get its DNS records with `integrations-email-verify-create`.
- PostHog never writes DNS. Call `integrations-domain-connect-check-retrieve` with the domain. When `supported` is true, give the person the URL from `integrations-domain-connect-apply-url-create`: they approve the records at their DNS host. Otherwise, give them the records that are not `success` as a table with type, name, value, and priority.
- Always include both SPF records, merged into an existing `v=spf1` record when there is one. Never add a second `_dmarc` record when the domain already has one.
- Call `integrations-email-verify-create` again after the records are published, at least a minute apart. `success` means done. `failed` means fix the records, then verify again. After about 15 minutes of `pending` with every record published, stop and tell the person that DNS is still propagating and they can ask you to check again later.
- A 403 that says the person lacks permissions in the project means a project admin must verify the sender. Do not retry.
- **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue.

17 changes: 17 additions & 0 deletions context/skills/migrate-email/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 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, moves each send PostHog can take to posthog-node, and writes a migration report that lists the sends that stay.
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-email-api.md
variants:
- id: resend
display_name: Resend
tags: [resend]
55 changes: 55 additions & 0 deletions context/skills/migrate-email/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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.

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, and hands over the PostHog secret key. Then it moves every send PostHog can take to `posthog-node`, checks one email on one person, and writes a report. Sends PostHog can't take stay on {display_name} with a reason, so both providers 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, what keeps a call site on {display_name}, and how to rewrite a site that moves.
- `account.md`: {display_name}'s DNS records, and how to get its templates and 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 send code only in Step 7, and fix it only in Step 8: the sites that move, and the helpers, callers, and tests they need. Leave every site that stays as it is.
- 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 or its API key. The report tells the person when to remove them.
- Never create, upload, or edit a template in PostHog. A moved local template uploads on its first send.
- 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-email-api.md` is the transactional email API page. It covers senders, the send forms, `posthog-node`, idempotency, and every error code.
38 changes: 38 additions & 0 deletions context/skills/migrate-email/references/1-detect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
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, and HTTP host, 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.

Search the source code, not only the manifest: a package without a call site is not a migration. Skip `node_modules`, build output, and lockfiles.

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 nothing, 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 the provider, note each file that uses it for Step 3, and continue.
41 changes: 41 additions & 0 deletions context/skills/migrate-email/references/2-preflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
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 other PostHog tools exist but `workflows-upload-email-template` and `workflows-send-email` don't, the transactional email API is not enabled for this project. Stop and tell the person: the API is in beta, and they can ask PostHog in-app to enable it for their project. Then emit:

```
[ABORT] transactional email api not enabled
```

- When both tools exist, the API is on. Record `api: on`.

Inside the Wizard, a missing tool can mean that the login lacks its scope, so missing tools do not stop the run. Record `api: not confirmed` and continue. The report asks the person to confirm it.

## 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.
68 changes: 68 additions & 0 deletions context/skills/migrate-email/references/3-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
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.

## Classify each site

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 needs the template's content to be classified. Read the provider's `account.md`, and ask the person once for every template the sites send: copy from the dashboard, or read through the API. Keep the content for Step 7, and never write it into the plan. When the person can't give the content, the site stays.

## 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
- [ ] rewrite
- [ ] verify
```

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.
38 changes: 38 additions & 0 deletions context/skills/migrate-email/references/4-sender.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Set up the senders and their DNS records
next_step: 5-suppressions.md
---

# Step 4, set up the senders and their DNS records

PostHog sends from a verified sender: an address on a domain whose DNS records PostHog checked. Set up a sender for each domain the app sends from, next to the provider's records, so both providers can send during the cutover. Never change DNS yourself, and never change the provider account.

## Status

```
[STATUS] Setting up the PostHog sender
```

## List the addresses

Collect every `from` in the plan's sites table. When a `from` comes from an environment variable or a config value, read it from the example env file or the config, or ask the person for the address. A `from` can carry a name, such as `Example <hello@mail.example.com>`: the sender is the address in it.

Group the addresses by domain. Each domain needs one verified sender.

## Check for verified senders

Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address: `success` means it is verified, and `pending` or `failed` means you continue from its records below. When the sender tools are missing, or the verify answers 403, ask the person whether a sender is verified for each domain.

The first sender that verifies becomes the project's default sender. A send without `from` uses it.

## Set up each domain without a verified sender

Read the provider's `account.md` first. It lists the records the provider keeps on the domain and the label PostHog must not take. Keep every one of the provider's records: removing one stops the provider's sends before the cutover is done.

Send from the domain of the existing `from` address, so the emails keep their sender. Ask for another domain only when the person wants one.

{{> email-sender-setup}}

## Write down

Tick the `sender` phase in the plan file: `- [x] sender — <verified domains>/<domains> verified — dns_ready: <true|false>`. `dns_ready` is `true` only when every domain has a verified sender. List each domain that is not verified, with what the person still has to do.
Loading