Skip to content

feat(messaging): upload email templates from code by key - #127169

Draft
Silthus wants to merge 15 commits into
email-v2/04a-template-code-fieldsfrom
email-v2/04-template-uploads
Draft

Silthus wants to merge 15 commits into
email-v2/04a-template-code-fieldsfrom
email-v2/04-template-uploads

Conversation

@Silthus

@Silthus Silthus commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Moved from Silthus#573.

Part of the code-first rebuild of the email send stack; replaces parts of #118924 (template keys, owners and hashes; upload, lock and adopt).

Problem

  • A team that keeps its email templates as files in its repository can't publish them to PostHog. The only way in is the PostHog editor, so the repository copy and the PostHog copy drift.
  • The code sends below need a template a key names, and a way to tell which copy is current.
  • Map: Silthus/posthog#461.

Impact

Pays into Grow Workflows through billed workflows_emails actions per paying org, once code-managed templates send. In scope: Messaging, transactional email templates. Expected move: up, after the flag turns on.

Merge Danger

Warning

🟡 Merge risk: medium · two-way door: a revert removes the upload route; uploaded templates stay as ordinary templates

  • The upload route and the MCP upload tool sit behind workflows-email-from-code. This PR adds no migration; feat(messaging): add code-managed fields to message templates #127168 adds the columns.
  • An upload publishes changed content at once, so the next send delivers it. A wrong drift check either republishes on every deploy or leaves a workflow that someone edited outside the template.
  • An upload under a key that a template made in PostHog already holds updates that template and keeps its id, workflow and sender.

Changes

Kind Files Lines
Production 13 +966 −159
Tests 2 +520 −4
Generated types and snapshots 10 +674 −6

What a caller can now do

  • PUT /api/projects/:id/messaging_templates/by_key/{key}/ uploads a template kept as an HTML file: name, subject, html, optional text, preheader and source (repository, path, ref).
  • The first upload creates a transactional template with the project's default verified sender.
  • A later upload with changed content publishes it at once. Unchanged content answers unchanged.
  • An upload also repairs the template's workflow when its email step no longer matches the upload: an edit to the body, the recipient, the sender or the reply-to made outside the template.
  • if_not_superseded keeps an older copy of the file from replacing newer content; that answer is superseded, and nothing changes. The check covers the last 100 different contents the template replaced.
  • An upload that changes only line endings answers unchanged, because it compares content hashes.
  • A first upload under a key whose code sends are paused answers 400 transactional_email_paused and names the key workflow to resume.
  • Each refusal names the field, why PostHog refuses it, and what to change (why, fix, docs_url).
  • Uploads have their own rate limit, so a deploy that uploads many templates leaves the send budget alone.
  • An agent does the same through the new MCP tool workflows-upload-email-template.

How it works

Mechanical: the regenerated OpenAPI, frontend and MCP types and the tool snapshots.

How did you test this code?

