Repository navigation
Conversation
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
This was referenced Oct 17, 2026
Closed
Closed
Closed
This was referenced Oct 20, 2026
Silthus
force-pushed
the
spec/code-managed-templates
branch
from
October 23, 2026 21:24
89f57b2 to
f04e5eb
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template library marks a code-managed template with its source and key, shows it read-only, and offers a release back to PostHog. Part 3 of 3 of PostHog#118924. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ield Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 24, 2026
Silthus
force-pushed
the
spec/code-managed-templates
branch
from
October 24, 2026 17:28
f04e5eb to
4b0ca47
Compare
Closed
6 of 23 tasks
This was referenced Oct 25, 2026
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Carries the migration from #118769 (key, owner, source and hash columns), renumbered
0009and0010aftermaster's template drafts (0007) and sensitive flag (0008). Rebased ontomasteron 2026-10-21, twice:mastergained template tags while the first rebase ran its gates. #118769 alone now conflicts withmaster; this PR replaces it.Problem
Engineers, and the coding agents working for them, keep transactional emails (welcome, password reset) as HTML files in their repository and want PostHog to send them (spec).
phs_) can send but cannot create or update a template, so code cannot upload one.Merge Danger
Caution
🔴 Merge risk: high · one-way door: the new columns and index stay after a revert (merge-risk rubric)
services/template_saves.py) that also writes the content hash and refuses edits to code-managed templates. A shadow diff at756474ff699over 14 ordinary edits shows identical responses and workflow versions. It ran no draft save and no tags save, which also go through that service.phs_keys withhog_flow:write: upload by key and the new send route. Both answer a plain 404 whileworkflows-transactional-email-apiis off. With the flag on, the API tab of every transactional template shows the new send route.0009(nullable columns) and0010(a concurrent unique(team, key)index) stay after a revert. The routes, thev1:hash format and the error codes become a public contract once the flag rolls out and posthog-node shipsemails.send.Changes
Each ticket in reading order, with its commits. The commits come from this PR's history before the review split it into three parts, so they are not ancestors of this branch. This branch carries them as
ab6174d3f9d,29b841ca21band745b6a388ee, each followed by its review fixes.PUT /messaging_templates/by_key/{key}/creates (201), updates or answersunchanged(200). The first upload makes the template transactional with the default verified sender. Every upload keeps the stored sender and reply-to.a7ef8ccf4d9,459a9fea1ce,98c0ca25f73403 immutablewithwhy,fixanddocs_url.PATCH {"managed_by": "gui"}releases it.84d4cd46c1dPOST /messaging/send/email/names the template as{key, hash?}or{id}. An unknown hash answers409 template_outdated. A replaced hash sends the current content with atemplate_supersededwarning.8120cbf7ce1POST /messaging/send/email/withtemplate.id, ortemplate.keyfor a template with a key.f772d0d38f1PATCHwhose only field iskeygives a template made in PostHog a key. The next upload takes it over with the same UUID and workflow.0c2a8d86106,0dae40b0e8c39fa349c482if_not_superseded: truemakes an upload of replaced content write nothing and answersuperseded. posthog-node sets it on its automatic upload.ae5f26ff1de403 immutableof a save, draft autosave or draft discard locks the page, reloads it read-only with the published content, and offers "Duplicate with my changes". Leaving the page first asks. Closing the offer discards the edits.62794d871e4,bae2da8f375,86ddda560fc,9589874b059449e7f0b780,49c271e66e0,99f3cfb0d2emastermaster" below.2c9d949f146,89a2a8fb665,74fc42607df,a1b3edb883e,4845efd98a9,99163db3f9d,8932b7657c6services/template_saves.py. Update, design and upload all call it, so every content save writes the hash.v1:<hex>. Replaced hashes move tosuperseded_content_hashes(at most 100).fromandreplyTo, so the workflow sync no longer writes them empty.5b45b9b3b51).33147f4be18,d83bc5d7611,4b0ca4756ed).400 invalid_requestwithattr: nulland the fix "Send the body as a JSON object and upload again." (or "send again").workflows-upload-email-templateandworkflows-send-emailare generated from the new serializers and gated by the same flag.Rebased onto
mastermastergained template drafts (#118664), the sensitive flag, anIdempotency-Keyon the per-template send, and template tags (#120931) while this PR waited. The rebase keeps all four:89a2a8fb665). The lock refuses a draft-onlyPATCHon a code-managed template, like every other edit, and checks ownership and workflow access again under the row lock. So an upload that claims the template mid-request can't get a stale draft written back, and a workflow linked mid-request still needs editor access (74fc42607df,4845efd98a9,8932b7657c6). So a code-managed template never holds a draft, and its read-only preview and tags need no draft handling.PATCHof onlysensitive, like a send. The setting protects what PostHog keeps about each email and is not part of the file (4845efd98a9).master's template tags (#120931) save inside the locked save when a form carries them, and a tags-onlyPATCHkeepsmaster's narrow path. A code-managed template takes aPATCHof onlytags, likesensitive: tags organize the library and are not part of the file (756474ff699).today-rail-navoffer "Stop managing in code", disable Delete and hide "Save as new template" on a code-managed template, like the current menu (99163db3f9d). Duplicate is disabled while its copy is made (e26bad4d07f).master's draft autosave share onesetTemplateValue/setTemplateValueslistener (a1b3edb883e). Two listener pairs in one object kept only the second, which stopped the draft autosave.POST /messaging_templates/{id}/send/keepsmaster'sIdempotency-Keyandreplayed. The newPOST /messaging/send/email/answersreplayed: falseand takes no key yet: the key arrives with the stacked send command.0009_messagetemplate_code_managed_fieldsand0010_messagetemplate_unique_key_per_team.Screenshots
UI walk at
4b0ca4756ed, in the order a member meets it: a production frontend build on a preview stack, the demo project, LemonUI, and thetoday-rail-navvariant for the last two. All eight stages ran in both variants (38 screenshots, 13 shown), and each matched apart from the two items in Decisions. No recording was made: gap.template.key.template.id.templates/weekly-digest.html.managed_bystays a normal editable template.today-rail-nav, the library tags only the code templates.today-rail-nav, an upload mid-edit shows the same refused-edits banner.Earlier Storybook evidence (mocked data, at the commits named)
Storybook renders of the real scene against mocked data (
example.comrepository, invented copy).The read-only preview's field labels (Subject, Preheader, HTML, Plain text) now use
LemonField.Pure, the label of other Lemon forms. Rendered on89f57b2cd3eand its parent at 560px (Storybook, mocked data); 1440px before, 1440px after:More states, before and after, at 560px and 1440px: #489 screenshots, #492 screenshots, #508 at 775px. The #508 render shows Storybook's own mock toasts in the corner.
Review fixes, rendered on
07d7e0ee544at 560px (Storybook, mocked data):Quill actions behind
today-rail-navon a code-managed template, rendered on756474ff699at 1280px (Storybook, mocked data). In "Before", only the actions menu ismaster's:master)A
target="_self"link in the preview opened a new tab, and the preview stayed: after the click.How did you test this code?
Test rationale: each ticket drove its slice at the template and send API (
APIBaseTestwith realphs_and personal keys, CDP mocked atinternal_requests.post), plusmessageTemplateLogickea tests for the UI. The hash has one unit test against the spec's test vectors, shared with posthog-node.test_template_upload.pytest_transactional_email.py(extended)test_code_managed_template_lock.pytest_template_adoption.pyphs_keytest_content_hash.pymessageTemplateLogic.test.tsMessageTemplateMenu.test.tsxEmailHtmlFrame.test.ts,gitMetadataParser.spec.tsProofs
987b927630e, the top of the stack this PR starts, which holds this head unchanged. Django, cdp-api and the email worker ran on a migrated clone of the local test database, with maildev catching mail. A template seeded with the keypassword-resetsent throughPOST messaging/send/email/withtemplate.key. It answered 202 and arrived with a tracked link (case 02).PUT messaging_templates/by_key/<key>/with a personal API key. Every stage matched, apart from the two items in Decisions below. Screenshots above.756474ff699, before the review split. It covers paths the final runs did not: data written bymastercode, adoption (403 withphs_),template_superseded, 409template_outdated,if_not_superseded, the stale hash repair, tags-only and sensitive-onlyPATCH, draft save 403, and flag off 404. Transcript below.756474ff699only. Basee23fa706bd0and that head ran the same 14 ordinary template edits (create, patch, put, design, make transactional, sendability refusals, soft delete), flag on and off, each on a fresh database. Normalized responses are identical. Both end at workflow version 2 with the same two revisions. Gap: the input set had no draft save and no tags save, and the diff did not run again at this head.987b927630e, all green: pytest 1764, CDP jest 3690 (1 skipped), cdp-api serial 288, frontend jest 84, ClickHouse 460, MCP 5035, repo-wide mypy,tscand the frontend typecheck. The pre-push preflight ran in strict mode with 0 failures.makemigrations messaging --checkatab6174d3f9d(part 1) finds no missing migration. Each review fix went red first.Earlier live run transcript on
756474ff699, before the review split (condensed; keys masked)Review
QA swarm before the rebase: two rounds with GPT-6.1 Sol (correctness, risk, design, systems, and the author's review perspective) and Opus 5.5 (fresh eyes, UI), then a final read of the last commit. Round 1 found 20 items: 13 fixed, 4 follow-ups, 3 rejected. Round 2 found 7 items in the fixes, all fixed. The final read found none. The fixes:
unchangedorsuperseded.template_outdated.#508 added two commits after that, reviewed by GPT-6.1 Sol in two more final reads. The first found 2 medium items, both fixed in
d746c355e26: a failed reload after the refusal left the page editable, and a duplicate navigated after the person left the page. The second found none. #508 changes only the template page, so the live run and shadow diff from07d7e0ee544still apply, andfleet proof gatepasses ond746c355e26.Follow-ups, outside this PR: the asset URL rewrite command writes content without the hash, the lock or the workflow sync (the sync gap predates this PR); the global loader toast repeats every failed template save next to the page's own message (it predates this PR, and the fix is one entry in
frontend/src/initKea.ts).Resolved: the release-then-reupload race is #508.
The final QA swarm split this PR into three parts by layer (fork PRs #547, #548, #549) and reviewed each one.
attrfor a body that is not an object, a refused draft save that left the page editable, a double Quill Duplicate, a banner Duplicate that dropped refused edits, and a leave without asking.PATCH {"managed_by": "code"}answers 200 and changes nothing, because DRF ignores a read-only field.The rebase onto
mastergot its own read by GPT-6.1 Sol, then a final read of the fixes. It found 3 medium items, all from the meeting ofmaster's drafts, sensitive flag and Quill actions with the lock. The final read found the draft fix incomplete: the workflow access check still used the read before the lock. All 4 are fixed (see "Rebased ontomaster"). Two more final reads followed the second rebase (master's template tags) and a one-line import-order fix. Neither found anything.Release status
Docs update
The beta docs page
/docs/workflows/transactional-email-apiships separately on posthog.com (Silthus/posthog#475). Every new error'sdocs_urlpoints at its anchor.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5) wrote the code and the fixes. Reviews by GPT-6.1 Sol and Claude Opus 5.5.spec/code-managed-templatesof the fork, each with/tdd. Skills:/improving-drf-endpoints,/implementing-mcp-tools,/adding-api-scopes,/writing-tests,/writing-ui-components,/writing-kea-logics,/writing-user-facing-copy,/writing-code-comments,/writing-pr-descriptions,qa-swarm.example.com.Decisions
/emailsroutePOST …/messaging/send/email/that names the template in the body, instead of the recommended…/by_key/{key}/send/key,managed_byandsource_*on the message templatekeyonce throughPATCHtemplate_supersededwarninghog_flow:write, flag, throttles and send error codesPATCHwith onlymanaged_by: "gui"; the next upload claims it back, and the lock refuses soft deletetemplate_outdated; it has no idempotency optionhashandid?if_not_superseded: a new ticket after the posthog-js review found the racePATCHschema but not in the MCP update toolwhy/fix/docs_url?master's drafts?master?master, recommended over a manual rebase or a fifth layerPATCHof onlysensitivepasses, because the setting is not part of the filePATCHof onlytagspasses, because tags organize the library and are not part of the file| While refused edits wait, does the code-managed info banner stay? | Yes, without its Duplicate action, under the refused-edits banner. Its text stays true. The alternative is to hide it while edits wait (UI walk UW-1) | 🤖 Agent | pending approval |
| Should the API tab of a keyed template show
template.id? | No, rejected: it showstemplate.key, which is the same in every project, and a GUI template shows its id (UI walk UW-2) | 🤖 Agent | pending approval |Open: the
rewrite_email_asset_urlfollow-up (➡️ route it through the shared save path). See the map status.Open: should
managed_bydefault toguiinstead of null for a template made in PostHog? It changes stored data and the documented "guior null" read contract (➡️ keep null; a later backfill toguiis additive).Open: should the read-only preview show the sender? The editor shows From and Reply-to, the preview does not (➡️ yes, in this PR: a read-only sender row from
EmailSenderSummary, because the sender is set only in PostHog).Involvement: 3 grilling rounds answered (the re-chart and two spec rounds); 1 prototype iterated over 4 rounds; QA swarm before the split with 2 review rounds (GPT-6.1 Sol ×5 lenses, Claude Opus 5.5 ×2 lenses each) and 3 final reads (GPT-6.1 Sol); a rebase read and its final read (GPT-6.1 Sol). Final QA swarm over the three parts: design pass (GPT-6.1 Sol high); round 1 (review and team lanes GPT-6.1 Sol high, fresh eyes and UI Claude Opus 5.5 high); round 2 on parts 2 and 3 (review lane GPT-6.1 Sol high, fresh eyes and UI Claude Opus 5.5 high, team lane on part 3); final reads 1 and 2 (GPT-6.1 Sol high); fixers Claude Opus 5.5 high; UI walk (Claude Opus 5.5); live run at the stack top. CodeRabbit is retired in this repo.
Closes Silthus#485
Closes Silthus#487
Closes Silthus#488
Closes Silthus#489
Closes Silthus#490
Closes Silthus#491
Closes Silthus#492
Closes Silthus#499
Closes Silthus#508
🤖 Generated with Claude Code