Skip to content

feat(skills): move SparkPost sends to PostHog - #468

Draft
Silthus wants to merge 57 commits into
feat/migrate-email-mandrillfrom
feat/migrate-email-sparkpost
Draft

Silthus wants to merge 57 commits into
feat/migrate-email-mandrillfrom
feat/migrate-email-sparkpost

Conversation

@Silthus

@Silthus Silthus commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

A person who sends with SparkPost and asks to move those sends to PostHog gets no help: no migrate-email variant covers SparkPost.

With this PR, migrate-email-sparkpost moves SparkPost sends to PostHog's email send in each language where both SparkPost and PostHog have an SDK, and wizard migrate sparkpost runs it.

Important

Stacked on #467. Merge that first. This PR's own changes: e1a4d44...ee9e706.
It adds one provider folder, one config variant, one AGENTS.md row 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: 'sparkpost' show whether people use this skill to move off SparkPost. Read it 14 days after the Wizard runs migrate as a family.

Merge risk

Warning

🟡 Merge risk: medium · two-way door

  • The new files run only when a person picks wizard migrate sparkpost, so reach is few. Then the skill edits their send code, and a wrong rewrite harms silently once they deploy: a refusal swallowed, open capture started for addresses that revoked it or on an account that minimizes open events, an inline value rendered unescaped, or a suppressed address mailed. Each moved site keeps its error behavior and its tracking, a site stays whenever an account, subaccount or template setting can't be checked, and the report's "Before cutover" list names what the person checks first.
  • No shared step, no other provider, and no build code changes in this PR's own diff.
  • Revert: git revert the merge commit. It removes the SparkPost variant.

Summary

references/sparkpost/ holds the three files each variant has:

  • sdk-reference.md: SparkPost's libraries in Node.js (2.x, and the 1.x transmissionBody and callback shape with its key renames), Python, Ruby, PHP, Go, Java and Elixir, the community .NET library, the HTTP API, SMTP and framework mailers. It covers how each reports errors and how each picks the EU host, the template language SparkPost also applies to inline content, stored templates, status reads, webhooks, and the APIs that are not sends.
  • mapping.md:
    • Fields. Each field maps to PostHog's or names what keeps a site on SparkPost: headers, metadata, recipient tags, campaign_id, several recipients or a stored list_id, start_time, sandbox, skip_suppression, IP pools and DKIM keys, amp_html, A/B tests, raw RFC 822 messages, transactional: false, a from on the sandbox domain sparkpostbox.com, SMTP and framework mailers, and branches on a SparkPost error code. Sites with a cc or bcc recipient (a header_to recipient, a CC header, or the SDK's cc and bcc), attachments or inline images stay too, because no PostHog SDK takes them yet.
    • Tracking. Opens and clicks are worked out per site from the recipient's options, then the transmission's, the template's, and the account's settings, and a subaccount can turn opens off. Both tracked moves with no field; neither moves a code site with captureEngagement: false and keeps a template site; one of each, or an unknown setting (account tracking not checked), stays. A site that tracked opens also stays when some addresses revoked open-tracking consent (Tracking Consent Revoked entries), unless the person accepts it, and when enable_minimized_event_passthrough is on at the account, the subaccount (which takes the account's when it sets none), the transmission or the recipient. When the person can't say and the API can't be read, it stays with open event minimization not checked.
    • Content. Inline content with plain expressions moves, with each value filled in code, an or fallback applied first, and HTML-escaped in html; a value in a link URL moves only when it is URL-safe. A statement, triple braces, macros or data-msys-* links keep the site. With perform_substitutions: false, text and html go as they are, but from, subject and reply_to are still substituted and read the same way.
    • Templates. A stored template send moves only to a template the person makes in PostHog. SparkPost's template language becomes Liquid: values, or defaults, if/elseif/else with comparisons and one and or or, each with loop_var, and the recipient's address. Triple braces, loop_index, nested loop_vars, # lengths, arithmetic, macros, data-msys-* links, a bare name inside each, a template's own tracking or transactional off, and tests on values that are not always booleans or lists keep it on SparkPost.
    • Subaccounts. Each account has its own suppression list. When moved sites send from several, the person agrees that they share PostHog's one list, or only one account's sites move.
    • Errors. Never widen a catch. A branch on 400 or 422 reads validation_error, a branch on 420 reads quota_limited, and a 429 branch reads status 429 with a code other than quota_limited. Python, Ruby and .NET raise network_error again, which SparkPost's error class never caught.
    • Webhooks. Each event type maps to a $workflows_email_* event. A handler that matches events by transmission_id, message_id, rcpt_meta, rcpt_tags or campaign_id keeps its sites on SparkPost.
    • Suppression writes. An app call that adds a transactional entry without a list_id keeps its SparkPost write and also writes PostHog's list, and a removal also removes from PostHog's list where feat(skills): move SendGrid sends to PostHog in every SDK language #459's Step 5 allows it. non_transactional and open_tracking writes stay SparkPost writes only. A send that runs right after the app removes its recipient from SparkPost's list stays.
  • account.md: SparkPost's DKIM, bounce and tracking records next to PostHog's, including the clash when the sending domain itself is a bounce CNAME; the EU host for every request; a read-only key; the account and subaccount tracking, minimization and transactional_default settings; a template's published version and options. The suppression import reads each account's list on its own and takes bounces, spam complaints, and transactional manual and compliance entries. It leaves out unsubscribes, non-transactional and list_id entries, and revoked tracking consent, and counts them for the report.

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>.md per 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 deletes posthog-sdks.md, and the plan's lang column is now language.

Evidence

  • Lane gate on ee9e706: bunx pnpm@10 test passes 262 tests in 29 files. bunx pnpm@10 run build builds migrate-email-sparkpost.zip, and skill-menu.json cliEntries lists sparkpost under migrate, next to the other providers and statsig.

  • CI: green, 7 checks.

  • Proof gate: fleet proof gate at ee9e706: gate: pass (kinds: live-run, prod-shape; review: carried from 8289926; 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 8289926: GPT-6.1 Sol read this round's change, e31f24b and the forward merges below it: clean.

  • Test seams (migrate-email-skill.test.js): the SparkPost bundle resolves every reference, holds its three files and the eight rewrite files, no posthog-sdks.md, and no other provider's folder; its mapping.md names migratedFrom: 'sparkpost'; it is a migrate subcommand.

  • Reused at ee9e706. 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 at 8289926 stand. The lane gate above ran at this head.

  • Dry run at 8289926 (live-run proof). 8289926 adds e31f24b, which points the SparkPost mapping at the rewrite file for the site's language in place of posthog-sdks.md (10 references). It turns the cc and bcc recipient row, content.attachments and content.inline_images into stay rows ("Always. PostHog's send doesn't take it yet"), and content.headers keeps a site for any header, the CC header included. The "Cc and bcc" section now only says how to spot them: its rules for recipients with the same values and for address placeholders are gone. It drops cc, bcc and attachments from the template rule, the Node.js, PHP, HTTP, Python and Go call shapes, and the recipient conversion. 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 takes cc, bcc or attachments. The expected outcome was written before the run: receipt.ts stays on cc and attachments, and every other site lands as in the last run. Result: 11 found, 4 moved, 7 left, as expected. receipt.ts stays with the reason "cc recipient, and attachments". The plan uses the language column, the run never names posthog-sdks.md, and the fixture was unchanged.

  • Dry run on 9d8e1fe. Claude Code on Sonnet 5.5 ran the built skill as a dry run, headless, with only read tools, on a fixture app with eleven SparkPost sends in TypeScript, Python and Go, plus suppression-list writes and a read, and a webhook route. The person's answers were in the prompt (a US account without subaccounts, tracking on by default, transactional by default, no Tracking Consent Revoked entries, open events not minimized); the expected outcomes were written before each run. The fixture was unchanged after the run.

    Site Expected Result
    welcome.ts: inline {{name}} from substitution_data, catch reads err.errors moves moved. The name is filled in code and escaped in html by a new escapeHtml helper; logs and returns on recipient_suppressed; throws an error with statusCode, name and code
    receipt.ts: SDK cc and a PDF attachment moves moved with cc and the base64 attachment. Logs data.id where it logged results.id (stays from 8289926: cc and attachments)
    verify-email.ts: 2.x callback (err, data) moves moved. The callback's code runs after the send with PostHog's error, and done is still called once
    password-changed.ts: raw fetch, click_tracking: false, opens left to the account stays stays: opens tracked and clicks not, and captureEngagement: false turns off both
    invoice.ts: stored template invoice-v2, a route passes req.body.items stays stays: items is not checked to be a list
    digest.ts: several recipients and campaign_id stays stays: several recipients, and campaign_id
    trial.ts: options.start_time stays stays: send time
    shipping.ts: recipient metadata stays stays: metadata, and the webhook matches deliveries by rcpt_meta
    send-test-email.ts: from on sparkpostbox.com stays stays: no sender can be verified for the sandbox domain
    password_reset.py: except SparkPostAPIException moves moved. except EmailError returns True on recipient_suppressed, as a sent mail would, and raises network_error again
    alert.go: (id, res, err) with a 429 branch moves moved. errors.As returns the rate-limit error on status 429 with a code other than quota_limited, so a used-up quota takes the wrapped error path; the id comes from result.ID

    The three suppression writes are listed with the other calls: the report asks the person to make the transactional upsert also write PostHog's list as Synced from SparkPost, to make the removal also remove from PostHog's list only for an Imported from or Synced from entry, and to leave the non_transactional upsert a SparkPost write. The run leaves out feat(skills): move SendGrid sends to PostHog in every SDK language #459's "no other imported list holds it" clause; with one account and one list, that changes nothing here. The webhook route's bounce and spam_complaint handling maps to $workflows_email_bounced and $workflows_email_blocked. Summary block: from: sparkpost, sites_found: 11, sites_rewritten: 5, sites_left: 6, as expected.

  • Prod-shape. A survey of real codebases' SparkPost send shapes (saved at e702f00, 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 sandbox domain sparkpostbox.com, the Node.js 1.x transmissionBody and callback shape, and fixture sites for raw HTTP sends, the callback form and an unverifiable sender.

  • Gaps:

    • No run applies the rewrite, typechecks or sends. The SDK capabilities are stated in the person's answers, not read from an installed release. That waits for the SDK releases.
    • The dry run moves sites in Node.js, Python and Go. Ruby, PHP, .NET and Elixir are covered by the files but not by a run.
    • No run at this head moves a template: the fixture's invoice now stays under feat(skills): move SendGrid sends to PostHog in every SDK language #459's rule on unchecked input. Runs at 10d5d3b and e702f00, before that rule, moved it to its PostHog template with if/else and each converted.
    • Rule paths without a fixture site: account tracking off or unknown, and a template's tracking or transactional off; subaccounts, including open_tracking_disabled, minimization inheritance and several accounts sharing one list; minimized open events on; revoked open-tracking consent present; perform_substitutions: false; an EU account; the Node.js 1.x shape; or fallbacks and link URL values; a branch on 400, 422 or 420; a send after a suppression removal; transactional_default off; return_path; an Idempotency-Key; the suppression import. A fixture for these is a follow-up.
    • No run with the PostHog MCP connected, and none inside the Wizard.

Decisions

Question Answer Who Where
When this merges Not before code sends are live and usable for customers, for #451 and every PR on it 👤 Human this PR
Which providers Mailgun, Loops, SendGrid, Postmark and SMTP, plus the providers the Wizard detects that a real codebase sends transactional email from 👤 Human Silthus/posthog#461
Stack shape One PR per provider in a row, on top of #459 🤖 Agent #459
Inline content with {{ }} Plain expressions move with the value filled in code and HTML-escaped in html, as SparkPost rendered them. Any statement keeps the site. With perform_substitutions: false, from, subject and reply_to are still read, since SparkPost still substitutes them 🤖 Agent this PR
A send without options.transactional Moves, and SparkPost used the account's transactional_default; unless that is true, the plan notes it sent as non-transactional. transactional: false stays 🤖 Agent this PR
SparkPost tracking Worked out from recipient, transmission, template, then account options; neither tracked moves a code site with captureEngagement: false; mixed, unknown, or a template with tracking off stays 🤖 Agent this PR
Revoked open-tracking consent Ask once whether the list holds Tracking Consent Revoked entries; when it does, a site that tracked opens stays unless the person accepts that PostHog tracks opens for those addresses 🤖 Agent this PR
Minimized open events A site that tracked opens stays when enable_minimized_event_passthrough is on at the account, the subaccount (taking the account's when unset), the transmission or the recipient, since PostHog's send doesn't take it; unknown stays 🤖 Agent this PR
Several accounts, one PostHog list Ask whether the moved sites may share one list; if not, only one account's sites move, since PostHog would refuse addresses another account suppressed 🤖 Agent this PR
Cc and bcc Spotted from header_to recipients and the CC header. From 8289926 the site stays, as the cc, bcc and attachments row says. Before, it moved when every recipient carried the same values, since PostHog sends one email to all of them 🤖 Agent this PR
Which suppressions move Bounces, spam complaints, and manual and compliance entries of type transactional, read per account. Unsubscribes, non-transactional and list_id entries are marketing preferences, and revoked tracking consent blocks no send 🤖 Agent this PR
App writes to SparkPost's list Only a transactional write without a list_id is mirrored into PostHog's list, the entries the import would take; non_transactional and open_tracking writes stay SparkPost writes 🤖 Agent this PR
A bare name inside each Keeps the template on SparkPost: SparkPost's docs read the item only through loop_var and don't say what a bare name reads 🤖 Agent this PR
A narrow catch on one SparkPost error Becomes a catch of PostHog's error that checks the same kind and raises the rest: 400 and 422 read validation_error, 420 reads quota_limited, 429 excludes quota_limited. Python, Ruby and .NET raise network_error again 🤖 Agent this PR
cc, bcc and attachments Stay on SparkPost in every language, since no PostHog SDK takes them yet (#472). Replaced moving them where the installed SDK took them 🤖 Agent this PR
Proof kinds Live run and a survey of real codebases' send shapes. No shadow diff: this PR's own diff changes no shared step 🤖 Agent this PR
Runtime proof the judge asked for Accepted as a gap under the standing rule: the dry run is the brief's proof form, and an applied rewrite with typecheck and send waits for the SDK releases. A fixture where the account or a subaccount minimizes open events is a recorded gap too; uncovered rule paths are listed under Gaps 🤖 Agent this PR
More final reads after round 2 Continued without escalating, under the standing rule: each read found a new, narrower issue and none came back after its fix. The last read, at this head, is clean 🤖 Agent this PR

Involvement: qa-swarm, 2 panel rounds (round 1: fresh-eyes, correctness, risk, design, anti-overengineering, sweep; round 2: correctness, fresh-eyes, risk, sweep) and 18 final reads, the last at 8289926, clean. Proof captured: 21 dry runs over the review, the last at 8289926, and 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

Silthus and others added 9 commits November 4, 2026 07:29
Adds the sparkpost variant with its SDK reference, field mapping, and
account notes: cc and bcc as header_to recipients, inline content that
SparkPost runs through its template language, templates to Liquid, the
msys webhook events, and the suppression list by source.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- headers, metadata, recipient tags and campaign_id always stay: no
  PostHog SDK takes headers or tags yet.
- if converts only for booleans, each only for lists; tracking options
  next to template_id keep the site.
- Go and Elixir decode base64 attachments; contentId is a bare name.
- A from name with special characters is quoted.
- Narrow catches stay narrow in every language, and Python and Ruby
  re-raise network_error where SparkPost's class never caught it.
- EU accounts read templates and suppressions from api.eu.sparkpost.com.
- Drop claims about PostHog the shared files don't make.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…arkpost

# Conflicts:
#	context/skills/migrate-email/config.yaml
#	scripts/lib/tests/migrate-email-skill.test.js
- A branch on SparkPost's 400 or 422 becomes a branch on PostHog's
  validation_error name in every language; Node.js carries error.name.
- A bare name inside each keeps the template: SparkPost reads the item
  through loop_var, and its docs don't say what a bare name reads.
- sdk-reference names each SDK's host setting as the EU signal, and
  account.md says up front which host every request uses for it.
- The client part says framework mailers serve other providers and
  always stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

🧙 Wizard CI

Run 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:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci feature-flags
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci feature-flags/django
  • /wizard-ci feature-flags/next-js
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a wizard branch:

  • /wizard-ci all wizard:my-branch

Add wizard:<branch> to any command above to pin the wizard branch. It defaults to main.

Results will be posted here when complete.

Silthus and others added 20 commits November 4, 2026 08:49
- Suppressions: read every source and type, so left-out entries can be
  counted, and read each sending account on its own: an unscoped search
  can be 20 minutes out of date.
- Templates: a stored version's tracking and transactional options
  apply unless the send overrides them; the dashboard intake asks for
  them, the AMP part and custom headers.
- Cc and bcc stay unless every recipient carries the same values.
- Inline values become strings before escaping; subject and reply_to
  follow the same rule as from; subjects are flagged for a test send.
- 420 maps to quota_limited; .NET re-raises network_error like Python
  and Ruby.
- Recipient variables avoid names the send already uses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
SparkPost takes only keys of 1 to 255 letters, digits, '.', '_' and '-', so
the bounded-key rule never keeps a SparkPost site.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rewrite throws

The Node rule rethrew PostHog's error with only its status, and a network
failure has none, so the old catch lost the code that says the request
never reached PostHog. Carry error.code as well.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Name SparkPost's sandbox domain sparkpostbox.com: a from on it keeps
  the site, since no sender can be verified for it.
- Cover sparkpost 1.x: transmissions.send({ transmissionBody }, callback)
  with camelCase keys, the (err, res) callback and res.body.results.id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ited, subaccount lists, 1.x key renames

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…es, and check ky's throwHttpErrors

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ng with the template's, and mirror suppression removals

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus and others added 28 commits November 4, 2026 13:12
…ome addresses revoked open-tracking consent

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…he account or subaccount minimizes open events

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…en-event minimization, also one that sets its own tracking

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…on setting takes the account's

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… the subaccounts that take it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ly_to with perform_substitutions off, and a recipient's minimization keeps a site

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…l-sparkpost

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]>
Every posthog-sdks.md reference now points at the rewrite file for the site's language. Cc and bcc recipients, attachments, and inline images now always stay on SparkPost, because no PostHog SDK takes them yet. Their conversion rules are gone, and the Cc and bcc section only says how to spot them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…arkpost

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]>
A stored SparkPost template now moves by bringing its email into the
code, rendered with the site's substitution_data, the recipient's values
first. Inline content that uses SparkPost's template language moves the
same way, where it used to stay. In the code, raw values, conditions,
loops, nested loops, empty(), lengths, whole-number arithmetic, array
indexes, dynamic content, snippets the code can pick, brace macros, and
URL-encoded link values now move. A template made in PostHog is used
only when the person asks for one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-email-sparkpost

# Conflicts:
#	scripts/lib/tests/migrate-email-skill.test.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant