Repository navigation
Conversation
Add the postmark variant of migrate-email: the SDK reference for the official Node.js, Python, Ruby, PHP, .NET and Java SDKs and the community Python and Go packages, the field, call, template and webhook mapping, and the account file with Postmark's DNS records, template reads and per-stream suppression import. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Headers, Metadata and Tag always stay: no PostHog SDK takes headers or tags yet. The tags rewrite bullet is gone. - Sections convert to Liquid if/unless only for booleans, null or missing values, and each converts to for only for lists. - Tracking switches next to a template keep the site. - Go and Elixir decode the base64 attachment content first; contentId is a bare name. - Never widen a catch, per language, including the network errors that Postmark's API-only catches let through. - No claims about PostHog beyond posthog-sdks.md and the steps. 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. |
- An invalid-send branch (ApiInputError, ValidationException,
InvalidEmailRequestError, or a 422) becomes a branch on PostHog's
error name validation_error in every language, never a status; the
Node.js rewrite carries name on the thrown error.
- A bare name inside {{#each}} reads the current item: {{ item.name }}.
- The Postmark package stays for Postmark's other APIs and for
postmark-rails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- InvalidEmailRequestError and Python's ValidationException never
covered inactive recipients: their branch now raises
recipient_suppressed first instead of catching it as validation_error.
- A one-entry To list moves as that entry.
- Inside {{#each}}, a top-level key needs ../../, as Postmark documents.
- A send with no tracking field followed the server's settings, and
postmarker never tracked links: the plan row notes that PostHog now
captures opens and clicks.
- Suppressions are imported from every server whose sites move, and
broadcast-stream lists are no longer counted.
- An app call that adds outbound suppressions is reported, so the person
moves it before the deploy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A batch call with a written list of one message is one send; its refusal came back in the answer's ErrorCode, so the rewrite reads PostHog's error instead. - TrackOpens: true or link tracking next to a template is dropped, as the Fields table says; only the off values keep the site. - .NET's positional SendMessageAsync and PostmarkMessage take the text body before the HTML body; SendMessage is the synchronous shim. - A Status or ErrorCode check after a single .NET or PHP send only saw successes, since both clients throw on any answer but 200. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d per-server lists Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ns and links captureEngagement: false turns off both, so such a site stays, as SendGrid's and Mailgun's do. Co-Authored-By: Claude Opus 5.5 <[email protected]>
PostmarkMessageBase's constructor sets TrackLinks to None, so a .NET send tracks links only when the code sets it, as postmarker's sends do. The rewrite adds captureEngagement: false only when Postmark tracked neither. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ull follows the server Only an SDK message whose code never sets TrackLinks keeps the constructor's None; null and plain HTTP sends follow the server's setting. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…Links default The positional SendMessageAsync extension builds a PostmarkMessage, so its links were not tracked. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…send leaves TrackOpens out Only TrackLinks has an SDK default of None in postmarker and .NET; TrackOpens defaults to null in both and follows the server. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…from PostHog's suppression list Co-Authored-By: Claude Opus 5.5 <[email protected]>
…check Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ss set Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Nov 4, 2026
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l-postmark Brings #472's rewrite-<language>.md files through the layer below. posthog-sdks.md is gone; this provider's mapping moves to the rewrite file in the next commit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Postmark mapping now sends the agent to the rewrite file for the site's language instead of the deleted posthog-sdks.md. Cc, Bcc, and Attachments now always stay on Postmark, because no PostHog SDK takes them yet. The rewrite bullets that converted them are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…stmark Brings #472's Elixir fix (fall back past a nil :posthog host) and each lower provider's move to the rewrite file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's single list of error names in Step 7 and the reply-to rule in each rewrite file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings #472's added assertion in the Step 7 proxy test. No skill text changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…eat/migrate-email-postmark
A Postmark template send now moves by bringing the template, inside its
layout, into the code and rendering TemplateModel there. A template made
in PostHog is used only when the person asks for one.
Moves now that stayed before: raw {{{x}}} and {{& x}} values, sections
on any value that is never a list, tested with Mustachio's truth test,
inverted sections used as fallbacks, ../ to any outer scope, and
template sends with opens or links untracked. The email carries the
CSS that InlineCss inlined.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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
A person who sends with Postmark and asks to move those sends to PostHog gets no help:
migrate-emailcovers Resend and, with #459, SendGrid.With this PR,
migrate-email-postmarkmoves Postmark sends to PostHog's email send in each language where both Postmark and PostHog have an SDK, andwizard migrate postmarkruns it.Important
Stacked on #459. Merge that first. This PR's own changes:
a8d0023...fdb4671.It adds one provider folder, one config variant, one
AGENTS.mdrow and the provider in the bundle test. The shared steps it uses, and their review fixes, come from #459.Like #459, it waits for the PostHog SDK releases with the code-form email send. Nothing merges until code sends are live and usable for customers: Michael's rule for #451 and every PR stacked on it.
Impact
Sends that arrive with
migratedFrom: 'postmark'show whether people use this skill to move off Postmark. Read it 14 days after the Wizard runsmigrateas a family.Merge risk
Warning
🟡 Merge risk: medium · two-way door
wizard migrate postmark, so reach is few. Then the skill edits their send code, and a wrong rewrite harms silently once they deploy: a refusal swallowed, open or link capture changed, or a suppressed address mailed. Each moved site keeps its error behavior and its tracking, a site stays whenever a server's tracking or list can't be checked, and the report's "Before cutover" list names what the person checks first.git revertthe merge commit. It removes the Postmark variant.Summary
references/postmark/holds the three files each variant has:sdk-reference.md: Postmark's official libraries in Node.js, Python, Ruby, PHP, .NET and Java, the community libraries in Python (postmarker) and Go, and the HTTP API. It covers how each reports errors, stored templates and layouts, status reads, webhooks, and the APIs that are not sends.mapping.md:Headers,Metadata,Tag, aMessageStreamother thanoutbound, batch and bulk sends, SMTP and framework mailers, and branches on otherErrorCodes. Sites withCc,BccorAttachmentsstay too, because no PostHog SDK takes them yet. A batch call with a written list of exactly one message is one send.postmarkerand .NET sendTrackLinks: None; PHP'ssendEmailModelsendsTrackOpens: false), or the server's setting. Both tracked moves with no field; neither moves withcaptureEngagement: false; one of each, or an unknown server setting (server tracking not checked), stays. A template site with either untracked stays.eachover lists,../to top-level keys, and the layout's{{{ @content }}}. Raw-HTML braces,pm:unsubscribe, and sections on values that are not always booleans or lists keep it on Postmark.ApiInputError, a422,ValidationExceptionandInvalidEmailRequestErrorreadvalidation_error; the last two excluderecipient_suppressed, which they never covered. A catch of Postmark's API errors only raisesnetwork_erroragain. AStatusorErrorCodecheck after a single .NET or PHP send is removed, since both clients threw on any answer but200. A batch of one logs a refused message and raises every other failure.RecordTypemaps to a$workflows_email_*event. A handler that matches events byMessageID,MetadataorTagkeeps its sites on Postmark.outboundsuppressions keeps its Postmark write and also writes PostHog's list, as feat(skills): move SendGrid sends to PostHog in every SDK language #459's Step 5 says, until the server's last send moves.account.md: Postmark's DKIM and Return-Path records next to PostHog's, a migration token per server for templates, tracking settings and suppressions, and the import of theoutboundstream's suppressions (hard bounces, spam complaints, manual suppressions). Broadcast streams and other transactional streams are left out, with the reason why.The mapping names PostHog's fields, client and errors through the rewrite file for the site's language. Those files come from #472, which the stack now sits on under #459: one
references/rewrite-<language>.mdper PostHog SDK (Node.js, Python, Go, Ruby, PHP, .NET, Rust and Elixir), and Step 7's single list of what every SDK's send takes. Steps 7 and 8 run each rewrite file's check on the whole project, in place of a typecheck per language. #459 deletesposthog-sdks.md, and the plan'slangcolumn is nowlanguage.Evidence
Lane gate on
fdb4671:bunx pnpm@10 testpasses 254 tests in 29 files.bunx pnpm@10 run buildbuildsmigrate-email-postmark.zip, andskill-menu.jsoncliEntrieslistspostmarkundermigrate, next toresend,sendgridandstatsig.CI: green, 7 checks.
Proof gate:
fleet proof gateatfdb4671:gate: pass (kinds: live-run, prod-shape; review: carried from ab933ea; CI: green; judge: fail accepted by standing rule). The judge's gap is recorded under the standing rule: the dry run is the proof form the brief names, and an applied rewrite with typecheck and send can't run until the PostHog SDK releases with the code-form email send are out.Final read at
ab933ea: GPT-6.1 Sol read this round's change,74f1efdand the forward merges below it. One finding, answered:migrate-email-skill.test.js(line 145) bans an old heading's wording instead of checking that the references resolve. Rejected: the assertion is feat(migrate-email): move Resend sends in every PostHog SDK's language #472's (bb711c5), not this PR's. It guards that the rewrite files no longer point at a provider section most mappings lack, and the build test already fails on an unresolved file reference for every provider. Routed to feat(migrate-email): move Resend sends in every PostHog SDK's language #472's owner as a low.Test seams (
migrate-email-skill.test.js): the Postmark bundle resolves every reference, holds its three files and the eight rewrite files, noposthog-sdks.md, and no other provider's folder; itsmapping.mdnamesmigratedFrom: 'postmark'; it is amigratesubcommand.Reused at
fdb4671. This head adds only clean merges of #472, at 5398fd9 and 6924d48 (one test assertion), which leave this PR's own diff unchanged. 5398fd9 changes one Step 7 rule: behind a reverse proxy, email gets its own client on PostHog's host. Every fixture's PostHog host is PostHog's own, so the rule can't change a site, and the dry run and final read atab933eastand. The lane gate above ran at this head.Dry run at
ab933ea(live-run proof).ab933eaadds74f1efd, which points the Postmark mapping at the rewrite file for the site's language in place ofposthog-sdks.md(10 references, for field names, the client, the async Python client, the synchronous .NET send, and the error under "The answer"). It turns theCc,BccandAttachmentsrows into stay rows ("Always. PostHog's send doesn't take it yet") and names them in the template rule among the fields that keep any site. It drops the rewrite bullets that splitCcandBccinto lists (theReplyTosplit stays) and converted an attachment, with its base64 decode in Go and Elixir and its barecontentId. The row for a language without a PostHog SDK that sends email now names Java or Kotlin and points at Step 7's list. The forward merge below it brings feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files and Step 7's list of what every SDK's send takes. The same headless dry run, on the same fixture, now runs with answers that no longer say an SDK takescc,bccor attachments. The expected outcome was written before the run:receipt.tsstays on cc and attachments, and every other site lands as in the last run. Result: 12 found, 6 moved, 6 left, as expected.receipt.tsstays with the reason "cc and attachments". The plan uses thelanguagecolumn, the run never namesposthog-sdks.md, and the fixture was unchanged.Dry run on
8bc3253. Claude Code on Sonnet 5.5 ran the built skill as a dry run, headless, with only read tools, on a fixture app with twelve Postmark sends in TypeScript, Python, Ruby, PHP and C#, plus a webhook route. The person's answers were in the prompt (one server, open and link tracking on); the expected outcomes were written before each run. The fixture was unchanged after both runs.welcome.ts: catch readsstatusCodeandcodestatusCode, PostHog's stringcodeandname;MessageStream: 'outbound'droppedreceipt.ts:Ccand a PDF attachmentccand the base64 attachment. Logsdata.idwhere it loggedMessageID(stays fromab933ea: cc and attachments)invoice.ts: template alias with a layout,paidanditemsbuilt by its callertemplateIdandtemplateData. Sections,eachand the layout listed as converteddigest.ts:sendEmailBatchtrial.ts:TagandMetadatanotice.ts: HTTP, a caller passes thebroadcaststreamMessageStreambroadcastpassword_reset.py: async,InactiveRecipientException, thenPostmarkAPIExceptionAsyncPosthog. Oneexcept EmailError: raisesnetwork_erroragain, then therecipient_suppressedbranch, then the general oneverify_email.py: HTTP with anErrorCodecheckexcept EmailErrorraises the sameRuntimeError, and letsnetwork_errorthrough as the httpx error wentusage_alert.rb:rescue Postmark::ApiInputErrorPostHog::EmailErrorand raises unless its name isvalidation_error, which also covers a suppressed recipientteam_report.rb:bccbccsignup.php:sendEmailBatch([$message]), readsErrorCode\PostHog\EmailError, throwsnetwork_erroragain, logs avalidation_erroras refusedAlertSender.cs: positionalSendMessageAsyncTrackLinks: Nonewhile the server tracks opens, andcaptureEngagement: falsewould turn off bothThe webhook route is listed:
BounceandSpamComplaintby address map to$workflows_email_bouncedand$workflows_email_blocked, and the report names the webhook destination. Summary block:from: postmark,sites_found: 12,sites_rewritten: 7,sites_left: 5, as expected.The two runs differ on one site, by fixture, not by model. The first run (
run-8bc3253) had no caller ofsendInvoicein the fixture, and kept the invoice with "paidis not checked to be a boolean, anditemsis not checked to be a list" (12/6/6), as feat(skills): move SendGrid sends to PostHog in every SDK language #459's strict rule says. A caller that builds both values was then added, with its expected outcome written first, and the second run (run-8bc3253-j) moved the invoice (12/7/5). The other eleven sites match in both.Prod-shape. A survey of real codebases' Postmark send shapes (saved at
a287f14, reused: the commits after it change only tracking, error and suppression rules, which the dry run at this head covers, the forward merge of feat(skills): move Resend sends to posthog-node in migrate-email #451's Step 6 key check, and feat(migrate-email): move Resend sends in every PostHog SDK's language #472's rewrite files). It added the batch of one, the .NET positional forms and their text-before-HTML order, and the removal ofStatusandErrorCodechecks after a send that throws.Gaps:
postmarkersends; PHP'ssendEmailModeldefault; a narrowInvalidEmailRequestErrororValidationExceptioncatch; several servers sharing one suppression list; app calls that add to or reactivate theoutboundsuppressions. A fixture for these is a follow-up.Decisions
outbound, the stream the moved sites send through. Broadcast streams are marketing mail, and other transactional streams stay with their sitescaptureEngagement: falseturns off both. Replaced a plan-row note that let PostHog start capturing what Postmark didn'tInvalidEmailRequestErrorandValidationExceptionexcluderecipient_suppressedInvolvement: qa-swarm, 2 panel rounds (round 1: fresh-eyes, correctness, risk, design, anti-overengineering, sweep; round 2: correctness, fresh-eyes, risk, sweep) and 13 final reads, the last at
ab933ea, with 1 finding on #472's files, answered. Proof captured: 19 dry runs over the review, the last atab933eaand a prod-shape survey.🤖 Agent context
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5, high), as the migration-skills slice of Silthus/posthog#461. Reviews on GPT-6.1 Sol (high), the fresh-eyes lens on Opus 5.5; fixes on Opus 5.5 (high). Dry runs on Claude Sonnet 5.5 (claude-sonnet-5-5).🤖 Generated with Claude Code