Repository navigation
Code-managed templates: the change prompt on the template API #512
Copy link
Copy link
Closed
Labels
Description
Map: Map: send code-managed email templates with one PostHog call
Spec: Spec: copy-as-code and open-a-PR for code-managed templates (revision 3), section "The change prompt (one builder, on the server)"
Cast: Opus 5.5, high
Repository: PostHog/posthog (backend)
Stack: A, first slice, on top of PostHog#118924
Question
Does the template API give every caller (the web page, PostHog AI, the Slack bot, any MCP client, and the PostHog Code task) the same "About this file" context for a code-managed template?
Delivers end to end.
- Builder. One Python builder in the messaging product (service, reached through the facade) turns a saved template into the spec's "About this file" block:
- key, repository and short ref;
- the file contract;
{{ variables.* }}only, with the variables found in the published subject, preheader and HTML, de-duplicated and in order of first use;- how the change goes live, for a file reference and for a send by key;
- the absolute template link.
- Each line is omitted when its value is unknown. Without a path, the line that finds the file by key replaces the file line.
- Starter. The same builder makes the starter:
In `<path>`, changeorIn the HTML file of the `<key>` email template, change. - Serializer. The template serializer returns two read-only fields,
change_prompt_contextandchange_prompt_starter, withhelp_text. They are null unless the template is code-managed. - Regenerate OpenAPI and the frontend types. The MCP retrieve tool returns both fields.
Test seams.
- Django
APIBaseTeston retrieve, for:- a GUI template (nulls);
- a code-managed template with full source;
- path only;
- no source;
- repeated variables and none;
- Liquid other than
variables, which is not listed.
- Assert the exact text.
- Prior art: the message template API tests in
products/messaging/backend/.
Write scope.
products/messaging/backend/: a new service module,facade/templates.py, the template serializer inpresentation/views/message_templates.py, tests.- Generated types.
products/workflows/mcp/email_templates.yaml, if the retrieve tool's response include list needs the fields.
Proof.
- Red: the "full source" text test fails.
- Green: the API tests pass.
- Local: upload a template through posthog-node,
GETit, and paste the two fields in the PR.
Acceptance criteria
- A code-managed template returns the block and the starter; a GUI template returns nulls.
- The block matches the spec line for line.
Blockers
None. Starts from the integration branch of PostHog#118924.
How to build (map #461)
/tddred-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.- Repo skills apply where the diff touches their area:
/writing-tests,/writing-ui-components,/writing-kea-logics,/writing-user-facing-copy,/writing-code-comments,/improving-drf-endpoints,/routing-outbound-api-calls. - Read code from
origin/spec/code-managed-templates(the fork's integration branch for feat(messaging): upload, send and lock code-managed email templates PostHog/posthog#118924), not frommaster. - Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use
example.com. The private note the map points at is background only; never quote it. - Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge. The conductor names the stack's integration branch.
Reactions are currently unavailable
Activity
Silthus commentedon Oct 17, 2026
Runtime: T3 child task
Task:
node:delegated-task:command%3Amcp%3A1778d880-5a25-404c-80d0-25fc5ccca3b2%3Adelegate-task%3Awf-email-impl-512-1Cast: Opus 5.5, high. Cap 180 minutes.
Silthus commentedon Oct 17, 2026
Resolution
Landed:
0ba8f7972c2onspec/template-agent-edit(fast-forward07d7e0ee544..0ba8f7972c2, two commits).Branch:
spec/template-agent-edit/512. No PR: the ship ticket #516 opens it.What landed
products/messaging/backend/services/template_change_prompt.py), reached throughfacade/templates.pyaschange_prompt(team_id, template). It returns aTemplateChangePrompt(starter, context)(new infacade/contracts.py) for a template managed by code, andNonefor any other.{{ variables.* }}only, with the variables of the published subject, preheader and HTML, de-duplicated and in order of first use. With none, the line ends after "posthog.emails.send.";SITE_URL/project/<team>/workflows/library/templates/<id>).posthog.emails.templateFile()for the key<key>", and the starter becomesIn the HTML file of the `<key>` email template, change.MessageTemplateSerializerreturns the read-only fieldschange_prompt_starterandchange_prompt_context, withhelp_text. Both are null unlessmanaged_byiscode.hogli build:openapiupdatedproducts/messaging/frontend/generated/api.schemas.tsand the MCP client.workflows-get-email-templatereturns both fields, with no YAML change, because its response uses an exclude list.workflows-list-email-templatesexcludes them, because that tool is documented as metadata only.Proof
Red: the "full source" test, run before the change:
Green:
test_template_change_prompt.pyhas7 passed. The wholeproducts/messaging/backendsuite has384 passed.APIBaseTest, upload by key, thenGET3f2c1ab, variables across subject, preheader and HTML{{x}}without spaces, a filter,{% if %}posthog.emails.send.variables(person.properties,event.properties.variables.x, plain text)variables.reset_urllistedmanaged_by: gui)Mutation: dropping the
managed_bycheck from the builder fails the released-template test (1 failed, 6 passed).Local gate on the landed tree:
uv run mypy --cache-fine-grained .throughfleet heavy:Success: no issues found in 24123 source files.tsgo --noEmit --checkers 1infrontend/throughfleet heavy: exit 0.pnpm --filter=@posthog/mcp run typecheck: exit 0.lint-tool-names: pass.tool-schema-snapshotsandgenerate-tools:138 passed.hogli build:openapi: exit 0, output committed.hogli ci:preflight --strict):"failures": 0.Local proof. Django from this worktree on a scratch database cloned from a migrated test database, with a verified default sender and only
workflows-transactional-email-apiforced on. posthog-node from PostHog/posthog-js#5312's head (5a896e92) uploaded a fileemails/welcome.htmlthroughposthog.emails.uploadTemplate(templateFile(...), { source: { repository, ref } }). The path came from the SDK. The repository and ref were passed explicitly, because #510's detection is not on that head.Upload result and GET
The file also used
{{ person.properties.email }}. As the spec says, it is not listed.Calls made
welcome, in the repositorygithub.com/example/app, last uploaded fromrelease/october." Without a source, the line is the spec's text exactly. This serves user story 9 (the right file in a monorepo or a workspace with several repositories).^[0-9a-f]{7,40}$, the frontend'sisCommitSharule) shows aslast uploaded from `main`, not as a commit, and is not shortened.{{ … }}and{% … %}).{% if variables.plan %}counts as use.variables.xin plain text andevent.properties.variables.xdon't.APIBaseTeston retrieve, templates created by the real upload route. The GUI case is a released template (keyed,gui). A template created in PostHog has no key, so it would pass even without the ownership check. The mutation run shows the released case catches that._with_creatorsbecame_serializable, because it now adds the prompt fields as well as the creator.Follow-ups
change_prompt_starterwhen the instruction doesn't, or namesource_pathin the task text.MessageTemplateinproducts/workflows/frontend/TemplateLibrary/types.tspicks generated fields one by one, so it needschange_prompt_starterandchange_prompt_contextadded.managed_byandchange_prompt_contextis Request a change: start a PostHog Code task that opens a PR for a code-managed template #513's, as the spec places it. This ticket did not change tool descriptions.