Skip to content

feat(skills): move Mailgun sends to PostHog - #461

Draft
Silthus wants to merge 49 commits into
feat/migrate-email-postmarkfrom
feat/migrate-email-mailgun
Draft

Silthus wants to merge 49 commits into
feat/migrate-email-postmarkfrom
feat/migrate-email-mailgun

Conversation

@Silthus

@Silthus Silthus commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

A person who sends with Mailgun and asks to move those sends to PostHog gets no help: migrate-email covers Resend and, with #459 and #460, SendGrid and Postmark.

With this PR, migrate-email-mailgun moves Mailgun sends to PostHog's email send in each language where both Mailgun and PostHog have an SDK, or where the app posts to Mailgun's HTTP API, and wizard migrate mailgun runs it.

Important

Stacked on #460. Merge that first. This PR's own changes: fdb4671...9f1773e.
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: 'mailgun' show whether people use this skill to move off Mailgun. 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 mailgun, so reach is few. Then the skill edits their send code, and a wrong rewrite harms silently once they deploy: a refusal swallowed, a failure that used to pass now thrown (or the reverse), open or click capture changed, a suppressed address mailed, or a test-mode client that starts sending. Each moved site keeps its error behavior and its tracking, a site stays whenever a domain's tracking or the client's error behavior can't be read, 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 Mailgun variant.

Summary

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

  • sdk-reference.md: Mailgun's libraries in Node.js (mailgun.js and the older mailgun-js), Python, Ruby, PHP, Go (v4 and v5) and Java, the HTTP API with the clients real sends use (requests, RestClient, Net::HTTP, curl, Guzzle, HttpClient, RestSharp), SMTP, and framework mailers. It covers how each reports errors, the setting that points each client at the EU region, the clients that send nothing (Ruby's test mode, Python's dry_run), the send fields, 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 Mailgun: recipient-variables and batch sends, %recipient.*% and unsubscribe variables, o:deliverytime and the other o: options, custom h: headers, v: values and o:tag, amp-html, MIME sends, mailing list recipients, SMTP and framework mailers, and any field the table doesn't name. Sites with cc, bcc, attachment or inline stay too, because no PostHog SDK takes them yet. A to string from one stored address is one address; one the code joins from several counts as several. An Idempotency-Key moves when the code bounds its length and characters.
    • Tracking. Opens and clicks are worked out per site from the send's o:tracking-opens or o:tracking-clicks, else its o:tracking, else the domain's setting, which Mailgun turns off for a new domain. A field set to no or false counts as set. Both tracked moves with no field; neither moves with captureEngagement: false; one of each, htmlonly, or an unknown domain setting (domain tracking not checked) stays. A template site with tracking off stays.
    • Domains. Mailgun keeps suppression lists per sending domain. When moved sites send from several domains, the person agrees that they share PostHog's one list, or only one domain's sites move.
    • Templates. A template send moves only to a template the person makes in PostHog. Handlebars becomes Liquid: values, if/else if/unless on booleans, each over lists (a bare name or a helper's argument inside it reads the item), with on a value that is always an object, and equal. Triple braces, other helpers, @index, partials, ../, a go engine version, and blocks on values that are not always booleans or lists keep it on Mailgun.
    • Errors. Never widen a catch. A plain HTTP send is read with its client's options: axios with a validateStatus that accepts a failure, got with throwHttpErrors: false, and Guzzle with 'http_errors' => false answer a failed status, as fetch and requests do. A site whose client threw keeps failing on every PostHog error. A site that never read the status logs PostHog's error and goes on, and raises network_error the way the request did. A status check moves into PostHog's error handling. A branch on 400 reads validation_error, and a branch on 429 skips quota_limited. Node.js throws an error with status, details, name and code.
    • Code around the send. A client that sends nothing, such as Ruby's test mode or a Python Client(..., dry_run=True), keeps its sites when the app turns it on outside its tests. When only the tests turn it on, the sites move and the report names the tests that now need the PostHog send stubbed. A read of the answer's headers keeps the site.
    • Webhooks. Each event maps to a $workflows_email_* event. A handler that matches events by message id, a v: or X-Mailgun-Variables value, or a tag keeps its sites on Mailgun.
    • Suppression writes. An app call that adds to or removes from a domain's bounce or complaint list follows feat(skills): move SendGrid sends to PostHog in every SDK language #459's Step 5. A call on the unsubscribes list stays a Mailgun write only.
  • account.md: Mailgun's SPF, DKIM, tracking and MX records next to PostHog's, a read-only API key that the person deletes at the end of Step 5, and the US or EU host for every read. It reads the template version a site sends (domain and account templates) and the domain's tracking settings. It imports the bounces and complaints of the domains whose sites move, and of no other domain. Unsubscribes are counted and reported, and allowlists 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>.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 9f1773e: bunx pnpm@10 test passes 255 tests in 29 files.

  • CI: green, 7 checks.

  • Proof gate: fleet proof gate at 9f1773e: gate: pass (kinds: live-run, prod-shape; review: carried from 4260cda; 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 4260cda: GPT-6.1 Sol read this round's change, 7a4a8a6 and the forward merges below it. One finding, answered:

  • Test seams (migrate-email-skill.test.js): the Mailgun 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: 'mailgun'; it is a migrate subcommand.

  • Reused at 9f1773e. 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 4260cda stand. The lane gate above ran at this head.

  • Dry run at 4260cda (live-run proof). 4260cda adds 7a4a8a6, which points the Mailgun mapping at the rewrite file for the site's language in place of posthog-sdks.md (6 references, for field names, the client, the error under "The answer", and the answer's message id). It turns the cc, bcc, attachment and inline rows into stay rows ("Always. PostHog's send doesn't take it yet"), and the template rule now names only the o:tracking options. It drops files= from the Python call shape, the cc and bcc address conversion, and the attachment conversion bullet. 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, 6 moved, 5 left, as expected. receipt.ts stays with the reason "cc and attachment, which PostHog's send doesn't take yet". The plan uses the language column, the run never names posthog-sdks.md, and the fixture was unchanged.

  • Dry run on 8667a82. 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 Mailgun sends in TypeScript, Python, Go and PHP, a bounce list read, three suppression writes, and a webhook route. The person's answers were in the prompt (two sending domains, mail.acme.dev tracking both on and notify.acme.dev both off, no shared suppression list); the expected outcomes were written before each run. The fixture was unchanged after the run.

    Site Expected Result
    welcome.ts: Name <address> in to, catch reads err.status and err.details moves moved. The To line loses the name. Throws an error with the same status and details, plus name and code
    receipt.ts: to list of one, cc, a PDF attachment moves moved with cc and the attachment. Logs data.id where it logged Mailgun's id (stays from 4260cda: cc and attachments)
    invoice.ts: stored template with if/else and each, paid and items built by its caller moves to the person's PostHog template moved with templateId and templateData. Sends no from or subject: the template holds them
    digest.ts: recipient-variables stays stays: several addresses in to, plus recipient-variables and %recipient.name%
    trial.ts: o:deliverytime and o:tag stays stays: o:tag, also o:deliverytime
    password_reset.py: checks response.status_code moves moved into except EmailError. Raises network_error again, then the recipient_suppressed branch, then returns False as before
    export_ready.py: v:export_id stays stays: the webhook reads it back
    invite.py: requests post, from built from the inviter's name, raise_for_status() moves moved without distinct_id, since the invitee may not be a user yet. The display name is always double-quoted. Every error but recipient_suppressed raises, as before
    alert.go: v5, UnexpectedResponseError moves moved. errors.As on *posthog.EmailError; network_error takes the old path
    weekly.go: v4, sends from notify.acme.dev stays stays: domain notify.acme.dev keeps its own suppression list
    password_changed.php: constant h:Reply-To, catch (HttpClientException) moves moved with reply_to. The catch keeps validation_error and 4xx answers, and throws every other error again

    The webhook route is listed: a permanent failed and complained map to $workflows_email_bounced and $workflows_email_blocked. The report asks the person to make addBounce also add to PostHog's list (Synced from Mailgun) and clearBounce also remove from it under feat(skills): move SendGrid sends to PostHog in every SDK language #459's Step 5 check, and leaves the newsletter unsubscribe a Mailgun write. Step 5 names mail.acme.dev's lists only. Summary block: from: mailgun, sites_found: 11, sites_rewritten: 7, sites_left: 4, as expected.

    Two fixture additions were made with their expected outcomes written first. The person's answers now decline a shared suppression list, which keeps weekly.go (an earlier run moved it with captureEngagement: false from its domain's setting). And a caller that builds paid and items was added for sendInvoice: without it, the run at 926c9ce kept the invoice as feat(skills): move SendGrid sends to PostHog in every SDK language #459's strict rule on unchecked template values says.

  • Prod-shape. A survey of real codebases' Mailgun send shapes (saved at fa9e83e, reused: the commits after it are review fixes to the tracking, error, suppression and test-client rules, 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 to rule for one stored address, the stay for unnamed fields and for reads of the answer's headers, the HTTP clients real sends use, and three fixture sites: a plain HTTP post with a from built at run time, the v4 Go call form, and PHP with a constant Reply-To.

  • 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, Go and PHP. No run has a Ruby site, a .NET HTTP send, or mailgun-js.
    • Rule paths without a fixture site at this head: a site that moves with captureEngagement: false; mixed tracking, htmlonly, and an unknown domain setting; Idempotency-Key; inline files; Handlebars unless, with and equal, a go engine version, and triple braces; Ruby's test mode and Python's dry_run; axios, got and Guzzle options that turn the throw off, and RestSharp; a branch on 429; the EU host; MIME and mailing list sends; the unsubscribe count from an export or the API. 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
