Repository navigation
Conversation
Teaches a coding agent to send one transactional email from server code with PostHog: verify a sender through the PostHog MCP first, keep the template as an HTML file in the repository, upload it by key, and send it with posthog-node or the HTTP API. Ships the transactional email API docs page as its reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check the PostHog MCP before any sender work, never recreate an existing sender, bound verification polling, set the default sender through a verify, ask before any test send, and cover Next.js and edge runtimes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… file The MCP upload passes source.repository from the origin remote without credentials, source.path from the repository root and source.ref from HEAD. A new section sends a change to a code-managed template to its file and leaves the upload to posthog-node or the deploy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reverted file goes live only through an upload, because a send with an older published hash gets the newer content. Any send from the changed code publishes it. A by-key change is uploaded after the change. An agent without the repository tells the person or requests the change. The SDK always sends a path, so only repository and ref keep their stored value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A revert to published content needs an upload only for templateFile(). A by-key change waits for the deploy step when there is one. A test through workflows-send-email publishes nothing, and an agent without the repository shares the PostHog Code task link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill now writes a local template in the app's code and sends it with templateKey, subject, html and templateData. The first send uploads it, so the MCP upload, templateFile() and the deploy-time upload by key go. Rendered content and templates made in PostHog (templateId) are the other two forms. Sends carry an idempotency key and retry with it. Job runners pass maxRetries: 0 and a key derived from the email's purpose. STEP 9 lists the contract's codes, including the 409 for a template edited in PostHog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside the Wizard the agent asks through wizard_ask, writes the secret key with set_env_values, reaches PostHog tools through posthog_exec, and ends a blocked run with an [ABORT] reason the Wizard's transactional-email program matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Wizard's login may lack the workflows and integrations scopes, so a hidden tool there no longer stops the run as "API not enabled". The Wizard section names its tools as the runtime shows them, and replyTo no longer needs a verified sender. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migrate-email skill now rewrites every call site PostHog can send: hosted Resend templates become local templates, React and html/text sites send rendered content, and the client carries migratedFrom. It checks the build and one email on one person before it writes the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d provider in tests Fixture runs looped over or truncated a one-recipient list, and one run mocked posthog-node to rebuild a Resend request. The rewrite now narrows the helper's parameter to one address, and the verify step asserts on the fields posthog.emails.send gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rate-email A moved site now treats recipient_suppressed like a send that went out, a site-built subject becomes a module placeholder so the template hash stays stable, fallbacks use single quotes, and the report warns that idempotency keys don't cross providers. The rules and Step 7 no longer contradict the mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… uses sending cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
Author
|
Note 🤖 Claude Opus 5.5 writing on behalf of Michael Decisions made while building and reviewing this PR:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
migrate-email-resend(#435) finds every Resend send and prepares the account, but it leaves every call site on Resend. The person still rewrites each send by hand.With this PR the skill does the cutover in code. It moves each send PostHog can take to
posthog.emails.sendfromposthog-node, leaves the rest on Resend with a reason, checks the build and one email on one person, and writes the report with the realsites_rewritten.Important
Stacked on #435, which is stacked on #433. Merge them first. This PR's own commits start after
0a48cff:0a48cff...4499957.It also waits for the
posthog-noderelease withposthog.emails(PostHog/posthog-js#5312): the skill installs a version that has it.Tracking: Silthus/posthog#539, slice 15 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. This is the slice that moves mail: every moved send carries
migratedFrom: 'resend', which send attribution (PostHog/posthog#120628) records asmigrated_from. Expected move: large, up, oncewizard migrate resendships and the skill is released.Merge Danger
Warning
🟡 Merge risk: medium · two-way door (merge-risk rubric)
Summary
Three steps replace the old step 7:
posthog-nodeclient withsecretKeyandmigratedFrom, set up as the send skill says (its client bullets are now the shared partialemail-node-client). Then each site whose plan row says it moves:{{{NAME}}}turns into{{ variables.NAME }}, and a fallback turns into Liquid'sdefault. The call site sendstemplateKey,subject,htmlandtemplateDatawith the oldvariables. A template with an HTML-valued variable, or anything beyond plain variables and fallbacks, stays on Resend.@react-email/render, andhtml/textsites send their content as is. Each gets atemplateKeynamed for the kind of email.from,replyTo,cc,bcc,headers,tags,attachmentsand the idempotency key move unchanged.error.namebranches keep working.distinctIdis the ID the app already identifies people with.{{ variables.* }}placeholders in the module, so the template hash stays stable and doesn't upload a new version on every send.recipient_suppressed), where Resend dropped it later. A site that throws or retries on errors gets a branch that treats it like a sent email, unless it setsccorbcc, where the report explains that a suppressed copy address now stops the email.$workflows_email_sent, an engagement event and a product event on the same distinct ID, read through the MCP'sexecute-sql.sites_rewrittenis now real.mapping.mdgains a Rewrite section,account.mdcovers getting a template's content (dashboard copy orGET /templates/{id}with a full-access key), andsdk-reference.mdadds template fallbacks and defaults.Evidence
Bundle test (vitest, red then green):
runs the rewrite and verify steps between the key and the reportexpects the workflow1-detect…6-key,7-rewrite,8-verify,9-report. Red before:7-report.mdwhere the three new steps belong. Full suite: 30 files, 227 tests pass. The builtomnibus-send-transactional-emailis byte-identical before and after the partial (diff -r).Agent runs on fixture apps. Claude Code on Sonnet 5.5 with only this skill installed, headless, with no PostHog MCP and the person's answers in the prompt.
posthog-nodecame from a local build of the release branch, because the release isn't out yet.resendSDK (PostHog/wizard-workbench#2221)from 'resend'left, 2posthog.emails.sendfor 2 old sends.svelte-check0 errors, mailer tests 3 passed. The first two runs looped or truncated a one-recipient list; after the fix both narrow it to one addresswelcome(a plain variable and a fallback) moved,order-receipt(HTML rows in a variable) stayed with that reason.tscpasses. Every report notes that an empty name now shows the fallbackpathattachment stayed.icsattachment (Silthus/next-session)4499957maxRetries: 0because the app's retrier retries. Convextscpasses, 665 tests pass. Before the fix, one run kept the tests green by mocking posthog-node to rebuild a Resend request; the rerun fakesposthog.emails.sendand asserts on its options. The run on the reviewed head also returns early onrecipient_suppressed(the site sets no cc) and lists the cross-provider duplicate in "Before cutover"Templates render as before. Each moved
welcomemodule, rendered with liquidjs (HTML escaping on) and compared with Resend's triple-brace output:"After entity decode" is
&in the link'shrefwritten as&, which browsers read back as&.On the wire. The migrated template app's
sendWelcomeagainst a local stub of the PostHog API:pnpm exec vitest run scripts/lib/tests/migrate-email-skill.test.js→ 5 passed.migrated_fromproperty on the send event are unproven. The send API slices are still open PRs.Decisions
GET /templates/{id}with a full-access key the person creates and deletes afterwards; the same key now also reads suppressions (RESEND_FULL_ACCESS_API_KEY)execute-sqlrecipient_suppressedlike a sent email, except on sites withcc/bcc, which keep failing with a report notesubjectandtextwithout HTML escaping (EMAIL_PLAIN_TEXT_FIELDSinnodejs/src/cdp/services/hog-inputs.service.ts)sites_rewrittencounts moved sitesInvolvement: no grilling rounds, no prototypes, one Opus 5.5 review of the strand (#435 and #436) with three final reads, ending clean. Proof captured.
🤖 Generated with Claude Code (Claude Opus 5.5)