Skip to content

feat(workflows): surgical email patching for workflow email steps - #76015

Merged
trunk-io[bot] merged 8 commits into
masterfrom
feat/workflows-patch-action-email
Aug 3, 2026
Merged

feat(workflows): surgical email patching for workflow email steps#76015
trunk-io[bot] merged 8 commits into
masterfrom
feat/workflows-patch-action-email

Conversation

@mayteio

@mayteio mayteio commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Problem

The only way to change the email on a workflow function_email step was to rewrite the entire config.inputs.email.value blob through a graph update_action. For an agent editing an email mid-workflow that is slow and token-hungry: it has to read the whole email (design JSON + rendered html can be huge), regenerate the full object just to tweak a subject line or one design block, and send it all back. Small edit, big round-trip. As a bonus it also removes a footgun where the rendered html could drift out of sync with the design it was built from.

Changes

Diff size note: this looks like a +1.1k PR but the real change is ~200 lines in hog_flow.py plus tests. The rest (~580 lines) is auto-generated API clients and MCP tool schemas (*/generated/*, *.zod.ts, tool-schemas/*.json) that get regenerated for any new endpoint. Reviewers only really need to read hog_flow.py and test_hog_flow_action_email.py.

Adds a surgical patch endpoint for the email on a single workflow step, so an agent can send just the delta instead of the whole email:

PATCH /api/projects/:id/hog_flows/:id/actions/:action_id/email

  • operations: the same id-addressed design operations used by the email template library, applied atomically to the step's email design. The html is re-rendered server-side from the patched design (via Unlayer), so design and sent content can't diverge.
  • email_patch: a deep merge for the plain fields (subject, preheader, text, to, from, replyTo, cc, bcc); a null leaf deletes the key. design and html are rejected here - the design is edited via operations and html is always derived.
  • Rides the existing /graph endpoint machinery: MCP-on-active edits route to a draft (gated by the workflows-revisions flag), an optimistic-concurrency staleness guard (409 on stale base_updated_at), draft composition, and revision bumps. Row-locked with select_for_update inside the transaction.

Reuses existing utilities rather than adding new ones: apply_design_operations, validate_design, render_design_html, DesignOperationSerializer from the messaging backend, and _deep_merge from workflows graph operations.

Exposed as the workflows-patch-action-email MCP tool, with regenerated frontend API client and MCP tool schemas. Docs touched: building-workflows and designing-email-templates skills.

How did you test this code?

15 new backend tests in test_hog_flow_action_email.py, all passing (15 passed in 50.30s), plus mypy clean and ruff clean. Coverage includes regressions no existing test caught:

  • design ops re-render html; field patch merges without re-rendering
  • derived keys (design/html) rejected in email_patch; empty patch rejected
  • unknown action_id and non-function_email step rejected
  • operations against a step with no design rejected
  • unknown content-id and render failure rejected without a partial write (transaction integrity)
  • MCP-on-active lands in a draft; rejected when revisions flag is off; composes on an existing draft
  • stale base_updated_at returns 409
  • web patch on an active flow applies live and bumps the revision

Also manually tested end to end through the real workflows-patch-action-email MCP tool against a local dev workflow: a design update_content op plus an email_patch subject change both applied correctly, the HTML was re-rendered via a live Unlayer call, the workflow's revision bumped, and the email_patch guard rejecting design/html fired as expected.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored across two agent sessions in PostHog Code (Claude). The first session built the feature and tests, ran mypy/ruff/pytest to green, then hit its usage limit right before opening the PR. A second session picked up the finished, committed-but-unpushed branch, re-ran the targeted suite (15 passed), pushed, and opened this PR - no code changes were made in the second session.

Skills invoked while shaping the work: building-workflows, designing-email-templates.

Key decisions: modelled the endpoint as a twin of the existing /graph update path so it inherits draft routing, the staleness guard, and revision bumps rather than reinventing them; kept html strictly derived from design (never client-settable) so sent content can't go stale; forbade design/html in email_patch to keep a single source of truth.


Created with PostHog Code

mayteio added 2 commits July 31, 2026 10:53
PATCH /hog_flows/:id/actions/:action_id/email applies the email template
library's id-addressed design operations to the email embedded in a
function_email step, plus an email_patch deep merge for subject/preheader/
text/recipients. HTML is re-rendered server-side from the patched design so
the sent content can't go stale. Rides the graph endpoint's draft routing,
staleness guard, and revision machinery. Exposed as the
workflows-patch-action-email MCP tool.
@mayteio mayteio self-assigned this Jul 31, 2026
@trunk-io

trunk-io Bot commented Jul 31, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions
github-actions Bot requested a deployment to preview-pr-76015 July 31, 2026 15:05 In progress
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ❌ build failed

The preview didn't come up for commit 2308a1e. See the build log for the failing step. It'll retry on the next push.

Previews are optional and never block merging. A failure here is often a hogland or tailnet hiccup rather than anything in your PR, so the check stays green and this comment is the status.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 31, 2026 15:06
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

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

Total: 65.53 MiB · no change

No file changed by more than 1000 B.

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.25 MiB · 22 files no change ███░░░░░░░ 27.7% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.14 MiB · 3,035 files no change ████████░░ 83.8% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.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.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
285.5 KiB ../node_modules/.pnpm/posthog-js@1.410.1/node_modules/posthog-js/dist/rrweb.js
267.7 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
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
231.5 KiB ../node_modules/.pnpm/posthog-js@1.410.1/node_modules/posthog-js/dist/module.js
154.3 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
105.3 KiB src/lib/api.ts
94.7 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/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.19 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.19 MiB · 17 files no change ████░░░░░░ 38.3% of 5.72 MiB
Deferred (lazy) 2.08 MiB · 33 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
718.3 KiB dist/toolbar/toolbar-app-FT4UJ3IP.css
551.5 KiB dist/toolbar/chunk-chunk-W7B762O4.js
484.6 KiB dist/toolbar/chunk-chunk-T44C2V5C.js
133.6 KiB dist/toolbar/chunk-chunk-ZYWF76EY.js
131.8 KiB dist/toolbar/chunk-chunk-T5KY5WYR.js
71.0 KiB dist/toolbar/toolbar-app-NOHXZCKZ.js
69.0 KiB dist/toolbar/chunk-chunk-27JL52RE.js
35.6 KiB dist/toolbar/chunk-chunk-3XKYMPJQ.js
20.9 KiB dist/toolbar/chunk-chunk-3GU2JABZ.js
12.2 KiB dist/toolbar/chunk-chunk-PIK3PADE.js

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

Dist folder size — 🔺 +806 B (+0.0%)

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

Total: 1389.05 MiB · 🔺 +806 B (+0.0%)

ℹ️ MCP UI apps size — 32 app(s), 17070.3 KB JS

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

App JS CSS
debug 599.6 KB 187.7 KB
action 457.8 KB 187.7 KB
action-list 564.4 KB 187.7 KB
cohort 456.8 KB 187.7 KB
cohort-list 563.4 KB 187.7 KB
email-template 456.6 KB 187.7 KB
error-details 472.4 KB 187.7 KB
error-issue 457.5 KB 187.7 KB
error-issue-list 564.3 KB 187.7 KB
experiment 561.5 KB 187.7 KB
experiment-list 565.2 KB 187.7 KB
experiment-results 563.2 KB 187.7 KB
feature-flag 567.2 KB 187.7 KB
feature-flag-list 570.9 KB 187.7 KB
feature-flag-testing 461.0 KB 187.7 KB
insight-actors 562.2 KB 187.7 KB
invite-email-preview 456.0 KB 187.7 KB
llm-costs 559.5 KB 187.7 KB
session-recording 458.6 KB 187.7 KB
session-summary 463.9 KB 187.7 KB
survey 458.4 KB 187.7 KB
survey-global-stats 562.2 KB 187.7 KB
survey-list 565.1 KB 187.7 KB
survey-stats 562.2 KB 187.7 KB
trace-span 457.2 KB 187.7 KB
trace-span-list 564.3 KB 187.7 KB
workflow 457.1 KB 187.7 KB
workflow-list 563.7 KB 187.7 KB
loops-review 461.2 KB 187.7 KB
query-results 747.4 KB 187.7 KB
render-ui 828.0 KB 187.7 KB
visual-review-snapshots 461.6 KB 187.7 KB
Playwright — all passed

All tests passed.

View test results →

⚠️ Backend snapshots — 8 updated (8 modified, 0 added, 0 deleted)

Query snapshots: Backend query snapshots updated

Changes: 8 snapshots (8 modified, 0 added, 0 deleted)

What this means:

  • Query snapshots have been automatically updated to match current output
  • These changes reflect modifications to database queries or schema

Next steps:

  • Review the query changes to ensure they're intentional
  • If unexpected, investigate what caused the query to change

Review snapshot changes →

⚠️ Backend coverage — 98.0% of changed backend lines covered — 6 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ████████████████████ 98.0% (309 / 315)

File Patch Uncovered changed lines
products/workflows/backend/api/hog_flow.py 94.8% 2045, 2075, 2110, 2114, 3009, 3017

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 30821248243 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.5% 8,806 / 22,287
demo ███████████░░░░░░░░░ 56.3% 1,497 / 2,661
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
data_tools ██████████████░░░░░░ 70.0% 63 / 90
tasks ██████████████░░░░░░ 70.4% 33,513 / 47,611
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 81.5% 25,463 / 31,226
cdp ████████████████░░░░ 82.1% 3,285 / 3,999
data_modeling █████████████████░░░ 85.7% 7,749 / 9,047
notebooks █████████████████░░░ 86.0% 7,794 / 9,060
managed_warehouse █████████████████░░░ 86.2% 5,471 / 6,347
actions █████████████████░░░ 86.6% 717 / 828
wizard █████████████████░░░ 86.7% 1,090 / 1,257
data_warehouse █████████████████░░░ 87.4% 10,967 / 12,547
cohorts █████████████████░░░ 87.5% 6,393 / 7,309
exports ██████████████████░░ 87.9% 7,078 / 8,054
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
business_knowledge ██████████████████░░ 89.0% 4,384 / 4,928
engineering_analytics ██████████████████░░ 89.3% 6,529 / 7,309
dashboards ██████████████████░░ 89.5% 5,989 / 6,693
visual_review ██████████████████░░ 89.5% 5,870 / 6,558
conversations ██████████████████░░ 90.0% 18,270 / 20,301
alerts ██████████████████░░ 90.3% 4,482 / 4,966
links ██████████████████░░ 90.6% 183 / 202
streamlit_apps ██████████████████░░ 90.7% 2,630 / 2,901
error_tracking ██████████████████░░ 91.0% 10,938 / 12,022
slack_app ██████████████████░░ 91.1% 9,718 / 10,664
marketing_analytics ██████████████████░░ 91.2% 12,111 / 13,284
stamphog ██████████████████░░ 91.3% 4,505 / 4,936
mcp_store ██████████████████░░ 92.2% 6,525 / 7,076
product_analytics ███████████████████░ 92.5% 5,849 / 6,321
managed_migrations ███████████████████░ 92.6% 1,556 / 1,681
early_access_features ███████████████████░ 92.6% 1,287 / 1,390
mcp_analytics ███████████████████░ 92.6% 3,775 / 4,076
notifications ███████████████████░ 92.6% 1,017 / 1,098
ai_observability ███████████████████░ 92.8% 15,618 / 16,821
surveys ███████████████████░ 93.2% 5,853 / 6,281
posthog_ai ███████████████████░ 93.2% 1,326 / 1,422
approvals ███████████████████░ 93.3% 3,437 / 3,682
reminders ███████████████████░ 93.4% 468 / 501
web_analytics ███████████████████░ 93.5% 15,375 / 16,447
legal_documents ███████████████████░ 93.8% 1,628 / 1,736
endpoints ███████████████████░ 94.2% 8,655 / 9,192
workflows ███████████████████░ 94.2% 7,559 / 8,021
tracing ███████████████████░ 94.5% 2,671 / 2,827
review_hog ███████████████████░ 94.6% 8,246 / 8,715
skills ███████████████████░ 94.6% 3,158 / 3,337
messaging ███████████████████░ 94.7% 2,885 / 3,048
experiments ███████████████████░ 95.5% 26,047 / 27,287
logs ███████████████████░ 95.5% 10,491 / 10,990
growth ███████████████████░ 96.1% 3,245 / 3,376
annotations ███████████████████░ 96.2% 732 / 761
revenue_analytics ███████████████████░ 96.3% 1,887 / 1,960
feature_flags ███████████████████░ 96.4% 17,488 / 18,144
replay_vision ███████████████████░ 96.4% 16,612 / 17,226
user_interviews ███████████████████░ 96.5% 2,638 / 2,734
access_control ███████████████████░ 96.9% 870 / 898
customer_analytics ███████████████████░ 97.1% 10,463 / 10,777
warehouse_sources ███████████████████░ 97.3% 360,281 / 370,268
data_catalog ████████████████████ 97.7% 2,588 / 2,648
analytics_platform ████████████████████ 98.0% 2,153 / 2,197
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(workflows): satisfy mypy strictness ..." | Re-trigger Greptile

@mayteio mayteio added the reviewhog ($$$) Reviews pull requests before humans do label Jul 31, 2026
@posthog

posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🦔 ReviewHog reviewed this pull request

Found 1 must fix, 1 should fix, 0 consider.

Published 2 findings (view the review).

@posthog

posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

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

ReviewHog Report

Feature

Issues: 2 issues

Files (4)
  • products/workflows/backend/api/hog_flow.py
  • products/workflows/frontend/generated/api.schemas.ts
  • products/workflows/frontend/generated/api.ts
  • products/workflows/frontend/generated/api.zod.ts
What were the main changes
  • New PATCH /api/projects/:id/hog_flows/:id/actions/:action_id/email endpoint (action_email) for surgical email edits on a function_email workflow step
  • HogFlowActionEmailUpdateSerializer validates operations (design ops) and email_patch (deep-merge, blocking design/html)
  • _apply_action_email_edit applies design operations via apply_design_operations, re-renders html via render_design_html, then deep-merges plain email fields with _deep_merge
  • Reuses /graph endpoint machinery: MCP-on-active routes to draft (gated by workflows-revisions flag), optimistic-concurrency base_updated_at staleness guard (409), select_for_update row locking, draft composition and revision bumps
  • Auto-generated frontend API client/types (api.schemas.ts, api.ts, api.zod.ts) regenerated for the new endpoint

Comment thread products/workflows/backend/api/hog_flow.py
Comment thread products/workflows/backend/api/hog_flow.py Outdated
Review fixes on the action-email endpoint:

- Declare the products.messaging dependency in tach.toml for the new
  design-operation imports (fixes the module-boundary CI failure).
- Hoist apply/validate/render out of the select_for_update transaction:
  render_design_html is a synchronous Unlayer HTTP call (up to 30s) and
  must not extend the row-lock hold. The in-lock apply installs the
  pre-rendered design only while the stored design still matches the one
  the ops were applied to, and 409s otherwise, so a concurrent edit
  can't be clobbered even when the client omits base_updated_at. Two new
  tests cover the conflict and consecutive drafted design edits.
- Regenerate services/mcp/schema/tool-inputs.json (fixes the MCP schema
  staleness CI failure).

Raised by ReviewHog on #76015.

Generated-By: PostHog Code
Task-Id: d83f5523-557d-445d-b6ce-c1414e9437a9
@github-actions
github-actions Bot requested a deployment to preview-pr-76015 August 3, 2026 12:35 In progress
Comment thread products/workflows/backend/api/hog_flow.py
@trunk-io

trunk-io Bot commented Aug 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

The pre-lock draft-routing prediction ignored revisions_enabled, so an
MCP edit to an active flow with the flag off ran the up-to-30s Unlayer
render and was then rejected under the row lock. Folding the flag into
the prediction wouldn't help (the request is doomed either way), so
reject before the render instead; the locked re-check stays
authoritative if the status flips in between. The flag-off test now
sends design operations and asserts the render is never called.

Raised by Graphite on #76015.

Generated-By: PostHog Code
Task-Id: d83f5523-557d-445d-b6ce-c1414e9437a9
@mayteio

mayteio commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

/trunk merge

@trunk-io
trunk-io Bot merged commit ae741c0 into master Aug 3, 2026
279 checks passed
@trunk-io
trunk-io Bot deleted the feat/workflows-patch-action-email branch August 3, 2026 15:04
@deployment-status-posthog

deployment-status-posthog Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-08-03 15:52 UTC Run
prod-us ✅ Deployed 2026-08-03 16:11 UTC Run
prod-eu ✅ Deployed 2026-08-03 16:14 UTC Run

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

Labels

reviewhog ($$$) Reviews pull requests before humans do

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants