Skip to content

feat(workflows): edit a transactional template's sender from the editor - #121079

Merged
trunk-io[bot] merged 1 commit into
posthog/transactional-email-sender-apifrom
posthog/transactional-email-sender-reply-to
Oct 22, 2026
Merged

trunk-io[bot] merged 1 commit into
posthog/transactional-email-sender-apifrom
posthog/transactional-email-sender-reply-to

Conversation

@mayteio

@mayteio mayteio commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • A transactional email can't choose who it comes from, and the editor has nowhere to set a sender name, address or reply-to.
  • Moving PostHog's own emails off Customer.io needs this. A security email should come from a security address and take replies there.

This is the editor half. The API that stores and checks the sender is #121871, the base of this stack.

Changes

  • The transactional template editor shows the sender as one line, "From Name ", with "Replies go to …" when a reply-to is set.
  • "Edit sender" opens a popover with sender, display name, address and reply-to. Empty fields show the sender's own values as hints.
  • The address field takes only the part before the @, and the sender's verified domain is fixed after it. Switching sender moves a custom address to the new domain.
  • An off-domain, malformed or too-long custom address shows an error inline, on the summary line, and blocks Publish.
  • Right after a template becomes transactional, the editor shows the sender the server stored.
  • Test sends default to the template's sender and keep its custom name and address.
  • Applying a transactional template inside a workflow email step keeps that step's own sender and reply-to.
  • Library templates (not transactional) and the workflow email step look the same as before.

Demo (local stack, narrated): transactional sender demo

Before After
before sender summary
Edit sender Wrong domain
edit sender wrong domain
The received email (local MailDev)

received

How did you test this code?

Manual, on a local stack with two verified MailDev senders:

  1. Made a template transactional from the UI. The default sender was stored and shown.
  2. In the popover, switched sender and set a display name, address and reply-to. Publish refused an off-domain address with one message, then published after the fix.
  3. Sent through the API with a project secret key. MailDev received From: Hedgebox Security Team <security-team@posthog.com> and Reply-To: security@posthog.com.
  4. Rendered the editor at 1440px and 820px.

Test rationale:

  • emailSenderFields.test.ts tables cover the address check, including the 254-character bound, and the local-part split and join. They are pure functions, so the cases stay cheap.
  • emailTemplaterLogic.test.ts: applying a transactional template keeps the step's own sender and reply-to.
  • messageTemplateLogic.test.ts: conversion shows the stored sender, with or without an edit made meanwhile.
  • messageTemplateTestSendLogic.test.ts: a test send uses the template's sender, name and address.
  • Each of these fails without its change. The API tests are in feat(messaging): store a sender and reply-to on transactional templates #121871.

Not checked:

  • A real SES send. Production SES already sets reply-to, so only MailDev was exercised. MailDev needed fix(cdp): pass reply-to to the local maildev transport #121872 to show the reply-to.
  • Publishing within about a second of the last edit leaves a duplicate draft after publish, from the existing draft autosave. It doesn't affect sends, and this PR doesn't cause it.

Release status

  • This change is behind a feature flag and is not available to users

Transactional templates sit behind workflows-transactional-email-api.

Docs update

None. No existing doc covers transactional template settings.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: PostHog Desktop with Claude Code, Claude Opus 5.5 (claude-opus-5-5)


Created with PostHog Desktop

🤖 Generated with Claude Code

@mayteio mayteio added the cross-lane-change Allow for cross lane (i.e. django + rust) changes to merge at once label Oct 21, 2026
@mayteio mayteio self-assigned this Oct 21, 2026
@trunk-io

trunk-io Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

😎 Stack merged successfully - details.

@github-actions

github-actions Bot commented Oct 21, 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.

⚠️ Complexity (TypeScript) — 5 functions above the limit (max 61)

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

Function Location Complexity Limit
MessageTemplate products/workflows/frontend/TemplateLibrary/MessageTemplate.tsx:58 61 10
NativeEmailTemplaterForm frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx:532 15 10
NativeEmailIntegrationChoice frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx:269 13 10
EmailSenderSummary frontend/src/scenes/hog-functions/email-templater/EmailSenderSummary.tsx:13 12 10
<anonymous> frontend/src/scenes/hog-functions/email-templater/emailTemplaterLogic.tsx:927 12 10
✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

