Repository navigation
Conversation
Seven nullable columns on the message template, and a partial unique index on (team, key) for templates that are not deleted. The index is built concurrently in its own non-atomic migration. Nothing reads or writes the columns yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PUT /api/projects/{project_id}/messaging_templates/by_key/{key}/ creates the
template on the first upload (keyed, managed by code, transactional with the
default verified sender), publishes changed content after that, and answers
unchanged when nothing changed. Project secret API keys with hog_flow:write can
call it, behind workflows-transactional-email-api.
The update, design and upload routes now share one save-and-sync service that
writes the template, its content hash and the workflow version together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template uploaded from code is replaced by its next upload, so an edit made in PostHog was lost without warning. One ownership policy now refuses every write to a template with managed_by = code except the upload, sends, make transactional and a release. It answers 403 immutable with why, fix (naming the file when the source is known) and docs_url. The policy keys on the action and the whole payload, so a write action added later is refused until someone allows it. It runs on the row read by the permission check, and again on the row locked by the save path, so an upload that claims the template mid-request still wins. A PATCH whose only field is managed_by: gui releases the template. It keeps its key, so the next upload claims it again. The MCP update and design tool descriptions point to the upload tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntent hash
POST /api/projects/{project_id}/messaging/send/email/ names the template in
the body as {key, hash?} or {id} and sends it through the existing send
service, behind workflows-transactional-email-api. Project secret API keys
with hog_flow:write can call only this action.
A hash the template never published answers 409 template_outdated and sends
nothing. A hash newer content replaced sends the current content with a
template_superseded warning, so an old deploy never brings old content back.
The send usage event records how the template was addressed and how the
hash compared. A generated MCP tool exposes the send.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/488 # Conflicts: # products/messaging/backend/facade/templates.py # products/messaging/backend/presentation/views/message_templates.py
A PATCH whose only field is key gives a keyless template its key, once, so the next upload under that key takes it over and keeps its id, workflow and sender. Refuses key_already_set, key_in_use, invalid_template_key, and a key change sent with other fields. Project secret API keys still cannot reach the PATCH. The MCP update tool exposes the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/490 # Conflicts: # products/messaging/backend/presentation/views/message_templates.py # products/messaging/frontend/generated/api.schemas.ts
A template uploaded from code gets a "Managed by code" tag on its card and page, a source line that links its file, repository and commit on known hosts, and a notice that edits here can't be saved. A keyed template shows its key, and the API tab sends through the send route by key or id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/490
An upload with `if_not_superseded` set whose content hash is in the template's superseded list writes nothing and answers `200 superseded`, naming the current hash. Without the option an upload publishes as before, so an explicit upload stays the rollback path. The MCP upload tool describes the option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code-managed template page now shows a read-only preview (subject, preheader, HTML in a sandboxed frame, plain text) in place of the email editor, with no Save, Discard or Save as new template. The form of a code-managed template never diverges from the server copy and its submit sends nothing, so neither a save nor any draft autosave can write it. "Stop managing in code" in the page menu asks for confirmation, sends the release PATCH and reloads the page as an editable template. Delete is disabled on the page and the library card until the template is released. Duplicate on both surfaces shares one copy payload that carries only the name, description and content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o spec/code-managed-templates/499
- An upload checks workflow editor access before it answers `unchanged` or `superseded`, so a member without access can't rewrite the source. - An upload repairs a stored hash that does not describe the published content, so a save that skipped the hash no longer loops on `template_outdated`. - Keys validate as whole strings, and the send no longer trims the key, so `welcome\n` is refused instead of stored or silently matched. - The PATCH schema declares the release (`managed_by: gui`). The MCP update tool leaves it out. - The shared transactional email API pieces move out of the template view into their own module. One PATCH matcher, one refusal base and one usage capture replace the pairs that adoption, release, upload and send each built. The key constraint name lives on the model. - The line-ending hash case checks a fixed digest instead of the function under test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A template save no longer sends the key, owner or source, which only the server sets, so a key set elsewhere never blocks a save. - Duplicate shows a loading state while it runs and the real error when it fails. - Links in the read-only preview open in a new tab instead of replacing the preview. - A long key truncates instead of overflowing the header. - Source links: Bitbucket branch and commit paths, encoded branch names, and GitLab projects in subgroups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Links in the read-only preview open in a new tab even when the email sets its own target. The preview sets the target on every link and keeps the email's doctype. Its test goes through the rendered frame. - An upload refused for missing workflow access reports its usage event. - A long key truncates with an ellipsis and keeps its copy button. - The single-field PATCH matcher moves from the facade contract into the ownership service, so the contract stays plain data. - A GitLab remote with a trailing slash links to the right project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A member who released a code-managed template and edited it could lose the edits when an upload claimed the template back: the save answered 403 immutable, the page toasted and stayed editable until a reload. Now a save refused with immutable reloads the template, so the page turns read-only with the managed-by-code notice, and a warning banner offers "Duplicate with my changes". The duplicate uses the shared copy payload with the unsaved values, so it carries no key, ownership, source or hash. Closing the banner discards the edits. Refs #508 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The final review found two gaps in the refused-edits flow: - A failed reload after `403 immutable` left the page editable, and closing the banner did not discard the edits. The logic now marks the loaded copy as managed by code and resets the form at once, then reloads. Only the offered duplicate keeps the refused edits. - A duplicate still navigated to the copy after the person had left the page. Both duplicate listeners now check their breakpoint after the request, before they navigate. Refs #508 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template API returns change_prompt_starter and change_prompt_context on a template managed by code, and null on any other. One builder in the messaging product makes both, so the web page, PostHog AI, Slack and any MCP client hand a coding agent the same text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…our agent A code-managed template shows "Edit with your agent" in the Save slot. It opens a popover where the person completes the server's starter sentence and copies it, followed by a blank line and the server's change prompt context. Nothing is saved. Copy waits until the sentence describes a change. The sentence stays in page state while the popover closes and reopens. The notice now points at the file or a coding agent. Events: agent prompt opened and copied, template duplicated, and released from code. No prompt text, path or repository goes into their properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side menu of "Edit with your agent" offers "Copy HTML file". It copies
the published template as the exact file posthog-node's templateFile()
reads: the subject goes into the first <title> and the preheader into the
first <meta name="preheader">, each added when missing. A tag whose value
already matches stays as it is, so a template uploaded from a file copies
back byte for byte. A blank preheader removes its tag, and Liquid stays
literal.
Nothing is saved. The toast says how the file goes live, and notes when
the saved template has a plain-text part the file can't carry.
Event: message template html file copied {template_id, dropped_text_part}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-agent-edit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 |
…of the change prompt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble list The lazy regex rescanned the rest of the HTML after every unclosed opener, so a large upload made every list and retrieve quadratic. change_prompt_variables gives the page the server's list, so the frontend no longer rescans the prompt's prose for its usage event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ied HTML file A blank preheader now removes every preheader tag, not only the first. Liquid inside the subject or preheader escapes the character references and the closing title tag the SDK would otherwise decode or stop at, so the file reads back as the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and keep focus on the agent button - variable_count reads change_prompt_variables instead of rescanning the prompt's prose - Duplicate with my changes and the library card's Duplicate fire message template duplicated - a reload that points the template at another file drops a sentence typed for the old file - the trigger announces its popover and gets focus back when the popover closes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scroll container inside the collapse inside the popover's 3D transform stays unpainted in Chromium until it scrolls, so the prompt now grows inside the popover's own scroll area. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only whitespace, backticks and control characters can carry an instruction in a path, so paths like app/(marketing)/emails/[locale]/welcome.html stay known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… full prompt - focus still inside the closing prompt goes back to the trigger; the popover keeps its content mounted for its exit, so the earlier body-only check never fired - the prompt panel is a labelled dialog, and Copy prompt sits above Show full prompt - the full prompt is a div, since global styles make every pre a scroll container - a blank preheader removes every preheader tag in one pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prompt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
added a commit
to Silthus/posthog
that referenced
this pull request
Oct 17, 2026
Silthus
added a commit
to Silthus/posthog
that referenced
this pull request
Oct 17, 2026
Silthus
added a commit
to Silthus/posthog
that referenced
this pull request
Oct 17, 2026
Silthus
added a commit
to Silthus/posthog
that referenced
this pull request
Oct 17, 2026
Silthus
added a commit
to Silthus/posthog
that referenced
this pull request
Oct 17, 2026
…s focus Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
4 of 5 tasks
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.
Important
Stacks on #118924 (
spec/code-managed-templates). GitHub can't base a PR on a fork branch, so this targetsmaster. Review starts after the merged tip ofspec/code-managed-templates,d746c355e26:git diff d746c355e26...spec/template-agent-edit.Problem
A person who sees a code-managed email template in PostHog has no path back to its file.
The page is read-only (#118924). Duplicate makes an unkeyed copy that nothing sends, and "Stop managing in code" lets the file and PostHog drift apart.
The change belongs in the repository, and today the person or their coding agent must work out which file it is, what format PostHog reads, and how the change goes live.
Warning
🟡 Merge risk: medium · two-way door (merge-risk rubric)
managed_byiscode, which only the flagged upload sets. The builder is a linear scan with no query per row, and it treats uploaded source values that could carry an instruction as unknown. Nothing writes, and a revert removes it all.Changes
On a code-managed template, the Save slot now holds Edit with your agent. Only the flagged upload (
workflows-transactional-email-api) makes a template code-managed, so nobody sees it today.In `emails/welcome.html`, change. "Copy prompt" stays disabled until the person describes a change. "Show full prompt" shows what gets copied.{{ variables.* }}the file uses, how the change goes live, and a link back.MessageTemplateSerializerreturnschange_prompt_starter,change_prompt_contextandchange_prompt_variables, all null unlessmanaged_byiscode. The MCP get tool returns them, so an agent in the repository reads the same text.posthog.emails.templateFile()reads. A file the SDK uploaded copies back byte for byte. A template with a plain-text part gets a toast note that the file drops it.message template agent prompt opened,… agent prompt copied,… html file copied,… duplicated(page, "Duplicate with my changes" and library card),… released from code. No prompt text, path or repository in properties.Captures from the real app built from this branch, on a scratch project with invented content (Hedgebox,
example.com), atfa25e693a33and069216473b7:Reading order
4e143097702,0ba8f7972c262c695af6cb8a50f6ef11c069216473b7spec/code-managed-templatesatd746c355e264a8f7e1a307,36868e96ec7,37b863356f6,aa430103f2f,5ff207cb20cchange_prompt_variables, encoder edge cases, events, focus, stale sentence01cbef1d39e,fa25e693a33,158cccc1401,3ae1cce0091,004b1bdc322,0960e3de116How did you test this code?
Live run in the real app, built from this branch, on a scratch database, at
069216473b7and again after the fixes:emails/welcome.htmlfrom a scratch git checkout.GETreturned the path starter, the full block with the short commit, andchange_prompt_variablesin order. A REST upload without a path got the path-less starter. A template made in PostHog, a released one, and one written by the base code all read correctly. List and retrieve agree.source.repository, backticks and spaces insource.path, and;insource.refproduced a prompt naming none of them. A route-group path (app/(marketing)/emails/[locale]/welcome.html) stays in the starter.change_prompt_context. The full prompt paints. Focus returns to the button after Escape, Ctrl+Enter and Copy prompt. "Copy HTML file" gives back the SDK's file byte for byte. A two-preheader template and Liquid holding&both read back through the SDK'sparseTemplateFile. No write request left the page after any copy.updatedwith a new content hash.004b1bdc322,0960e3de116) and the stricter path shape (3ae1cce0091) came after the last browser run. Their evidence is the render test and the API test below.Test rationale:
test_template_change_prompt.py(APIBaseTest, templates created by the real upload route, thenGET): the exact block for full source, path only, no source, a branch ref, a route-group path, source values that could carry an instruction (with and without spaces), repeated and non-variablesLiquid, andchange_prompt_variables. A released template returns null fields, which catches a builder that ignoresmanaged_by.messageTemplateLogic.test.ts(API mocked at HTTP): the disabled copy, the exact clipboard text, noPATCHorPOSTon either copy, toasts and event properties, the sentence dropped when a reload points at another file, the duplicate event on "Duplicate with my changes".EditWithAgentButton.test.tsx(rendered, user events): focus returns to the button after Escape (from the text area and from the button), a second click on the button, and Copy prompt. Only the DOM shows this.templateHtmlFile.test.ts: three SDK-uploaded files encode back byte for byte. Ten template shapes, including duplicate preheader tags and Liquid holding&or</title, encode to files that the SDK's recordedparseTemplateFilereads back as the template. A second encode changes nothing.Local gate on HEAD: messaging backend 387 passed,
mypyclean, frontendtsgoclean, TemplateLibrary + Git jest 9 suites (110 tests) passed, MCP typecheck, tool schema snapshots and tool-name lint, pre-pushci:preflight --strict0 failures.Gaps: CI on this fork PR waits for a maintainer to approve the run. Prod shape is not measured until production read access exists (2026-11-02). A real send into the dev mail catcher was skipped: the send path is unchanged and #118924 proved it.
Release status
Docs update
The docs section on changing a code-managed template ships with PostHog/posthog.com#21306 (Silthus/posthog#507).
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Opus 5.5 wrote the code and the review fixes.
Built as three tickets of spec Silthus/posthog#502 (map Silthus/posthog#461), each test-first with
/tdd, then reviewed as one PR with a QA swarm because the diff extends a public API contract. Skills:/improving-drf-endpoints,/implementing-mcp-tools,/writing-ui-components,/writing-kea-logics,/writing-tests,/writing-user-facing-copy,/writing-pr-descriptions. Every screenshot and fixture uses invented data (example.com, MockHog). CodeRabbit is retired on this repository, so it did not run.Decisions
`main`, not as a shortened commitcopyToClipboard's toasts</title, which the SDK would decode or stop at; a preheader whose Liquid holds"gets single quotes. Replaced "escaping skips Liquid"change_prompt_variables, instead of rescanning the prompt's prose; two tickets had built the scan twiceInvolvement: no grilling or prototype rounds for this stack; QA swarm with 2 panel rounds and 4 final reads (GPT-6.1 Sol high: correctness, risk, design, systems, author perspective; Opus 5.5 high: fresh eyes, UI); 3 live runs captured in the real app.
Closes Silthus#512
Closes Silthus#503
Closes Silthus#509
🤖 Generated with Claude Code