Which suppressions move Bounces and complaints of the domains whose sites move, and of no other. Unsubscribes are counted and reported, since PostHog's opt-outs cover marketing mail only, and allowlists are not suppressions. Replaced "every sending domain" 🤖 Agent this PR
Several domains, one PostHog list Ask whether the moved sites may share one list; if not, only one domain's sites move, since PostHog would refuse addresses another domain suppressed 🤖 Agent this PR
Mailgun tracking Work out opens and clicks from the send's fields, else the domain's setting; mixed, htmlonly or unknown stays, since captureEngagement: false turns off both. Replaced a rule that kept every yes or true field 🤖 Agent this PR
Which template versions convert Handlebars versions only. A go engine version keeps its site on Mailgun 🤖 Agent this PR
A plain HTTP send's error behavior Read from the client and its options: a client that threw keeps failing on every PostHog error; one that answered a failed status logs it and goes on, and raises network_error as before 🤖 Agent this PR
A Python site that read the response status Mailgun's Python SDK doesn't raise on an API error, so the status branch moves into except EmailError, which raises network_error again first 🤖 Agent this PR
A narrow catch on one Mailgun error Becomes a catch of PostHog's error that checks the same status, validation_error for a 400, or 429 without quota_limited, and raises the rest, never wider 🤖 Agent this PR
A client that sends nothing Ruby's test mode or Python's dry_run keeps its sites when the app turns it on outside its tests; when only tests do, the sites move and the report names the tests to stub 🤖 Agent this PR
Which host the account reads use The sending domain's region, api.mailgun.net or api.eu.mailgun.net, told by the client's URL or host setting 🤖 Agent this PR
Behavior tests for tracking and the domain choice No vitest seam runs an agent; the dry run is the proof, and the fixture gained a second domain to cover the choice. Refused as unit tests under the standing rule 🤖 Agent this PR
cc, bcc and attachments Stay on Mailgun 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 🤖 Agent this PR
More final reads after round 2 Continued without escalating, under the standing rule: each read found a new, narrower issue or covered a gate judge's point, and none came back after its fix. The last read, at this head, had one low on #472's test file, answered under Evidence 🤖 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 14 final reads, the last at 4260cda, with 1 finding on #472's files, answered. Proof captured: 21 dry runs over the review, the last at 4260cda, 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:27
Adds the Mailgun variant: how Mailgun sends look in Node.js, Python,
Ruby, PHP, Go, Java, HTTP, SMTP, and framework mailers; how each field,
template, and webhook event maps to PostHog; and Mailgun's DNS records,
template reads, and suppression lists.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Custom headers, v: values and o:tag always stay: no PostHog SDK takes
  headers or tags yet. The tags rewrite bullet is gone.
- if/unless, each and with convert only for booleans, lists and set
  objects; tracking options next to a template keep the site; a template
  site moves from, Reply-To or subject only as a fixed value.
- Catches stay as narrow as they were in Node.js, Python, Ruby, PHP and Go.
- A built from quotes a name with special characters; contentId is the
  bare file name; text content becomes bytes, since PostHog reads a
  string as base64.
- Every account read names the US and the EU host.

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

# Conflicts:
#	context/skills/migrate-email/config.yaml
#	scripts/lib/tests/migrate-email-skill.test.js
- A branch on Mailgun's 400, or on Mailgun::BadRequest, becomes a branch
  on PostHog's validation_error name in every language; other statuses
  keep their status checks. Node.js carries name on the thrown error.
- A bare {{name}} inside {{#each}} becomes {{ item.name }}, and {{this}}
  becomes {{ item }}.
- sdk-reference.md lists each SDK's EU host setting, Go's
  SetAPIBase(mailgun.APIBaseEU) and Rails' api_host included; the region
  note sits at the top of account.md.
- mapping.md names the packages a Mailgun send needs that also serve
  other code: form-data, mailgun-ruby's ActionMailer delivery method,
  PHP's PSR-18 and PSR-7 packages, and the app's HTTP client.

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 19 commits November 4, 2026 08:48
- Python: a catch of HTTPError, and a moved status check, raise
  network_error again, as requests and Mailgun's SDK raised it past them.
  Any language: a status check never saw a network failure, and a site
  that read no status logs API errors and goes on.
- Tracking: work out opens and clicks from the send, else the domain's
  setting (off for a new domain). Both on moves with no field, both off
  with captureEngagement: false, mixed or htmlonly stays; a template site
  with tracking off stays. account.md reads the domain's settings.
- Handlebars: names and helper arguments inside each read the item, and
  nested loops get their own names.
- account.md reads a pinned account-template version, asks for the
  version's From, Reply-To and Subject, and names where the unsubscribe
  count comes from.
- A Ruby client in test mode keeps its site when the app turns it on
  outside its tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…write 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]>
- A `to` string from one stored address holds one address; one joined
  from several, or taken from input or a mail object, counts as several.
- A field the Fields table doesn't name, such as `sender`, keeps the site.
- A read of the answer's headers, such as a rate-limit header, keeps the
  site.
- sdk-reference.md names the HTTP clients real sends use: RestClient,
  Net::HTTP, curl_init, Guzzle, HttpClient and RestSharp, and a request
  built from a base URL and a path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ilgun's error rewrite

axios with a permissive validateStatus and got with throwHttpErrors: false answer a failed status without throwing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-js keeps PostHog's code

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus and others added 21 commits November 4, 2026 13:07
…ression list, and name Mailgun's suppression writes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…domains whose sites move

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…g, like Ruby's test mode

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

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]>
Replace every posthog-sdks.md reference with the rewrite file for the site's language.
cc, bcc, attachment, and inline now stay on Mailgun because no PostHog SDK takes them, and the rewrite bullets that converted them are gone.

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

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 Mailgun template site now moves by bringing the version's HTML into
the code, rendered with its Handlebars semantics, with the version's
From, Subject, and Reply-To headers where the site sets none. A
template made in PostHog is used only when the person asks for one.

What moves that stayed before: t:text (the code writes the text part),
triple braces, if/unless on non-boolean values, each with else, @index,
@FIRST, @last, ../, with that can be missing or has an else, tracking
fields next to a template, and a send to one address with
recipient-variables, whose %recipient.<name>% values the code now puts
into the subject and content itself.

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

# 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