⚠️ Comment density — 4% of added code lines are comments (17 of 463)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
frontend/src/scenes/hog-functions/email-templater/emailSenderFields.ts 5 57
frontend/src/scenes/hog-functions/email-templater/emailTemplaterLogic.tsx 3 15
frontend/src/scenes/hog-functions/email-templater/EmailSenderForm.tsx 2 109
frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx 2 60
products/workflows/frontend/TemplateLibrary/messageTemplateLogic.ts 2 42
frontend/src/scenes/hog-functions/email-templater/emailSenderFields.test.ts 1 37
products/workflows/frontend/TemplateLibrary/messageTemplateTestSendLogic.test.ts 1 14
products/workflows/frontend/TemplateLibrary/messageTemplateTestSendLogic.ts 1 18

This check does not block merging. It updates on every push and clears when the share drops.

⚠️ Bundle size — 🔺 +554 B (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 75.83 MiB · 🔺 +554 B (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

⚠️ Eager graph — 2 over budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.01 MiB · 24 files no change █████████░ 88.6% of 1.14 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
2.93 MiB · 615 files no change ██████████ 96.8% of 3.03 MiB
🟡 authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.59 MiB · 2,580 files no change ██████████ 101.2% of 7.50 MiB
dashboard scene
src/scenes/dashboard/Dashboard.tsx
9.61 MiB · 3,646 files no change ██████████ 96.0% of 10.01 MiB
🟡 today home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
7.99 MiB · 2,919 files no change ██████████ 103.3% of 7.74 MiB
events scene
src/scenes/activity/explore/EventsScene.tsx
9.09 MiB · 3,437 files no change █████████░ 94.8% of 9.59 MiB
web analytics scene
src/scenes/web-analytics/WebAnalyticsScene.tsx
9.93 MiB · 3,652 files no change █████████░ 91.7% of 10.83 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
8.16 MiB · 3,021 files no change █████████░ 86.2% of 9.47 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/zod/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/billing/billingLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/ProductEmptyState/productSetupPreloadLogic.ts stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/dashboards/frontend/widgets/previews/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/wizard/frontend/runs/WizardRunDetailsDrawer.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 zod/v4/locales/de.js stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/queries/validators.js stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/scenes/project-homepage/today/TodayReportPage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/queries/Query/Query.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/validators.js stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/scenes/web-analytics/LiveMetricsDashboard/LiveWebAnalyticsMetrics.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 products/product_analytics/frontend/insights/trends/WorldMap/countryVectors.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 src/scenes/settings/SettingsMap.tsx stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 @jspm/core/nodelibs/browser/fs stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.4 KiB src/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
1.3 KiB src/lib/utils/bootTiming.ts
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
69.4 KiB src/lib/lemon-ui/icons/icons.tsx
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
29.4 KiB src/scenes/scenes.ts
26.8 KiB src/lib/constants.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
143.2 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
143.2 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
143.2 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/activity/explore/EventsScene.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
143.2 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/web-analytics/WebAnalyticsScene.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
183.2 KiB src/queries/validators.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
143.2 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Size File
321.6 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
315.9 KiB ../node_modules/.pnpm/posthog-js@1.438.12_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
280.7 KiB src/taxonomy/core-filter-definitions-by-group.json
238.5 KiB ../packages/quill/packages/quill/dist/index.js
220.9 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.8 KiB src/products.tsx
98.3 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

✅ Toolbar bundle — eager 2.28 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.28 MiB · 19 files no change ████░░░░░░ 39.9% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 40 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
898.5 KiB dist/toolbar/toolbar-app-LIRVHRPD.css
691.0 KiB dist/toolbar/chunk-chunk-HVEQZXL4.js
258.3 KiB dist/toolbar/chunk-chunk-YUTUHJHZ.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
130.4 KiB dist/toolbar/chunk-chunk-MGILAAF6.js
74.8 KiB dist/toolbar/toolbar-app-F7Q25OJS.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-CWKXICRU.js
21.7 KiB dist/toolbar/chunk-chunk-XK3VNHDG.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

✅ Dist folder size — 🔺 +71.9 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1016.12 MiB · 🔺 +71.9 KiB (+0.0%)

ℹ️ MCP UI apps size — 34 app(s), 27827.8 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 840.0 KB 208.3 KB
action 764.0 KB 208.3 KB
action-list 831.0 KB 208.3 KB
cohort 763.0 KB 208.3 KB
cohort-list 829.9 KB 208.3 KB
email-template 762.9 KB 208.3 KB
error-details 767.9 KB 208.3 KB
error-issue 764.4 KB 208.3 KB
error-issue-list 831.6 KB 208.3 KB
experiment 828.1 KB 208.3 KB
experiment-list 831.7 KB 208.3 KB
experiment-results 830.6 KB 208.3 KB
feature-flag 830.3 KB 208.3 KB
feature-flag-list 834.1 KB 208.3 KB
feature-flag-testing 766.3 KB 208.3 KB
inline-scan 763.5 KB 208.3 KB
insight-actors 829.4 KB 208.3 KB
llm-costs 826.7 KB 208.3 KB
session-recording 765.3 KB 208.3 KB
survey 764.6 KB 208.3 KB
survey-global-stats 826.8 KB 208.3 KB
survey-list 831.6 KB 208.3 KB
survey-stats 826.8 KB 208.3 KB
trace-span 763.4 KB 208.3 KB
trace-span-list 830.9 KB 208.3 KB
vision-observation-list 830.0 KB 208.3 KB
workflow 763.4 KB 208.3 KB
workflow-list 830.3 KB 208.3 KB
workflow-setup-status 763.0 KB 208.3 KB
query-results 1081.8 KB 208.3 KB
render-ui 1156.7 KB 208.3 KB
visual-review-snapshots 767.9 KB 208.3 KB
workflow-data-suggestions 765.7 KB 208.3 KB
workflow-sending-rules 764.4 KB 208.3 KB
ℹ️ MCP agent API — agent-facing tool changes

What this PR changes for agents, from the tool schema snapshots and tool definitions.

Tools changed (2):

Tool Params Scopes Annotations Schema chars
workflows-create-email-template 3,354 -> 4,347
workflows-update-email-template 3,440 -> 4,433
✅ Playwright — all passed

All tests passed.

View test results →

@trunk-io

trunk-io Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
Scenes-App/Experiments ExperimentWithHealthFindings play-test The Storybook test could not find the text “Why?” because the experiment page remained stuck loading. Logs ↗︎
Scenes-App/Project Homepage ProjectHomepage smoke-test The test timed out while waiting for loading indicators to disappear. Logs ↗︎
Scenes-App/People/Person PersonNotFound smoke-test The test timed out while waiting for loading indicators or spinners to disappear. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

@mayteio

mayteio commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@mayteio PostHog Review is reviewing this pull request.

@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 7265a6b: 1 Must fix, 3 Should fix.

Comment on lines +198 to +202
fields["from"] = EmailTemplateSenderSerializer(
required=False,
help_text="The sender a transactional template sends from. A template made transactional without "
"one gets the sender it was made transactional with.",
)

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.

P1 · Preserve creation of templates with an empty sender

Creating a blank library template sends NEW_TEMPLATE.content.email.from === '' through saveTemplate unchanged. The new nested serializer rejects this with “Expected a dictionary, but got str”, so ordinary template creation now returns 400 even with transactional emails disabled. Normalize the empty sender away or update all existing authoring paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dde6371. The email serializer treats an empty or null from as no sender, so a blank template from the editor creates again. I reproduced the 400 in the UI first. The workflow sync also ignores a legacy non-object value. test_create_email_template_with_subject_succeeds now sends the editor's from: '' shape.

Comment on lines +158 to +160
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
if not Integration.objects.filter(
team_id=self.context["team_id"], kind="email", id=data["integrationId"]

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.

P2 · Validate missing integration IDs safely on PATCH

A PATCH containing content.email.from: {} or a sender with only name raises KeyError('integrationId') here. DRF propagates partial=True into nested serializers, so the required integer field does not reject its omission before validate runs. The endpoint returns 500 instead of a validation error; explicitly reject a missing integration ID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dde6371. The sender serializer now checks for a missing integrationId itself and returns a 400, "Choose an email sender." A new sender_without_an_id case is in test_rejects_an_update_that_would_stop_a_transactional_template_sending.

Comment on lines +101 to +102
sender_id = resolve_transactional_email_sender(team_id, from_integration_id or sender.get("integrationId"))
template.content = {**template.content, "email": {**email, "from": {**sender, "integrationId": sender_id}}}

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.

P2 · Merge the resolved sender into the editor after conversion

Converting a template now changes its content, but the frontend makeTransactional listener merges only transactional_hog_flow_id and updated_at from the response. The editor therefore shows “Choose sender” despite the configured sender, and editing its name produces a sender without an integration ID. Merge the returned sender into form state during conversion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dde6371. The makeTransactional listener merges the returned sender into both the baseline and the form, with or without an edit made during conversion. The conversion test is now a test.each over both paths.

from={from}
replyTo={emailTemplate.replyTo}
senders={senders}
onChangeFrom={(next) => setEmailTemplateValue('from', next)}

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.

P2 · Carry custom sender overrides into test emails

After setting a custom name or address here, clicking “Send test” sends neither override. messageTemplateTestSendLogic.sendTestEmail spreads the template content, then replaces from with only the test integration ID, so the received message uses the integration's default identity. Preserve the template's name and address when constructing the test sender.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dde6371. A test send defaults to the template's own sender and keeps its custom name and address. The send test now sets a non-default sender with overrides and asserts all three reach the email step.

@mayteio mayteio added the reviewhog label Oct 22, 2026 — with PostHog

@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 dde6371: 1 Must fix, 3 Should fix.

This Standard review started from the posthog-review label. Comment @posthog review --deep for a Deep review, or @posthog help to see all commands.

Comment on lines +206 to +207
fields["from"] = EmailTemplateSenderSerializer(
required=False,

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.

P1 · Normalize legacy senders when serializing stored templates

For a stored legacy content.email.from: "", list/retrieve raises AttributeError when EmailTemplateSenderSerializer reads its required integrationId. to_internal_value only normalizes incoming writes, so one existing row breaks the library listing even with transactional emails disabled. Normalize legacy senders during representation or migrate them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #121871 (23588b8), which now holds the API half of this stack. Reads now drop a stored from that is a string, empty, or has no integration ID, so a legacy row can't break the list or detail view. test_retrieve_message_template lists and reads each legacy shape.

Comment on lines +198 to +202
def to_internal_value(self, data: Any) -> Any:
# New templates and templates saved before senders existed hold an empty string, which means no sender.
if isinstance(data, dict) and data.get("from") in ("", None):
data = {key: value for key, value in data.items() if key != "from"}
return super().to_internal_value(data)

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.

P2 · Preserve saving templates from destination email editors

When a Mailgun destination uses “Save as new template”, saveAsTemplate forwards its nonempty string from unchanged. This normalization handles only "" and None, so the new nested serializer returns 400 and breaks the existing template-saving flow. Strip destination sender strings when constructing library content, or accept them here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #121871. A write treats any string from as no sender, so a destination's "Save as new template" saves again. test_create_email_template_with_subject_succeeds now covers the editor's blank value and a destination sender string.



# Matches the send path, which ignores a custom address that fails these checks and sends from the sender's own.
SENDER_ADDRESS_PATTERN = re.compile(r'^[^\s@"<>,;]+@[^\s@"<>,;]+\.[^\s@"<>,;]+$')

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.

P2 · Bound sender addresses before running the regex

Publishing from.email containing a@, thousands of dots, and a trailing @ triggers quadratic backtracking in SENDER_ADDRESS_PATTERN. The new CharField has no length limit: 16,000 dots take about 0.7 seconds locally, and larger inputs occupy a request worker for much longer. Bound the address length before matching or use linear-time validation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #121871. The address field is capped at 254 characters, and the check returns before the regex for anything longer, in the API and the editor alike. A new custom_address_too_long case covers it.

Comment on lines +107 to +110
sender_id = resolve_transactional_email_sender(team_id, from_integration_id or sender.get("integrationId"))
template.content = {**template.content, "email": {**email, "from": {**sender, "integrationId": sender_id}}}
hog_flow_id = create_transactional_email_workflow(
team_id=team_id, user_id=user_id, content=_content(template, from_integration_id)
team_id=team_id, user_id=user_id, content=_content(template)

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.

P2 · Validate the custom address against the resolved conversion sender

When a template has a custom address on sender A's domain, calling make_transactional with sender B's from_integration_id retains A's address while replacing its integration. Conversion bypasses sender_address_error, so it succeeds and sends silently use B's default address. Validate the resulting sender/address pair before creating the workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #121871. Conversion now checks the custom address against the sender it resolved and rejects it with template_not_sendable when the domains differ. A new case is in test_rejects_making_a_template_transactional_it_cannot_send.

@posthog posthog Bot removed the reviewhog label Oct 22, 2026
@mayteio
mayteio force-pushed the posthog/transactional-email-sender-reply-to branch from dde6371 to 05fd57c Compare October 22, 2026 07:36
@mayteio mayteio removed the cross-lane-change Allow for cross lane (i.e. django + rust) changes to merge at once label Oct 22, 2026
@mayteio
mayteio changed the base branch from master to posthog/transactional-email-sender-api October 22, 2026 07:36
@mayteio
mayteio added this pull request to stack #121873 October 22, 2026 07:37
@mayteio mayteio changed the title feat(messaging): let transactional templates set their own sender and reply-to feat(workflows): edit a transactional template's sender from the editor Oct 22, 2026
- The transactional template editor shows the sender as one summary line, with an Edit sender popover for sender, display name, address and reply-to.
- The address field takes the part before the @, and the sender's verified domain is fixed after it.
- An invalid address shows inline and blocks Publish.
- Making a template transactional shows the stored sender, and test sends keep the template's sender, name and address.
- Applying a transactional template inside a workflow email step keeps the step's own sender and reply-to.

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

Generated-By: PostHog Desktop
Task-Id: 90d9a51d-beba-47ba-8f50-b351327676f9
@mayteio
mayteio force-pushed the posthog/transactional-email-sender-reply-to branch from 05fd57c to 877fa19 Compare October 22, 2026 08:01
@mayteio
mayteio marked this pull request as ready for review October 22, 2026 08:20
@mayteio mayteio added the stamphog Request AI approval (no full review) label Oct 22, 2026 — with PostHog
@parameterai

parameterai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Risk: No findings

This PR adds the frontend editor for a transactional email template's sender (from-name, address, reply-to) — a sender summary line with an edit popover, client-side address validation mirroring the server's checks, sender carry-over on template apply, and sender defaults for test sends. It is frontend-only; the storing and validating API is the stacked base PR already in pr-base. The main risk considered was a client/server validation differential, but the frontend regex, length bound, templating escape, and domain derivation exactly mirror the server's sender_address_error, and the server re-validates on save and send, so the client checks are UX only.

Sentinel reviewed 877fa19 · Review settings

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 22, 2026 08:21

@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.

Risky sender configuration changes have owning-team and STRONG-familiarity assurance, with targeted regression tests. The current code addresses the substantive review concerns, including the remaining unmarked thread; neither historical reviews nor the bot's scanning announcement constitute a current-head review.

  • Author wrote 83% of the modified lines and has 4 merged PRs in these paths (familiarity STRONG).
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 468L, 8F substantive, 576L/12F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (576L, 12F, two-areas, feat)
stamphog 2.6.0 .stamphog/policy.yml @ 877fa19 · reviewed head 877fa19

mayteio commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

/trunk merge

@trunk-io
trunk-io Bot merged commit 3f7b986 into master Oct 22, 2026
359 of 367 checks passed
@trunk-io
trunk-io Bot deleted the posthog/transactional-email-sender-reply-to branch October 22, 2026 09:43
@deployment-status-posthog

deployment-status-posthog Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-10-22 10:06 UTC Run
prod-us ✅ Deployed 2026-10-22 10:20 UTC Run
prod-eu ✅ Deployed 2026-10-22 10:22 UTC Run

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

Labels

stamphog Request AI approval (no full review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant