Skip to content

docs(messaging): send transactional email from code by default - #128870

Open
Silthus wants to merge 10 commits into
masterfrom
docs/transactional-email-skill-capability-copy
Open

Silthus wants to merge 10 commits into
masterfrom
docs/transactional-email-skill-capability-copy

Conversation

@Silthus

@Silthus Silthus commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The sending-transactional-emails skill sets up the email as a PostHog template. It gets a template made transactional, hands over the key, then writes a template_id send. But the send API is gaining a code form (#127123): the app renders the email itself and sends it with a template_key, subject and html. An email in the repo is easier to edit, version and review than one in a separate editor, so that should be the default the skill sets up.

The skill and the installing skill's email step also list email providers and packages for the agent, and offer PostHog whenever one comes up. That reads as a sales pitch, and agents can discount the rest of a skill that reads that way.

With this PR, an agent sets up the email in the app's code by default. It asks once, "Keep the email in your code (recommended), or design it in PostHog?", and takes the template path when people who don't edit code will change the copy. It offers PostHog transactional email once, by what it does for the user, and only when the app sends email and the user works on that code or just finished setup.

Important

Merge together with the SDK releases, once code sends are live and usable for customers. The skill keeps the email in code by default, sends it with the backend's SDK, and has no fallback for projects without the code form.

Merge order:

  1. feat(messaging): send an email with inline html from code #127123 is deployed, and workflows-email-from-code is on for every organization (done 2026-11-05).
  2. All eight SDKs are released: posthog-js#5509, posthog-python#1171, posthog-go#406, posthog-ruby#324, posthog-php#288, posthog-dotnet#365, posthog-rs#324 and posthog-elixir#236.
  3. Each <release> in references/sdks.md is replaced with that SDK's first release that sends content from code.
  4. #129864 merges and is deployed, so a key or MCP login with transactional_email:write can make a template transactional, as the skill's make-transactional step says.
  5. This PR merges.
  6. context-mill#456 merges and is released.
  7. wizard#1507 merges.

Until then this PR stays unmerged.

Merge Danger

Warning

🟡 Merge risk: medium · two-way door

  • Merged before the SDK releases, an agent writes a send with an SDK version that has no emails API, and references/sdks.md still shows <release> in place of each minimum version. Hence the merge condition above.
  • Only agent instructions change: four Markdown files in products/*/skills. The Wizard publishes this skill through context-mill#456, which reads it from master.

Changes

  • Code is the default (sending-transactional-emails/SKILL.md):
    • The skill opens with where the content lives: in the app's code, the default, or in a PostHog template.
    • Code or template: ask once, recommend code, and don't ask when the user already said or the app already sends templates.
    • Set it up is the code path, in six steps: the verified sender, branding, the email, the API key, the send and the test. The email is a function in the repo that returns the subject and an HTML string (plus text when the app's other emails have it), or a React Email component rendered to HTML when the app already uses React Email. It gets a template_key that says what it is.
    • Design it in PostHog holds every template-only step, and replaces The email. when the user picks a template: the template email, no marketing category, and make it transactional.
    • API key asks for Transactional email set to Write, and says that a key that already has Workflow set to Write works too.
    • Send from code names the SDK for each backend: Node.js, Python, Go, Ruby, PHP, .NET, Rust and Elixir, with each package and its secret key option, and the HTTP API for any other language. A new references/sdks.md gives each SDK's client setup, code send, template send, result and error, and status read. Every name in it comes from the SDK PR heads listed above. New rules:
      • Sender: the code form's from fallback and sender_not_verified.
      • Engagement events: for every send, code or template, the agent checks the project's engagement events setting, which is off by default, and offers to turn it on. It says these events count toward event usage. A code send can still leave out its own events with captureEngagement: false.
      • Template send fields: a template send takes none of the code send's fields. The template carries its own sender and reply-to address.
      • Client: the agent reuses the app's PostHog client, unless the app's host is a reverse proxy. Then email gets its own client on PostHog's host, because a client with the secret key loads flag definitions from its host with it. A self-hosted instance sets the app host.
      • Secrets in the email: a code send under its own key is stored as sensitive.
  • Offer it once (both skills): the offer names what PostHog does, "send this app's transactional emails directly from code, with delivery and engagement events next to your product analytics", asks one question that is easy to decline, and never comes back. The description and the installing skill's step 7 no longer list providers or packages.
  • Moving an existing email (references/moving-from-other-providers.md): loads only when the user asks to move an email.
    • The provider table says where each email lives today, code or provider. An email built in code keeps its subject and bodies, and one stored at the provider becomes code first.
    • Before the switch, the agent tells the user what the old send passes that a PostHog send doesn't take, such as attachments, cc or bcc, a scheduled delivery time, custom headers or tags.

How did you test this code?

  • python -m products.posthog_ai.scripts.build_skills --lint on b1a5947: 173 product and 117 project skills pass. The 9 advisory warnings are in other skills.
  • oxfmt over the three files, and the pre-push ci:preflight --strict on each push: no findings.
  • context-mill#456 at 76c38e8 builds with these files in place of master's. Its packaged references/sending-transactional-emails.md is byte for byte this SKILL.md (at 46c18e1).
  • references/sdks.md, checked on 2026-11-05 against the head of each SDK PR above: each package, client option, send and status call, result and error name is in that PR's code.
  • context-mill#456 at fc7d606 builds with this branch's files at 316b4a6 in place of master's. Its bundle carries references/sdks.md byte for byte.
  • build_skills --lint on 316b4a6 (sparse checkout, this product's skills): passes. oxfmt over the changed files: no changes. The pre-push preflight didn't run on this box (no hogli), so CI is the full check.
  • Wizard walks of both paths, run on 2026-11-05 with wizard#1507 at f95c37b1 and context-mill#456 serving this skill at 99e527f, in project 2 on an Express sample app. Both runs exited 0.
    • Code path: the agent asked "Keep the email in your code (recommended) or design it in PostHog?" first, then the sender, branding, the key (with this PR's scope wording), engagement events and a test send.
      • It wrote the email in the repo with template_key welcome, and a send at signup with the user's distinct ID and an idempotency key.
      • It sent over the HTTP API, because the latest posthog-node has no code send yet. That's why this PR waits for the SDK releases.
      • The generated send, called with an invalid address, returned the API's 400 invalid_input.
    • Template path:
      • The agent made the template in project 2 and asked for confirm before making it transactional. Read back over the API, the template is transactional with no marketing category.
      • It wrote posthog.emails.send({ to, templateId, templateData, distinctId, idempotencyKey }) on the app's client, with secretKey and enableLocalEvaluation: false.
      • An invalid address returned validation_error / invalid_input.
    • Not shown: a delivered email. The test send was skipped in both runs.
    • The walks ran before 00b8e40 and 2c9d269. Those change the reverse proxy client, the engagement check and the template send's fields, and the sample app has no proxy.
  • 00b8e40 and 2c9d269: build_skills --lint passes, and oxfmt --check passes on both files. context-mill#456 at 3390e97 builds with this branch's files at 2c9d269, and its bundled SKILL.md is byte for byte this one.
  • Checked against master:
    • Engagement events are recorded only when shouldCaptureEngagementEvents(teamId) is true (email.service.ts, email-tracking.service.ts). That reads capture_workflows_engagement_events, default False.
    • SendEmailSerializer rejects every CODE_SEND_FIELDS field sent with template_id.
    • posthog-python's head calls load_feature_flags() from flag evaluation whenever a secret key is set, so it fetches {host}/flags/definitions with the key.

Docs: the send API docs for the code form follow when the flag opens, as #127123 says.

Decisions

Question Answer Who
Where the email lives by default In the app's code; a PostHog template when the user picks it, asked once with code recommended 👤 Human, 2026-11-04
When this merges After workflows-email-from-code reaches 100%; no fallback for projects without code sends 👤 Human, 2026-11-04
How the offer reads What PostHog does, no provider list, asked once, easy to decline, never repeated; the migration reference may name providers 👤 Human
How the template path is laid out One Design it in PostHog section in place of per-step Template markers 🤖 Agent
The offer in the installing skill One sentence of its own, not a pointer: that skill runs where sending-transactional-emails isn't loaded 🤖 Agent

Involvement: no grilling rounds, no prototypes. Two Opus 5.5 review panels: round 1 (fresh eyes, correctness, risk, design, anti-overengineering, sweep: four major, eight minor and nit findings; ten fixed in f1742ba, two rejected) and round 2 on the fixes (three major findings on this description's stale merge hold, fixed here, and five minor and nit findings, fixed in 89e131c), then two final reads (one minor finding, fixed in b1a5947). On the eight-SDK change, one Opus 5.5 final read: two major findings, the Elixir package line (fixed in 21d379c) and a merge order that waited only for Node.js and Python (fixed in this description). Then two GPT-6.1 Sol final reads. One read 00b8e40, covering the scope wording and the reverse proxy client. The other read 2c9d269, the fix for two PostHog Review findings (engagement events checked for every send, and no code fields on a template send). Both found nothing.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5), as the docs, skills and Wizard slice of Michael's transactional email map.

🤖 Generated with Claude Code

The sending-transactional-emails skill was template-first: it got a
template made transactional in PostHog, then wrote a template_id send.
It now keeps the email in the app's code by default, as an HTML string
or a React Email component sent with template_key, subject and html,
because code is easier to edit, version and review. It asks once
whether to keep the email in code or design it in PostHog, for when
people who don't edit code change the copy.

A project without the code form answers a code send with 404
not_found. The skill then says so in one line and moves the email to
a PostHog template.

The skill and the installing skill's email step also listed email
providers and packages, and offered PostHog whenever one came up.
Agents now offer it once, only when the app sends email and the user
works on that code or just finished setup, by what PostHog does for
them. The provider mapping reference loads only when the user asks to
move an existing email, and maps each send to the code form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Nov 4, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions github-actions Bot added the docs Improvements or additions to documentation label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 6791b36: 2 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Silthus and others added 2 commits November 4, 2026 07:29
Review fixes for the code-first skill:

- The setup steps are now the code path only. The template-only steps
  (design, no marketing category, make transactional, engagement
  events) live in one "Design it in PostHog" section that replaces the
  email step when the person picks a template.
- A code send records engagement events by default, so the code path
  now tells the person they count toward event usage and offers
  captureEngagement: false.
- The fallback keeps the code send until the template is transactional
  and stops when the template steps can't run, so a project with no
  transactional email access keeps its code.
- The fallback and the provider reference name the HTTP field
  message_data, not template_data.
- The provider table says where each email lives, and the send fields
  live in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill publishes only after workflows-email-from-code reaches every
project, so no project answers a code send with 404 not_found for that
reason by then. The fallback section and its limits line go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 3 commits November 4, 2026 07:38
- The HTTP code-send line lists from, reply_to and capture_engagement,
  and the engagement rule gives the JSON form, "capture_engagement": false.
- Moving an email built with a React Email component renders it to HTML
  for the html field.
- Before a move, the agent tells the user when the old send passes
  attachments, cc or bcc, which a PostHog send doesn't take.
- The provider reference no longer restates the send fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check before a move named only attachments, cc and bcc. It now
covers anything outside the code-send fields, such as a scheduled
delivery time, custom headers or tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kill

The skill sent every backend other than Node.js and Python to the HTTP
API. It now lists the eight SDKs that send email (Node.js, Python, Go,
Ruby, PHP, .NET, Rust and Elixir), and a new references/sdks.md gives
each one's client setup, both send forms, its result and error, and its
status read. Minimum versions stay placeholders until the releases. A key
with the older Workflow write scope still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus added a commit to PostHog/context-mill that referenced this pull request Nov 5, 2026
… eight SDKs

PostHog/posthog#128870 adds references/sdks.md, which covers each SDK's
client setup, sends and status read. The build fetches it with the
skill's other files, and the description names all eight SDKs.

The build answers 404 for it until #128870 merges into master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 2 commits November 5, 2026 08:00
The line read like a mix.exs dependency to paste, with the version
placeholder inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ional

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus marked this pull request as ready for review November 5, 2026 08:42
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team November 5, 2026 08:43
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-growth (products/wizard/product.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). For a skipped owner, the locator is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

@Silthus Silthus added the stamphog Request AI approval (no full review) label Nov 5, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-add the stamphog label to request another review once you have addressed this.

These are documentation-only changes with current-head review assurance, but @PostHog's earlier engagement-capture concern is not demonstrably fixed. Request confirmation from @PostHog/team-workflows or Harley Alexander and address that comment before re-requesting.

  • PostHog Review (Standard) reviewed the current head: no findings.
  • @PostHog on products/messaging/skills/sending-transactional-emails/SKILL.md: The engagement-capture concern remains unaddressed: the project-setting check still applies only to templates, while code sends promise engagement events by default. The checkout's team workflows configuration still disables engagement capture by default. Address this comment by checking the setting for both paths, or obtain independent confirmation that the dependent implementation removes that requirement, before re-requesting.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 0L, 0F substantive, 207L/4F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (207L, 4F, single-area, docs)
stamphog 2.7.0 .stamphog/policy.yml @ 99e527f · reviewed head 99e527f

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Nov 5, 2026
Silthus and others added 2 commits November 5, 2026 09:26
A client with the secret key loads flag definitions from its host with
it, so adding the key to the app's client behind a reverse proxy hands
it to the proxy. The skill now gives email its own client on PostHog's
host behind a proxy, and sets the app host only on a self-hosted
instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog records a send's engagement events only when the project's
capture_workflows_engagement_events setting is on, for code sends as
for template sends. The skill said a code send records them by
default, and checked the setting only on the template path. It now
checks it for every send, and says a template send takes none of the
code send's fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus Silthus added the stamphog Request AI approval (no full review) label Nov 5, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

Only instructional documentation changes; no executable API, security, or write-path behavior changes. Release readiness remains unverified: the checkout still requires a template ID and the SDK minimum versions remain placeholders, so confirm the stated rollout prerequisites before publishing.

Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 0L, 0F substantive, 208L/4F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (208L, 4F, single-area, docs)
stamphog 2.7.0 .stamphog/policy.yml @ 2c9d269 · reviewed head 2c9d269

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation stamphog Request AI approval (no full review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant