Skip to content

Spec code-managed email templates and slice them into PRs #484

Closed

Description

@Silthus

Question

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-node emails.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.

wayfinder:spec

Activity

  1. self-assigned this
    on Oct 16, 2026
  2. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    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.

  3. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    Parked before the spec started.

    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

    1. 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.
  4. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    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):

    1. #486 migration for key, managed_by and source_* (migration-only, bottom of the stack)
    2. #487 upload a template by key (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 #486
    3. #488 send a template by key (MCP tool), blocked by Email templates: upload a template by key from code #487
    4. #489 "Managed by code" tag, source line and overwrite notice, 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 #488
    5. #490 adopt an existing template by giving it a key, blocked by Email templates: upload a template by key from code #487
    6. #491 server lock: 403 immutable, release, blocked by Email templates: upload a template by key from code #487
    7. #492 read-only view, no autosave, duplicate, stop managing, blocked by Email templates: show "Managed by code" and the source file in the library #489 and Email templates: refuse edits to code-managed templates on the server #491
    8. #474 PostHog/posthog-js: emails.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 #488
    9. #475 PostHog/posthog.com: docs page, retargeted, blocked by posthog-node: emails.send from a key or an HTML file, synced by content hash, returning { data, error } #474, 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 #488
    10. #476 PostHog/context-mill: agent skill, retargeted, blocked by posthog-node: emails.send from a key or an HTML file, synced by content hash, returning { data, error } #474, Docs: transactional email API page for code-managed templates, an anchor per error code #475, 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 #488

    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.

    1. 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.
    2. 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.
    3. 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).
    4. 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).
  5. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    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 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.
  6. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    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):

    # Ticket Repository Blocked by
    1 #486 migration: key, managed_by, source_*, content_hash, superseded_content_hashes, concurrent unique index (migration-only) PostHog/posthog none
    2 #487 upload by key, content hash, shared save-and-sync service, MCP tool PostHog/posthog #486
    3 #488 send route /messaging/send/email/ with the template in the body and the hash rule, MCP tool PostHog/posthog #487
    4 #489 "Managed by code" tag, source line, overwrite notice PostHog/posthog #487, #488
    5 #490 adopt an existing template by giving it a key PostHog/posthog #487
    6 #491 server lock: 403 immutable, release PostHog/posthog #487
    7 #492 read-only view, no autosave, duplicate, stop managing PostHog/posthog #489, #491
    8 #474 emails.send from a key or an HTML file, synced by content hash PostHog/posthog-js #487, #488
    9 #475 docs page with an anchor per code PostHog/posthog.com #474, #487, #488
    10 #476 agent skill send-transactional-email PostHog/context-mill #474, #475, #487, #488

    Decisions:

    • 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).

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions