Repository navigation
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
… uses sending cap Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e-email posthog-node has no migratedFrom option. The Wizard records the migration on its setup wizard finished event instead. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-email rewrites a site A posthog-node release with posthog.emails can still lack templateKey, local template content, or the extra send fields. The rewrite now checks the installed types for what each moving site needs, and keeps a site on Resend when the latest release lacks it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rt every behaviour change Step 8 checks that the app sends to the MCP's project, notes the start of the test, and counts only events since then whose workflow matches the send. It compares typecheck errors with a baseline step 7 takes before it changes code. A template's content waits in a temporary file for step 7 instead of in memory, an HTML fallback keeps a template on Resend, and a derived templateKey is normalized and unique. The report lists each suppressed-recipient branch, copy-address refusal, and cross-provider idempotency risk, and reminds the person to delete the full-access Resend key. The posthog-node client setup is inlined into step 7, and description.md says this skill wins over the general moving guide. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
… keep template files traceable Step 8 takes the start of the test from `execute-sql` and compares Unix timestamps, so the project's timezone can't shift the window. The plan lists each saved template file, step 7 reads the module from it, and step 9 deletes every listed file. A derived templateKey falls back to the template id when the name normalizes to nothing and stays unique across the run. Step 8 may fix other posthog-node code the upgrade breaks, and the report lists those files. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ndency upgrades The clock query carries a fresh random literal so a cached answer can't widen the test window. Step 3 may write the template files it saves, the dashboard copy includes the template's alias or name for its key, and a major dependency upgrade lands in the report's "Before cutover" list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Test against a wizard branch:
Add Results will be posted here when complete. |
Resolves the inventory, mapping and report conflicts toward the scope cut: a Resend template site moves only to a template made in PostHog, sent by templateId, and headers, tags and attachments stay on Resend. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under the scope cut, a moved template site sends a template made in PostHog by templateId with templateData. Rendered sends keep templateKey, subject, html and text. No local-template module, no upload on first send, and no cc, bcc, headers, tags or attachments. Co-Authored-By: Claude Opus 5.5 <[email protected]>
posthog-node's migratedFrom client option sends the provider a send moved from with every email request. Step 7 now sets it, and the Resend mapping names its value. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… site moves Review round 1 on #451: - Step 3 asks for the template ID once the person made the template in PostHog. A template without one stays: PostHog template not made yet. - Step 7 checks that the client takes migratedFrom, and undoes the posthog-node install when no site is left to move. - Template files from an API read are deleted once their site is classified, and the full-access Resend key right after Step 5. - Step 8 reads only the token's line of the env file, and picks a signed-in flow for the test email. - replyTo moves as one address or a list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… cutover Review round 2 on #451: - Step 3 asks for the address a PostHog template sends from and writes it in the site's from column, so Step 4 sets up a sender for its domain. - The report asks the person to send a test of each PostHog template and check that it is transactional, has a verified sender and fills its variables. - Step 5 asks to delete the full-access key on every way out of the step, and after a partial import only once the person won't repeat it. - Step 8 allows a signed-out flow: the person signs in as the recipient afterwards, so product events show on the same distinct ID. - migratedFrom is set only when the client options take it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…mes a person Review round 3 on #451: - Step 5 asks once whether to repeat an import that stopped partway, and asks about the full-access key after that answer or the repeat. - Step 8's test email comes from a moved site that passes distinctId; without one, the chain is recorded as not checked. - The client example marks migratedFrom as conditional. Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts: # context/skills/migrate-email/references/3-inventory.md # context/skills/migrate-email/references/7-report.md # context/skills/migrate-email/references/resend/mapping.md
account.md said template content goes only into a temporary file, while Step 3 reads the person's dashboard copies where they saved them. Only an API answer goes into the temporary file now. Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts: # context/skills/migrate-email/references/9-report.md # context/skills/migrate-email/references/resend/mapping.md
… the test email Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus
force-pushed
the
feat/migrate-email-resend-cutover
branch
from
November 2, 2026 16:49
21b5edd to
21f683e
Compare
Rename "Find the user ID" to "Link emails to your users", name the identify() ID, say that without distinctId PostHog records the email on a separate person keyed by the address, and add invites and signup checks to the cases that leave it out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 7 now leaves distinctId off an address check before the account exists, so Step 8 picks its test site from the plan rows that pass one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rows said only "Always", so an agent made up a reason, such as posthog-node not taking attachments. They now say this skill doesn't move them yet. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 9's report tells the person what would let a site move, from the row's reason. cc, bcc, attachments, headers and tags now say PostHog's send doesn't take them yet, not that this skill doesn't move them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus
added a commit
that referenced
this pull request
Nov 4, 2026
Carry #451's reason wording to SendGrid's field rows that kept a field with a bare "Always". For Resend's cc, bcc and attachments, say the skill doesn't move them yet: posthog-sdks.md in this PR lists them for Node.js, so "PostHog's send doesn't take it" would contradict it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…unded A key built from a URL or caller text could read as either "can be longer than 255 characters" or not. Such a key now counts as one that can, so the site stays on Resend and the report names the value to bound. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A double-quoted Liquid fallback inside the template's JSON body raises TokenizationError. Every fallback is quoted with ', and a fallback that holds a ' keeps the site on Resend, since Liquid has no escape character. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…finds it Step 6 ticked the key phase as set when the person said they would add the key themselves. The step now checks the env file with a count-only grep (or a successful set_env_values in the Wizard). A promise to add it later, or a refusal, records the key as not set, and the report lists it before cutover. Co-Authored-By: Claude Opus 5.5 <[email protected]>
dotenv loads a key with leading whitespace or spaces around =, so the count-only grep accepts them too. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The count-only grep counted a key that an env loader reads as empty: a value that is only a comment (= #phs_x), and a key overridden by a later empty line. The check now reads only the last line that sets the variable and needs its value to start with phs_, still printing only a count. The test runs the documented command against fixture env files in place of matching its wording. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Nov 4, 2026
…feat/migrate-email-resend-cutover
…nd-cutover Co-Authored-By: Claude Opus 5.5 <[email protected]>
dmarchuk
approved these changes
Nov 5, 2026
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(#450) finds every Resend send and gets the account ready, but it leaves every send on Resend. The person then rewrites each send by hand.With this PR the skill moves the sends in code. Each send PostHog can take moves to
posthog.emails.sendin posthog-node. Every other send stays on Resend with a reason. The skill then checks the build and one email on one person, and writes the report with the realsites_rewritten.Important
Stacked on #450, which is based on
main. Merge that first. This PR's own commits:07dbf3a...5dcdd21.The skill sends content from code with
templateKey, and setsmigratedFromon the client. Both ship in the next posthog-node minor release, from PostHog/posthog-js#5509. Hold the merge until that release is out.Merge risk
Warning
🟡 Merge risk: medium · two-way door
Changes
Three steps take the place of the old report step:
secretKeyandmigratedFrom: 'resend'. Each site whose plan row says it moves is rewritten in one of two forms:@react-email/renderand sends the result ashtml. Anhtmlortextsite sends its content as it is. Each send gets atemplateKeynamed for the kind of email.templateIdwithtemplateData, which takes the place of Resend'svariables.from,replyTo, the idempotency key anderror.namebranches keep working.distinctIdis the ID the app already identifies people with. A send to a suppressed address gets a branch that treatsrecipient_suppressedlike a sent email.cc,bcc,headers,tags,attachments, scheduled sends and batch sends stay on Resend.$workflows_email_sent, an engagement event of the same workflow, and a product event on the same distinct ID, read through the MCP'sexecute-sql.Step 3 asks the person once per Resend template whether to move it or keep it on Resend, reads its content to classify it, and asks for the PostHog template ID and the address it sends from. Step 4 then sets up a sender for that address.
mapping.mdgains the rewrite rules, andaccount.mdcovers reading a template's content.This revision (2026-11-03) adds "Link emails to your users" to Step 7: pass the
distinctIdthe app already passes toidentify(), so each send and its sent, opened and clicked events land on the user's person. Without it, PostHog records the email against a person whose distinct ID is the recipient's address, as master does today (distinct_id or to). An invite or an address check before the account exists leaves it out. Step 8's test email now comes from a site whose plan row passesdistinctId.The revision before (2026-11-02) matches the skill to the first release of the code send:
cc,bcc,headers,tagsandattachmentsstay on Resend.migratedFromis back on the client, now that posthog-node has the option.Evidence
Bundle test (vitest):
runs the rewrite and verify steps between the key and the reportexpects the steps1-detectto6-key, then7-rewrite,8-verify,9-report. Ond157859,pnpm testpasses 225 tests in 29 files, andpnpm buildbuildsmigrate-email-resend.zip.Agent runs on
85f6096. Claude Code on Sonnet 5.5 ran the skill as a dry run, headless, with only read tools, on a fixture app with four Resend sends: a React email,htmlandtextwithreplyTo, the Resend templateinvoice, and one with an attachment. The person's answers were in the prompt. Neither run changed a file in the fixture.invoiceon Resendrender(<WelcomeEmail … />)and sendshtmlwithtemplateKey: 'welcome'; thehtmlandtextemail keepsreplyTo.invoicestays (kept on Resend for now), the attachment email stays (attachments). The client setsmigratedFrom: 'resend'invoice; its PostHog template ID and senderinvoicemoves withtemplateIdandtemplateData: { INVOICE_URL: invoiceUrl }, and itsfromcolumn holds the template's sender, so Step 4 covers that domain. "Before cutover" lists a test of that template: transactional, verified sender,INVOICE_URLfilledThe runs are on
85f6096. Since then,a759ff4changes only Steps 5 and 8 and a comment in Step 7's example, the merges of feat(skills): add the migrate-email skill with a Resend variant #450 add one sentence to Step 3 for a template site that another field keeps on Resend,2c36853rewordsaccount.mdto match Step 3,21f683eends Step 8's email check when the person declines the test email, andd157859and5dcdd21rewrite Step 7'sdistinctIdsection and change Step 8's example site. ThedistinctIdsection changes which ID a moved site passes, not which sites move. The fixture's template site sets no other field, so the runs take the same path. Raw sessions and prompts are in proof storage.SDK facts come from posthog-node at #5509:
migratedFrom?: stringon the client options, sent as theX-PostHog-Migrated-Fromheader, and theerror.namevalues the rewrite keeps, such asvalidation_errorforrecipient_suppressed.Not checked:
Decisions
templateId, ortemplateKeywith renderedhtmlortext. No templates in code, no upload@react-email/renderand sendshtmlcc,bcc,headers,tags,attachmentsmigratedFromon the clientrecipient_suppressedlike a sent email at a site that throws or retries on errors🤖 Agent context
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5), as the docs, skills and wizard slice of Silthus/posthog#461. Reviews infleet proof, by Claude Opus 5.5 (high) on fresh eyes, correctness and risk: round 1 on5edeb92(findings fixed in64cf8ef) and round 2 (findings fixed in85f6096), and round 3 (findings fixed ina759ff4). GPT-6.1 Sol (high) final reads ona759ff4,e053dab,2c36853and21f683e: no findings. Opus 5.5 (high) final reads on thedistinctIdsection:d157859(one minor finding, fixed in5dcdd21) and5dcdd21(no findings).Earlier revisions of this description
The earlier text described templates converted to modules in the app's code, an upload on the wire, and
cc,bcc,headers,tagsandattachmentsmoving. The first release has none of these. Its "Impact" paragraph is removed. The rest is kept as it was.Moved from Silthus#2.
Problem
migrate-email-resend(PostHog/context-mill#450) 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.Tracking: Silthus/posthog#539, slice 15 of the Resend parity spec. Contract: PostHog/posthog#120921.
Merge Danger
Summary
What this adds beyond
moving-from-other-providers.md(PostHog/posthog#125415): that reference maps any provider's send to a PostHog template. This PR moves Resend sends in code to the code form of posthog-node: the field mapping, the Resend template conversion to Liquid, the suppressed-recipient and idempotency rules, and the verification chain from the send to the engagement and product events.Three steps replace the old step 7:
posthog-nodeclient withsecretKey. 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 an HTML fallback, or anything beyond plain variables and fallbacks, stays on Resend. Step 3 saves the content in a temporary file, and step 7 writes the module from it. ThetemplateKeyis the alias or name, lowercased, with other characters turned into-, and unique.@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. Renderedhtmlandreactcontent sends withouttemplateDataand uploads nothing.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 with the same$workflow_id, and a product event on the same distinct ID, all after the start of the test, 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. Lane gate on50b5f27:vitest run scripts/plugins/tests scripts/lib/tests, 29 files, 225 tests pass. CI runs on this PR. Up to date on 2026-11-02:50b5f27already holds its base, #450 at4ad8c6e, andmainatb699b20.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 (earlier revision, with the
migratedFromclient option that03011c3removes). The migrated template app'ssendWelcomeagainst a local stub of the PostHog API:pnpm exec vitest run scripts/lib/tests/migrate-email-skill.test.jspasses.50b5f27:03011c3. It drops one client option and renames one docs file in step 8, so the rewrite steps the runs above exercised are unchanged.8c841cd, which changes step 8's query and adds the typecheck baseline and the temporary template file.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)migratedFromclient optionmigrated_fromonsetup wizard finishedinsteadsites_rewrittencounts moved sites$workflow_idmatching the send's; no message id is on the eventsif_code_managedandif_not_superseded, so it never replaces a template edited in PostHog; another app's code-managed key is a follow-uptemplateDataand uploads nothingOpen: accept the live-run and prod-shape gaps. Recommended: accept; the fixture-app rerun of the skill on this head is owed.
Follow-up: a
templateKeythat another app's code-managed template already uses in the same project. It needs a tool that lists the project's template keys, which the skill's MCP tools don't name today.Involvement: no grilling rounds, no prototypes, one Opus 5.5 review of the strand (#435 and #436) with three final reads, ending clean, and one GPT-6.1 Sol (high) round on this revision (one finding, fixed) with a clean read of the fix. 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 (seven lenses), and three final reads ending clean: 23 findings, 21 fixed, 1 rejected (template churn on rendered sites), 1 follow-up. Proof captured.
Agent context
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5). Reviews: GPT-6.1 Sol (high) and Claude Opus 5.5 (high) in fleet proof panels on the fork. The reviewed head50b5f27is pushed here unchanged; its commits are signed.🤖 Generated with Claude Code