You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Spec code-managed email templates and slice them into PRs #484
What exactly does the code-managed template slice build on top of the transactional email API on PostHog/posthog master, and how does it split into stacked PRs?
Write the spec from the map's re-chart decisions (2026-10-16): template key + managed_by + source_* metadata, the idempotent upsert by key, send by key, the "Managed by code" UI and then the read-only lock, MCP tools, posthog-nodeemails.send with the template union (a key, or a direct HTML template reference that infers the key), the docs page and the agent skill. Then slice it into implementation tickets and retarget #474–#476.
Dispatched to T3 thread thread:project:f66f06fc-0bc1-4bfe-b00a-4aab7f54eb13:1df45b10-ae6e-4d8f-bb24-f50af0ead8af (Opus 5.5, high), worktree from PostHog/posthog master.
The to-spec and to-tickets skills only run when a person invokes them, and the ticket worker cannot call them. No spec issue or implementation tickets exist yet. #474, #475 and #476 are unchanged.
Pending question
How should the spec run? Recommendation: run /to-spec in the ticket worker's thread, then /to-tickets after the spec is written. The research inputs are ready.
Decisions locked in the spec (on its own, open to change): upload and send by key reuse hog_flow:write, the existing flag, throttles and error envelope; concurrent first uploads converge on one template; "unchanged" is an exact comparison of the stored fields, and a draft makes an upload updated; an upload never reactivates a paused workflow or changes the sender; the lock also refuses soft delete and offers a release; key stays optional and the content shape does not change; the SDK has no idempotency option and never retries a send; data maps to variables, and distinctId is attribution only.
Merge-risk danger areas: auth and API scopes (two new actions open to project secret API keys), migrations (new columns, concurrent unique index), the save-and-sync transaction (moved into a service shared by three routes), racing first uploads, and the public contracts of the by-key routes and emails.send.
Pending question: four decision cards.
Send route. (a) POST /messaging_templates/by_key/{key}/send/: mirrors the UUID send route and adds no new URL convention. (b) A shorter send route that takes the key in the body: closer to a general send API, but it sets a new convention alone. Recommend (a), with the SDK route in one constant so a later move is one line.
SDK file reference and upload timing. (a) emails.send with a file reference uploads first, memoized per process: one call, but during a rolling deploy an old instance that starts late puts the old content back, and it can stay there. (b) The file reference only names the key; emails.uploadTemplate(ref) uploads in a deploy step, CI or through the MCP, and send never writes. (c) Key strings only in this slice. Recommend (b): the file stays the single name for the template in code, and no send can revert a newer upload.
Adoption of an existing template. (a) Set-once key through the ordinary template PATCH, with no other field, for people and personal keys; the next upload claims it. (b) An adopt field on the upload that names a UUID: one call, but a project secret key could then claim any template. Recommend (a).
Soft-deleted key. (a) Released: the next upload creates a new template. (b) Reserved: the upload answers 409, and with no restore in the UI the key is stuck. Recommend (a).
Decided by Michael: Card 1 → (b), one send route with the template in the body. Card 3 → (a), set-once key through the ordinary PATCH. Card 4 → (a), a deleted template releases its key. Card 2: a send from a file reference must work with no upload or deploy step, through a content hash.
What changed:
Send route: POST /api/projects/{id}/messaging/send/email/ on a viewset of its own, registered at messaging with a send/email collection action. The send is not an action on one template; its own viewset gets its own allowed actions for project secret keys, throttles and schema; and the path leaves room for other channels. Body: the UUID route's body plus template: {key, hash?} | {id}. 202 {message_id, warnings}. The UUID route stays.
Content hash: SHA-256, v1:<hex>, over the length-prefixed UTF-8 subject, preheader, HTML and text, with only line endings normalized. The upload stops trimming those fields, so both sides hash the same bytes. Python and Node agree on the test vectors in the spec. Stored as content_hash next to key, with superseded_content_hashes (newest 100), both in the #486 migration.
SDK: a file reference sends {key, hash}. On 409 template_outdated or 404 template_not_found it uploads, then retries once. Steady state is one request. emails.uploadTemplate stays only as the explicit rollback path. Agents upload through the MCP tool.
This is a hybrid of Resend's two modes: the file is the source like Resend's inline content, but PostHog stores and versions it like a stored template. Resend never syncs files.
Tickets updated: #486, #487, #488 (now the body-named send route with the hash rule), #489, #490, #492, #474, #475, #476. Stack order and blockers are unchanged.
Pending question: Card 2, what a send carrying a replaced hash does.
A send queues one run of the template's linked workflow. That workflow holds one embedded copy of the content, and the worker always loads its current version. So a send can only deliver the current content, and the question is only what an old hash may do to it.
(i) Remember every published hash (recommended). A replaced hash sends the current content with a template_superseded warning, and the SDK does not upload. Only a hash PostHog never published triggers an upload. An old instance in a rolling deploy can never put old content back, and no email fails. Costs: going back to an earlier file in development does not upload on send (the warning says to upload explicitly), and two people sharing one project with different local files see each other's content.
(ii) Refuse a replaced hash with a clear code. No revert, but real emails such as password resets fail while a rollout runs.
(iii) Send the exact revision the caller holds. Django maps the hash to a workflow version. The CDP route and the queued run carry that version. The worker loads and builds the flow from a stored revision, which Node does not read today, with a cache per flow and version. That changes the CDP hot path across a mixed-version rollout and keeps every old revision sendable. (i) leaves this open for later.
Card 1 → (b): one send route, POST /api/projects/{id}/messaging/send/email/, with template: {key, hash?} | {id} in the body. The UUID route stays. The SDK keeps each route in one constant.
Card 2 → (i): a send from a file reference needs no upload or deploy step. The SDK sends {key, hash}. Every published hash is remembered: a replaced hash sends the current content with a template_superseded warning and never uploads. Only a hash PostHog never published triggers 409 template_outdated, an upload, and one retry.
Card 3 → (a): adoption is a set-once key through the ordinary template PATCH. Never by display name.
Card 4 → (a): a soft-deleted template releases its key.
Hash: SHA-256, v1:<hex>, over the length-prefixed UTF-8 subject, preheader, HTML and text, with only line endings normalized. The spec carries the test vectors.
Upload and send reuse hog_flow:write, the existing flag, throttles and error envelope. No idempotency anywhere. data maps to variables, and distinctId is attribution only.
Merge-risk danger areas: auth and API scopes (a new send viewset and the upload action open to project secret API keys), migrations (#486), the save-and-sync transaction (#487), racing uploads, and the public contracts (send route, upload route, hash format, emails.send).
Activity
Silthus commentedon Oct 16, 2026
Dispatched to T3 thread
thread:project:f66f06fc-0bc1-4bfe-b00a-4aab7f54eb13:1df45b10-ae6e-4d8f-bb24-f50af0ead8af(Opus 5.5, high), worktree from PostHog/posthog master.Silthus commentedon Oct 16, 2026
Parked before the spec started.
The
to-specandto-ticketsskills only run when a person invokes them, and the ticket worker cannot call them. No spec issue or implementation tickets exist yet. #474, #475 and #476 are unchanged.Pending question
/to-specin the ticket worker's thread, then/to-ticketsafter the spec is written. The research inputs are ready.Silthus commentedon Oct 16, 2026
Parked for review.
Spec: #485 Spec: code-managed email templates
Tickets, in stack order (all PostHog/posthog unless noted, native blocking set, all sub-issues of #461):
key,managed_byandsource_*(migration-only, bottom of the stack)PUT .../by_key/{key}/, MCP tool, shared save-and-sync service), blocked by Email templates: migration for key, managed_by, source and content hash columns #486403 immutable, release, blocked by Email templates: upload a template by key from code #487emails.send, retargeted, blocked by Email templates: upload a template by key from code #487 and Email templates: one send route that names the template in the body, with a content hash #488Decisions locked in the spec (on its own, open to change): upload and send by key reuse
hog_flow:write, the existing flag, throttles and error envelope; concurrent first uploads converge on one template; "unchanged" is an exact comparison of the stored fields, and a draft makes an uploadupdated; an upload never reactivates a paused workflow or changes the sender; the lock also refuses soft delete and offers a release;keystays optional and the content shape does not change; the SDK has no idempotency option and never retries a send;datamaps tovariables, anddistinctIdis attribution only.Merge-risk danger areas: auth and API scopes (two new actions open to project secret API keys), migrations (new columns, concurrent unique index), the save-and-sync transaction (moved into a service shared by three routes), racing first uploads, and the public contracts of the by-key routes and
emails.send.Pending question: four decision cards.
POST /messaging_templates/by_key/{key}/send/: mirrors the UUID send route and adds no new URL convention. (b) A shorter send route that takes the key in the body: closer to a general send API, but it sets a new convention alone. Recommend (a), with the SDK route in one constant so a later move is one line.emails.sendwith a file reference uploads first, memoized per process: one call, but during a rolling deploy an old instance that starts late puts the old content back, and it can stay there. (b) The file reference only names the key;emails.uploadTemplate(ref)uploads in a deploy step, CI or through the MCP, andsendnever writes. (c) Key strings only in this slice. Recommend (b): the file stays the single name for the template in code, and no send can revert a newer upload.keythrough the ordinary templatePATCH, with no other field, for people and personal keys; the next upload claims it. (b) Anadoptfield on the upload that names a UUID: one call, but a project secret key could then claim any template. Recommend (a).Silthus commentedon Oct 16, 2026
Revised after Michael's answers. Parked again with one open card.
Spec: #485, revision 2.
Decided by Michael: Card 1 → (b), one send route with the template in the body. Card 3 → (a), set-once
keythrough the ordinaryPATCH. Card 4 → (a), a deleted template releases its key. Card 2: a send from a file reference must work with no upload or deploy step, through a content hash.What changed:
POST /api/projects/{id}/messaging/send/email/on a viewset of its own, registered atmessagingwith asend/emailcollection action. The send is not an action on one template; its own viewset gets its own allowed actions for project secret keys, throttles and schema; and the path leaves room for other channels. Body: the UUID route's body plustemplate: {key, hash?} | {id}.202 {message_id, warnings}. The UUID route stays.v1:<hex>, over the length-prefixed UTF-8 subject, preheader, HTML and text, with only line endings normalized. The upload stops trimming those fields, so both sides hash the same bytes. Python and Node agree on the test vectors in the spec. Stored ascontent_hashnext tokey, withsuperseded_content_hashes(newest 100), both in the #486 migration.{key, hash}. On409 template_outdatedor404 template_not_foundit uploads, then retries once. Steady state is one request.emails.uploadTemplatestays only as the explicit rollback path. Agents upload through the MCP tool.Pending question: Card 2, what a send carrying a replaced hash does.
A send queues one run of the template's linked workflow. That workflow holds one embedded copy of the content, and the worker always loads its current version. So a send can only deliver the current content, and the question is only what an old hash may do to it.
template_supersededwarning, and the SDK does not upload. Only a hash PostHog never published triggers an upload. An old instance in a rolling deploy can never put old content back, and no email fails. Costs: going back to an earlier file in development does not upload on send (the warning says to upload explicitly), and two people sharing one project with different local files see each other's content.Silthus commentedon Oct 16, 2026
Resolved. Michael accepted the spec on 2026-10-16.
Spec: #485 Spec: code-managed email templates
Implementation tickets, in stack order (native blocking set, all sub-issues of #461):
key,managed_by,source_*,content_hash,superseded_content_hashes, concurrent unique index (migration-only)/messaging/send/email/with the template in the body and the hash rule, MCP tool403 immutable, releaseemails.sendfrom a key or an HTML file, synced by content hashsend-transactional-emailDecisions:
POST /api/projects/{id}/messaging/send/email/, withtemplate: {key, hash?} | {id}in the body. The UUID route stays. The SDK keeps each route in one constant.{key, hash}. Every published hash is remembered: a replaced hash sends the current content with atemplate_supersededwarning and never uploads. Only a hash PostHog never published triggers409 template_outdated, an upload, and one retry.keythrough the ordinary templatePATCH. Never by display name.v1:<hex>, over the length-prefixed UTF-8 subject, preheader, HTML and text, with only line endings normalized. The spec carries the test vectors.hog_flow:write, the existing flag, throttles and error envelope. No idempotency anywhere.datamaps tovariables, anddistinctIdis attribution only.Merge-risk danger areas: auth and API scopes (a new send viewset and the upload action open to project secret API keys), migrations (#486), the save-and-sync transaction (#487), racing uploads, and the public contracts (send route, upload route, hash format,
emails.send).