Test seams: the upload route through the DRF client (test_template_upload.py), the code send route next to it (test_transactional_email.py), and the hash (test_content_hash.py, in #127168).

Test rationale:

  • A first upload that sends nothing, a changed upload that does not publish, and an older copy that replaces newer content under if_not_superseded.
  • test_an_upload_repairs_what_no_longer_matches_the_uploaded_content: a workflow whose body, recipient or sender someone edited outside the template, which an unchanged upload leaves as it is.
  • test_refuses_a_first_upload_under_a_key_whose_code_sends_are_paused: an upload that silently resumes paused code sends.
  • An upload with other line endings that publishes a version, two racing first uploads that make two templates, and uploads that use up the send rate limit.
  • The flag rows: the upload route answering with only workflows-transactional-email-api on.

Proofs on this head 6df2894 (saved with fleet proof):

  • Live run: Django at this head, cdp-api and both CDP workers at the node stack f9abed0, maildev as the sink. 21 requests through the public API: uploads by key (created, unchanged, updated, superseded), an upload under the key of a template made in PostHog, sends by id and by key, and code sends.
  • Shadow diff: the same 21 requests on master 43333f8. The upload answers, the new template fields and the upload usage events differ, as the goal names. An edit in PostHog of a code-managed template still answers 200 here; feat(messaging): lock code-managed templates and adopt templates into code #127172 locks it.
  • Prod shape: a gap, accepted by the standing rule until read-only production access exists on 2026-11-02.
  • Lane gate on this head: ruff, the IDOR check, the migration checks, repo-wide mypy, and the messaging and workflows tests pass, and so do the MCP server's typecheck, lint and unit tests.
Earlier proofs, at 3e24e8b before the split into #127168, #127169 and #127172
  • Live run at 3e24e8b, cdp-api and workers from feat(cdp): deliver a code send's content on its own transactional email route #127122 at 24644bc, curl into maildev. The workflow that ran each job comes from a row-level audit of the job queue (saved with fleet proof):

    Case Answer Delivered Ran on
    Upload under a new key 201 created – –
    The same content again 200 unchanged – –
    Code send under that key 202 the caller's content the template's workflow
    Code send under a key no template has 202 the caller's content the key's workflow
    Upload under that used key, then a code send 201 created, 202 the caller's content the template's workflow
    Code send under key K2, then a transactional template made in PostHog takes K2 by PATCH, then a code send 202, 200, 202 the caller's content the key's workflow both times
    Template by id (control) 202 the stored template –
  • Base-written data: code sends under two keys and a template made in PostHog with Django at feat(workflows): send an email with inline html from an MCP tool #127167, then Django at 3e24e8b after its migration:

    Case after the migration Answer Ran on
    Code send under the first key 202, delivered the same key workflow as before
    Upload under the second key, then a code send 201 created, 202, delivered the template's workflow
    The earlier template by id 202, delivered its own workflow
    Upload under a new key 201 created –
  • Prod shape: a gap until read-only production access exists on 2026-11-02.

Release status

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

Docs update

None in this PR. The send API docs follow when the flag opens.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

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

Question Answer Who Where
Is managed_by nullable? Yes; null means made in PostHog 👤 Human map Silthus#461, lane brief of 2026-10-25
Does the create schema list transactional_email_keys? Yes, and the validator refuses it 👤 Human map Silthus#461, lane brief of 2026-10-25
Which flag gates uploads? workflows-email-from-code, per organization 👤 Human map Silthus#461, 2026-10-25
May the code carry comments? No; names carry the reasons 👤 Human map Silthus#461, 2026-10-25
Do release and adoption check the new flag? No; gating release would strand code-managed templates when the flag is off, and adoption does nothing until a gated upload or send 🤖 Agent pending approval
Does this land as one PR? No, as three: schema (#127168), upload, lock and adopt (#127172); replaced one PR 👤 Human lane brief of 2026-10-31
Does an upload repair a workflow edited outside the template? Yes, any change to its email step, recipient and sender included 🤖 Agent QA swarm 2026-11-01
What does a first upload under a paused key do? Answers 400 transactional_email_paused and names the workflow to resume, instead of resuming code sends 🤖 Agent QA swarm 2026-11-01
Do code sends move to a template when someone gives it a key? No, only once an upload claims the key (managed_by set); a release keeps them on the template 🤖 Agent pending approval
Can the hog_flow API edit a code-managed template's workflow? Yes, for now; a follow-up that refuses it is open for Michael 🤖 Agent pending approval

Open: may the hog_flow update, graph, action-email and publish paths change a code-managed template's workflow? Recommended: a follow-up PR refuses those writes with immutable, and an upload repairs any drift.

Involvement: one brief from Michael with decisions; one review round (GPT-6.1 Sol: review, design and Michael's lens; Opus fresh eyes) with its fixes; QA swarm on 2026-11-01 with two panel rounds and two final reads, the last clean: 7 findings fixed, 2 rejected; live and shadow-diff proof captured.

🤖 Generated with Claude Code

Silthus and others added 9 commits October 31, 2026 11:09
`PUT /api/projects/:id/messaging_templates/by_key/:key/` creates or
updates the template with that key from HTML kept in a repository. The
first upload makes it transactional with the project's default verified
sender; later uploads publish changed content at once, answer
`unchanged` for the same content and, with `if_not_superseded`,
`superseded` for content a newer upload replaced. Code sends under a
key move to the template's workflow once an upload claims the key.

The upload sits behind `workflows-email-from-code`, keeps the PSAK
bypass and workflow access checks of the send route, gets its own rate
limits, and reports a usage event. The MCP tool
`workflows-upload-email-template` calls it.

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

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

# Conflicts:
#	products/messaging/frontend/generated/api.schemas.ts
#	products/messaging/frontend/generated/api.zod.ts
#	services/mcp/src/api/generated.ts
#	services/mcp/src/generated/email_templates/api.ts
#	services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-send-email.json
…ode sends paused

An upload counts as unchanged only when the template's workflow still sends
the uploaded content, so a re-upload repairs a workflow edited elsewhere.
A first upload under a key whose code sends are paused is refused, because
taking the key over would start those sends again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code send under the key of a template no upload claimed stays on the
key's own workflow, and a test now pins that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upload compared only the email's content with the workflow's, so a
workflow edited to send elsewhere still reported "unchanged". It now
compares the workflow's whole email step with the one the upload writes.

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

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

github-actions Bot commented Nov 2, 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) — clean

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.

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

✅ Bundle size — 🟢 -586 B (-0.0%)

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

Total: 77.12 MiB · 🟢 -586 B (-0.0%)

File Size Δ vs base
posthog-app/src/lib/components/CommandKSearch/CommandKSearch.js 63.0 KiB 🟢 -1.2 KiB (-1.9%)

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

✅ Eager graph — within 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.03 MiB · 25 files no change █████████░ 89.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
3.00 MiB · 623 files 🔺 +1.2 KiB (+0.0%) ██████████ 99.0% of 3.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
3.82 MiB · 1,222 files 🔺 +1.2 KiB (+0.0%) ██████████ 95.4% of 4.01 MiB
dashboard scene
src/scenes/dashboard/Dashboard.tsx
7.58 MiB · 2,930 files 🔺 +1.2 KiB (+0.0%) ████████░░ 77.1% of 9.84 MiB
today home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
4.69 MiB · 1,749 files 🔺 +1.2 KiB (+0.0%) ██████████ 95.0% of 4.93 MiB
AI-first home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
5.98 MiB · 2,256 files 🔺 +1.2 KiB (+0.0%) ██████████ 95.3% of 6.27 MiB
events scene
src/scenes/activity/explore/EventsScene.tsx
6.66 MiB · 2,508 files 🔺 +1.2 KiB (+0.0%) ███████░░░ 70.8% of 9.41 MiB
web analytics scene
src/scenes/web-analytics/WebAnalyticsScene.tsx
7.68 MiB · 2,870 files 🔺 +1.2 KiB (+0.0%) ███████░░░ 73.2% of 10.49 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
6.89 MiB · 2,599 files 🔺 +709 B (+0.0%) ████████░░ 84.8% of 8.13 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 products/product_analytics/frontend/insights/activityDescriptions 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/lib/logic/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/kea-test-utils/ 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
🟢 products/product_analytics/frontend/insights/activityDescriptions stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/kea-test-utils/ 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
🟢 src/layout/panel-layout/ProjectTree/menus/SessionReplayMenuItems.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/navigation/projectNoticeLogic.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/navigation/ImpersonationNotice/ImpersonationNotice.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/cohorts/cohortSavedToast.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/wizard/frontend/runs/WizardRunDetailsDrawer.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodayShell.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodayContextPanelFloating.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodaySidePanel.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodayTopBar.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/todayRailItems.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/subscriptions/frontend/components/Subscriptions/exportNudge/exportNudgeLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/billing/paymentEntryLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/conversations/frontend/components/SidePanel/sidepanelTicketsLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/zod/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/authentication/auth/loginLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/authentication/shared/passkeyLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/cohorts/CohortFilters/constants.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/persons/frontend/logics/personsLogic.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/surveys/constants.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@tiptap/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/prosemirror- stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/re2js/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/react-markdown/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/Command/Command.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 packages/quill/packages/ 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
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/@tiptap/ stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/prosemirror- stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/marked/ stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/frimousse/ stays out of src/scenes/dashboard/Dashboard.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/queries/web-analytics-validators.js stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/re2js/ stays out of src/scenes/dashboard/Dashboard.tsx
🟢 node_modules/kea-test-utils/ stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/scenes/debug/HogDebug.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/queries/QueryEditor/QueryEditor.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.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
🟢 packages/quill/packages/charts/ stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 products/posthog_ai/frontend/components/tool/widgets/VisualizationWidget.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 products/posthog_ai/frontend/components/tool/widgets/RecordingsWidget.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 src/queries/Query/Query.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 products/posthog_ai/frontend/api/primitives.ts stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 src/scenes/notebooks/Notebook/notebookLogic.ts stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/@tiptap/ stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/prosemirror- stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/marked/ stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/frimousse/ stays out of src/scenes/activity/explore/EventsScene.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/queries/web-analytics-validators.js stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/re2js/ stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 node_modules/kea-test-utils/ stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/nodes/InsightViz/InsightViz.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/nodes/DataVisualization/DataVisualization.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/nodes/WebOverview/WebOverview.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/scenes/debug/HogDebug.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/QueryEditor/QueryEditor.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/@tiptap/ stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/prosemirror- stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/marked/ stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/frimousse/ stays out of src/scenes/web-analytics/WebAnalyticsScene.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/queries/validators.js stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/re2js/ stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 node_modules/kea-test-utils/ stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 src/scenes/debug/HogDebug.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 src/queries/QueryEditor/QueryEditor.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 packages/quill/packages/quill/dist/ stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 node_modules/@tiptap/ stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 node_modules/prosemirror- stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 node_modules/marked/ stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.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
🟢 node_modules/re2js/ stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 node_modules/kea-test-utils/ stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 node_modules/d3/src/ 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.7 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
4.0 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.9 KiB src/lib/utils/bootTiming.ts
1.7 KiB src/index.tsx
1.7 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.2 KiB src/RootErrorBoundary.tsx
1.1 KiB src/lib/utils/deferredAppContext.ts
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
105.6 KiB src/products.tsx
87.4 KiB src/lib/api.ts
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
41.9 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
36.4 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
28.3 KiB src/lib/constants.tsx
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
105.6 KiB src/products.tsx
87.4 KiB src/lib/api.ts
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
41.9 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
36.4 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
32.3 KiB src/lib/utils/eventUsageLogic.ts
28.3 KiB src/lib/constants.tsx
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
115.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/surveys-preview.js
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
96.1 KiB ../packages/quill/packages/components/dist/index.js
87.4 KiB src/lib/api.ts
79.0 KiB src/scenes/dashboard/dashboardLogic.tsx
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
87.4 KiB src/lib/api.ts
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
68.0 KiB ../products/posthog_ai/frontend/logics/runStreamLogic.ts
41.9 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
36.4 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
87.4 KiB src/lib/api.ts
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
68.0 KiB ../products/posthog_ai/frontend/logics/runStreamLogic.ts
41.9 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
41.7 KiB src/scenes/hog-functions/sub-templates/sub-templates.ts
Largest files eagerly shipped from src/scenes/activity/explore/EventsScene.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
96.1 KiB ../packages/quill/packages/components/dist/index.js
87.4 KiB src/lib/api.ts
79.0 KiB src/scenes/dashboard/dashboardLogic.tsx
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
68.0 KiB ../products/posthog_ai/frontend/logics/runStreamLogic.ts
Largest files eagerly shipped from src/scenes/web-analytics/WebAnalyticsScene.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
115.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/surveys-preview.js
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
96.1 KiB ../packages/quill/packages/components/dist/index.js
87.4 KiB src/lib/api.ts
79.0 KiB src/scenes/dashboard/dashboardLogic.tsx
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Size File
359.8 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
321.1 KiB ../node_modules/.pnpm/posthog-js@1.447.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
224.1 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
110.8 KiB ../packages/quill/packages/primitives/dist/index.js
105.6 KiB src/products.tsx
96.1 KiB ../packages/quill/packages/components/dist/index.js
87.4 KiB src/lib/api.ts
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
68.0 KiB ../products/posthog_ai/frontend/logics/runStreamLogic.ts

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

✅ Frontend layers — no new upward imports, 0 removed

Static imports from a lower frontend layer into a higher one (layers, lowest first: kernel, shell, query, features). Compared with the base branch, so an import that a PR also adds to layer-baseline.txt still shows here. Sizes are the source bytes on the eager path of every logged-in page.

Layer Eager source bytes Δ vs base
kernel 2.97 MiB 🟢 -184 B (-0.0%)
shell 2.35 MiB no change
query 56.2 KiB no change
features 273.6 KiB no change

Download the interactive layer graph for this PR.

Posted automatically by check-layers

✅ Toolbar bundle — eager 2.42 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.42 MiB · 19 files 🔺 +1.2 KiB (+0.0%) ████░░░░░░ 42.3% of 5.72 MiB
Deferred (lazy) 2.61 MiB · 42 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
961.3 KiB dist/toolbar/toolbar-app-F4WAIZ6W.css
731.7 KiB dist/toolbar/chunk-chunk-N27OD2N3.js
290.5 KiB dist/toolbar/chunk-chunk-B4EIGQIG.js
131.8 KiB dist/toolbar/chunk-chunk-FQJO25M6.js
130.4 KiB dist/toolbar/chunk-chunk-3F26PAMR.js
75.7 KiB dist/toolbar/toolbar-app-KS35NDNZ.js
69.0 KiB dist/toolbar/chunk-chunk-BFAXZMC5.js
33.8 KiB dist/toolbar/chunk-chunk-43ZVAZS7.js
21.7 KiB dist/toolbar/chunk-chunk-F6URCRZB.js
6.8 KiB dist/toolbar/chunk-chunk-RVLGRPCH.js

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

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

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

Total: 985.20 MiB · 🔺 +12.8 KiB (+0.0%)

ℹ️ MCP UI apps size — 35 app(s), 29268.6 KB JS

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

App JS CSS
debug 848.8 KB 220.9 KB
explain-chart 1122.9 KB 220.9 KB
action 771.5 KB 220.9 KB
action-list 839.8 KB 220.9 KB
cohort 770.5 KB 220.9 KB
cohort-list 838.8 KB 220.9 KB
email-template 770.4 KB 220.9 KB
error-details 775.4 KB 220.9 KB
error-issue 772.5 KB 220.9 KB
error-issue-list 841.0 KB 220.9 KB
experiment 836.9 KB 220.9 KB
experiment-list 840.5 KB 220.9 KB
experiment-results 838.7 KB 220.9 KB
feature-flag 839.2 KB 220.9 KB
feature-flag-list 842.9 KB 220.9 KB
feature-flag-testing 773.8 KB 220.9 KB
inline-scan 771.0 KB 220.9 KB
insight-actors 838.3 KB 220.9 KB
llm-costs 835.6 KB 220.9 KB
session-recording 772.8 KB 220.9 KB
survey 772.1 KB 220.9 KB
survey-global-stats 835.6 KB 220.9 KB
survey-list 840.5 KB 220.9 KB
survey-stats 835.6 KB 220.9 KB
trace-span 770.9 KB 220.9 KB
trace-span-list 839.7 KB 220.9 KB
vision-observation-list 838.9 KB 220.9 KB
workflow 770.9 KB 220.9 KB
workflow-list 839.1 KB 220.9 KB
workflow-setup-status 770.6 KB 220.9 KB
query-results 1094.4 KB 220.9 KB
render-ui 1198.6 KB 220.9 KB
visual-review-snapshots 775.4 KB 220.9 KB
workflow-data-suggestions 773.2 KB 220.9 KB
workflow-sending-rules 771.9 KB 220.9 KB
ℹ️ MCP agent API — agent-facing tool changes

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

Tools added (1): workflows-upload-email-template (2,038 chars)

Tools changed (1):

Tool Params Scopes Annotations Schema chars
workflows-send-email-prepare 2,394 -> 2,551
✅ Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

messaging.0017_messagetemplate_code_managed_fields
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: content_hash
  └─ #2 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: managed_by
  └─ #3 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: source_path
  └─ #4 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: source_ref
  └─ #5 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: source_repository
  └─ #6 ✅ AddField
     Adding nullable field requires brief lock
     model: messagetemplate, field: superseded_content_hashes

📚 How to Deploy These Changes Safely

AddField:

This operation acquires a brief lock but doesn't rewrite the table.

Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.

Last updated: 2026-11-06 09:20 UTC (ec48ef8)

@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 9e0c2ea: 2 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

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.

Comment on lines +64 to +66
sent_email = _email_value(actions or [])
sender_id = content.from_integration_id or (sent_email.get("from") or {}).get("integrationId")
return bool(sent_email) and sent_email == _email_value(transactional_email_actions(content, 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 · Compare execution settings when detecting workflow drift

When a workflow edit changes inputs.email.templating but preserves its value, this comparison still returns true. An identical upload then answers unchanged and skips synchronization, leaving Liquid placeholders—including the recipient—incorrectly rendered by HogInputsService. Compare the execution-relevant email step settings as well as its value.



def _template_an_upload_claimed(team_id: int, key: str) -> MessageTemplate | None:
return MessageTemplate.objects.filter(team_id=team_id, key=key, deleted=False, managed_by__isnull=False).first()

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 · Restrict the code-send lookup to the identifiers it needs

For code sends using an uploaded template, this replaces an ID-only lookup with a full row fetch, transferring and decoding content and draft_content before _transactional_workflow_id fetches them again. Large HTML and editor designs now add database traffic and JSON parsing to every send. Select only id and transactional_hog_flow_id here.

Silthus and others added 3 commits November 2, 2026 09:10
Silthus and others added 3 commits November 3, 2026 10:26
…l api page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template code fields layer now sits on master, so this merge keeps
master's code and re-applies only this layer's change: uploading email
templates from code by key.

Master moved on in a few places this layer touches, and the merge keeps
master's shape there:
- the code send sender helper is code_send_sender_id
- a key's workflow lookup takes the template, not its id
- send and make_transactional declare their scopes on the action, so
  only upload joins the write actions list
- make_transactional still drops content for a send-scoped key

OpenAPI types, MCP tools and tool snapshots are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patch the workflow reload where master's hog flow model calls it, and
expect a key workflow's email step to carry its key when no upload
claimed the template that shares it.

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

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant