Skip to content

Spec: copy-as-code and open-a-PR for code-managed templates #502

Open

Description

@Silthus

Map: Map: send code-managed email templates with one PostHog call
Answers: Spec copy-as-code and open-a-PR for code-managed templates
Built on: Spec: code-managed email templates as shipped in PostHog#118924 (integration branch spec/code-managed-templates), the resolutions of #489, #491, #492 and #499, the HTML file contract of PostHog/posthog-js#5312, and the agent skill PostHog/context-mill#433.
Research: #500, report research/template-copy-to-code.md on the fork branch research/template-copy-to-code.

This issue is the reference document for the implementation tickets.

Revision 2 (2026-10-17), Michael's answers to the first three cards:

  • Card 1 → none of a, b, c. People don't edit by hand anymore. PostHog doesn't edit the template; it gives the person a prompt for their coding agent. The visual local edit mode is gone. The read-only preview stays.
  • Card 2 → b. The prompt for the agent is the primary action. Copying the HTML file is secondary.
  • Card 3 → source metadata. The template's source carries the repository, path and ref. posthog-node and the skill's MCP upload fill them. With no repository, the UI still offers the prompt and the copy, with no dialog.

Revision 3 (2026-10-17), Michael's answer to Card 4 → b, widened:

  • "Run with PostHog Code" ships.
  • The core of stack B is that PostHog AI in the sidebar and the PostHog bot in Slack recognize a code-managed template and start a PR, instead of updating it through MCP.
  • So stack B is one server action, "request a change", that starts a PostHog Code task. The web button, both agents and any MCP client use it.
  • The "About this file" prompt block moves to the server, so the copied prompt and the task prompt are the same text from one builder.

Revision 4 (2026-10-17), Michael: an agent that can reach the repository, such as the user's own coding agent over MCP, edits the template's file directly. Going through PostHog makes no sense there. "Request a change" is only for agents and people without access to the repository: PostHog AI in the sidebar, the Slack bot, a chat client with no checkout, and the web button.

No cards are open.

Problem Statement

A code-managed email template lives as an HTML file in the user's repository. PostHog shows it read-only (#492): a preview, a "Managed by code" notice, Duplicate, and "Stop managing in code".

A person who wants to change such a template sees it in PostHog but has no path back to the file:

  • Duplicate makes a different template with no key, so nothing sends it.
  • Stop managing in code makes the next send from code overwrite the change, or lets the file and PostHog drift apart.

The change belongs in the repository, and today the person or their coding agent works out by hand:

  • which file it is;
  • which file format PostHog reads;
  • how the change goes live.

PostHog also often doesn't know the repository. posthog-node's automatic upload stores only a path relative to the app's working directory, so even the source line under the title is incomplete.

Solution

All of it is behind the existing flag workflows-transactional-email-api.

Stack A: edit with your agent.

  • On a code-managed template, the action in the Save slot is "Edit with your agent".
  • It opens a small popover. The person completes one sentence that starts with the file, for example "In emails/welcome.html, change …", and copies a prompt.
  • The prompt adds everything the agent needs:
    • the repository and file;
    • the template's key;
    • the HTML file contract;
    • the variables the file may use;
    • how the change goes live.
  • The person pastes it into their coding agent. The agent edits the file in the repository, and the next send from code uploads it.
  • A side menu offers "Copy HTML file" for a template that has no file yet.
  • PostHog saves nothing. The template, its hash and the server lock stay unchanged.

Stack B: request a change, and PostHog Code opens the PR.

  • "Request a change" is a template action. It takes the person's sentence and starts a PostHog Code task on the template's GitHub repository with the same prompt. The task edits the file and opens a pull request.
  • Web. The popover gains "Run with PostHog Code" next to "Copy prompt".
  • Agents. Every agent checks managed_by before it edits a template. For a code-managed template it never calls the update, patch or design tools. Then:
    • with access to the repository (the user's coding agent over MCP), it edits the file itself, following the template's change_prompt_context. The next send uploads it;
    • without it (PostHog AI in the sidebar, the Slack bot, a chat client with no checkout), it calls the new MCP tool workflows-request-email-template-change and shares the task link.
  • The lock's 403 immutable and the tool descriptions say the same, in that order.
  • Nothing in PostHog changes until the PR is merged and the app sends or uploads the file.

Source metadata (posthog-js and the skill).

  • posthog-node's automatic upload fills source.repository, source.path (relative to the repository root) and source.ref. It reads them from well-known CI and hosting environment variables, then from the git checkout.
  • The skill tells the agent to pass the same three fields when it uploads through MCP.
Template page (read-only preview)
  └─ Edit with your agent ──► "In `emails/welcome.html`, change …" (person completes it)
         └─ Copy prompt ──► coding agent edits the file in the repo ──► commit, deploy ──► next send uploads it
         └─ Run with PostHog Code ──► PostHog Code task ──► pull request ──► merge, deploy ──► next send uploads it
  └─ ▾ Copy HTML file ──► for a template without a file yet

Any agent: "make the welcome email's button green"
  └─ get template ──► managed_by: code
        ├─ repository reachable (user's coding agent) ──► edit the file ──► next send uploads it
        └─ no repository (PostHog AI, Slack bot, chat client) ──► workflows-request-email-template-change ──► PostHog Code task ──► pull request

User Stories

  1. As a marketer, I want to start a change to a code-managed template from its page in PostHog, so that I don't have to find the file myself.
  2. As a marketer, I want to describe the change in my own words in one sentence, so that I don't have to know HTML.
  3. As a marketer, I want the sentence to start with the file's path, so that my agent knows which file to change.
  4. As a marketer, I want PostHog to add the technical context to my sentence, so that my agent gets the change right the first time.
  5. As a marketer, I want to see the full prompt before I copy it, so that I know what I hand to my agent.
  6. As a marketer, I want Copy prompt to wait until I described a change, so that I don't paste an empty instruction.
  7. As a marketer, I want to be told what happens after I copy, so that I know the email changes only after the file is committed and the app sends or uploads it.
  8. As a marketer, I want the preview to stay the page's default, so that opening a code-managed template never looks like I am changing it.
  9. As a coding agent, I want the prompt to name the repository and the file, so that I edit the right file in a monorepo or in a workspace with several repositories.
  10. As a coding agent, I want the prompt to name the template's key, so that I can find the templateFile() call or the upload that uses it.
  11. As a coding agent, I want the prompt to state the file contract (subject in <title>, preview text in <meta name="preheader">, the rest is the HTML), so that my edit keeps the subject and preheader working.
  12. As a coding agent, I want the prompt to say to personalize only with {{ variables.* }}, and to list the variables the file uses, so that I don't introduce values the app never passes.
  13. As a coding agent, I want the prompt to say how the change goes live, so that I neither upload by hand nor forget to upload a template the app sends by key.
  14. As a coding agent, I want a prompt without a known path to tell me how to find the file by its key, so that I can still do the change.
  15. As an engineer, I want to copy a template as an HTML file, so that I can move a template uploaded through the REST API or MCP into a file.
  16. As an engineer, I want the copied file to be in the exact format posthog.emails.templateFile() reads, so that pasting it is the whole change.
  17. As an engineer, I want copying a template that came from my file to give back my file byte for byte, so that a copy never creates noise in a diff.
  18. As an engineer, I want to be told when the template has a plain-text part the file cannot carry, so that I know the text alternative goes away after the next upload from the file.
  19. As an engineer, I want posthog-node to record the repository, the file's path in it and the commit when it uploads a template, so that PostHog and my agent can point at the exact file.
  20. As an engineer, I want that to work on GitHub Actions, GitLab CI, Vercel and Railway without configuration, so that I don't have to pass source details by hand.
  21. As an engineer, I want it to fall back to the local git checkout, so that it also works in development and on servers deployed with their .git folder.
  22. As an engineer, I want an explicit source option to win over detection, so that I can correct an unusual setup.
  23. As an engineer, I want detection never to send a credential that sits in a git remote URL, so that a token never leaves my server.
  24. As an engineer, I want detection to fail quietly when nothing is available, so that a send never fails because of source details.
  25. As a coding agent using the skill, I want to be told to pass the repository, path and commit when I upload through MCP, so that templates I upload point at their file too.
  26. As a marketer without a coding agent, I want PostHog Code to make my change and open a pull request, so that I don't need an engineer to start it.
  27. As a marketer, I want the "Run with PostHog Code" button to say why it's unavailable, so that I know whether to connect GitHub or fix the template's source.
  28. As a marketer, I want a link to the PostHog Code task, so that I can follow it to the pull request.
  29. As a person asking PostHog AI in the sidebar to change a code-managed template, I want it to open a pull request instead of failing on the lock, so that my request still gets done the right way.
  30. As a person asking the PostHog bot in Slack to change an email template, I want it to notice the template is managed by code and open a pull request, so that Slack requests follow the same path.
  31. As a PostHog AI agent, I want the template page to tell me a template is code-managed before my first tool call, so that I pick the right tool at once.
  32. As a user's coding agent connected to PostHog over MCP, I want PostHog's tools to tell me to edit the template's file in the repository I already have, so that the change goes through the user's normal review without a detour through PostHog.
  33. As an engineer, I want the task to change only the template's file and never upload it, so that live email changes only through my repository and deploy.
  34. As a project admin, I want project secret API keys to stay unable to start tasks, so that a key in an app server never gains repository write access.
  35. As the Workflows team, I want events for opening the agent popover, copying the prompt or the file, and requesting a change from each surface, so that we can see whether people use the way back to code and where they drop off.

Implementation Decisions

"Edit with your agent" (stack A)

Placement.

  • A code-managed template shows "Edit with your agent" in the slot where Save sits on other templates.
  • The button has a side menu with "Copy HTML file".
  • The read-only preview, the notice, Duplicate and "Stop managing in code" stay as Email templates: read-only view of code-managed templates #492 built them.
  • The notice's text changes to: "This template is managed by code, so it can't be changed here. To change it, edit its file, or let your coding agent do it."

The popover. Clicking the button opens a popover anchored to it, not a modal and not a source dialog. It holds:

  • A text area labeled "Describe the change", autofocused, prefilled with the starter text, with the cursor at its end. The person completes the sentence.
    • Starter with a known path: In `emails/welcome.html`, change .
    • Starter without one: In the HTML file of the `welcome` email template, change .
  • A collapsed "Show full prompt", which shows the assembled prompt read-only.
  • "Copy prompt", disabled with "Describe the change first" while the text equals the starter or is blank.
  • The text lives only in the page's state, never on the server. It survives closing and reopening the popover until the person leaves the page.

What the prompt contains.

  • The person's sentence comes first, exactly as typed.
  • Then a block that PostHog builds from the saved template only. Each line is omitted when its value is unknown:
<the person's sentence>

About this file:
- It is the PostHog email template with the key `welcome`, in the repository `github.com/example/app`, last uploaded from commit `abc1234`.
- The file is the whole email. The subject is the first `<title>`. The inbox preview text is the `content` of `<meta name="preheader">`. Everything in the file is the HTML that PostHog sends.
- Personalize only with `{{ variables.<name> }}`. The values come from the `data` the app passes to `posthog.emails.send`. This file uses `{{ variables.first_name }}` and `{{ variables.reset_url }}`.
- Do not upload the file by hand. If the app sends it with `posthog.emails.templateFile()`, the next send uploads the changed file. If the app sends it by the key `welcome`, upload the file again after the change (PUT /api/projects/@current/messaging_templates/by_key/welcome/ or the PostHog MCP upload tool).
- The template in PostHog: <absolute link to the template page>
  • Without a path, the first line becomes: "Find the HTML file that the app loads with posthog.emails.templateFile() for the key welcome."
  • The variable list comes from scanning the published subject, preheader and HTML for {{ variables.<name> }}, de-duplicated and in order of first use. With none, the line ends after "posthog.emails.send."
  • A commit ref is shortened to 7 characters.
  • Repository forms. The repository is shown as stored (github.com/owner/name, or owner/name).
  • Code location. See "The change prompt (one builder, on the server)" below.

After copying.

  • A toast says: "Prompt copied. Paste it into your coding agent. PostHog keeps sending the current version until your app sends or uploads the changed file."
  • The popover closes.
  • What happens next is the existing SDK path: the next emails.send with the changed file gets 409 template_outdated, uploads the file, and retries once (Spec: code-managed email templates #485).

Clipboard. Clipboard handling, pending state and the failure toast follow PostHog#104452 (fork branch feat/workflows-copy-code).

The change prompt (one builder, on the server)

  • The messaging product builds the "About this file" block and the starter sentence in one Python service, reached through its facade.
  • The template serializer returns them as read-only fields change_prompt_context and change_prompt_starter, both null unless the template is code-managed.
  • The popover shows the starter and copies <sentence>, a blank line, and change_prompt_context verbatim. There is no prompt builder in the frontend.
  • "Request a change" uses the same block for the PostHog Code task.
  • MCP retrieve returns both fields, so an agent working in the repository sees the same context.
  • Why on the server: the copied prompt and the task prompt must never drift, and agents read the template through the API, not the page.

"Copy HTML file" (stack A)

One pure encoder turns the published template into the file. Its contract is the SDK's parser in PostHog/posthog-js#5312:

  • the subject is the first <title>, with entities decoded and whitespace collapsed and trimmed;
  • the preheader is the first <meta name="preheader">;
  • the HTML is the whole file;
  • there is no text part.

Document. The published HTML, always. There is no editor export any more.

Subject.

  • The encoder writes the subject into the document's first <title>.
  • It adds a title at the start of <head> when there is none, and adds a <head> when the document has none.
  • It escapes &, < and >.

Preheader.

  • The encoder writes the preheader into the first <meta name="preheader">, and adds the tag after the title when it is missing.
  • It escapes & and ".
  • A blank preheader removes the tag.

What stays as it is.

  • A tag whose parsed value already matches stays untouched. An author's own entities, such as &#233;, survive.
  • Liquid stays literal. The encoder never renders, reformats or reorders anything else.

Invariants, each a test:

  • parsing the output with the SDK's rules gives back the template's subject (whitespace-collapsed), its preheader, and the output as the HTML;
  • encoding the output again changes nothing;
  • a template uploaded from a file by the SDK encodes to that file's exact bytes.

Text part.

  • The file never carries text.
  • When the saved template has a nonempty text part, the toast adds: "This file has no plain-text part. The next upload from this file removes the plain-text version."
  • Copy is not blocked.

After copying. The toast says: "Copied. Save it as the template's file. PostHog keeps sending the current version until your app sends or uploads the file."

Request a change: one action for every surface (stack B)

Route. POST /api/projects/{project_id}/messaging_templates/{id}/request_change/.

  • Body: {instruction}, required, at most 2,000 characters.
  • Response: 201 {task_id, task_url, repository}.
  • Typed request, response and error serializers with help_text.

Callers and auth.

  • Session, personal API key and OAuth callers with task:write and read access to the template. The PostHog AI sandbox and Slack runs call it through MCP with OAuth.
  • It is the path for callers without access to the repository. The route can't know what a caller can reach, so the tool descriptions and the skill steer agents with access to the file instead.
  • Project secret API keys are refused.
  • The flag workflows-transactional-email-api applies, and so does the gate that decides whether PostHog Code is available to the project (the one task creation uses).
  • The ownership policy allows the action on code-managed templates only.

The task. It is started through the tasks facade (create_and_run_task):

  • repository: owner/name from source_repository, GitHub only;
  • create_pr: true;
  • title: "Change the <key> email template";
  • description: the instruction, the change prompt block, then "Change only this file. Open a pull request. Do not upload the template or call PostHog's email template tools.";
  • origin: the existing generic user-created origin.

The service never calls save, upload, release or workflow sync. The template and its workflow stay unchanged. PostHog Code owns the branch, the commit and the PR.

Errors, in the #485 envelope (code, detail, why, fix, docs_url):

  • template_not_managed_by_code;
  • template_repository_missing (the fix points at the SDK's detection and source.repository);
  • template_repository_unsupported (not on GitHub);
  • github_repository_not_connected;
  • posthog_code_not_available;
  • instruction_missing;
  • posthog_code_run_failed (the task's start error, passed through).

No idempotency key. The web button guards double submission. The tool description says a repeated call starts another task, as the tasks tools already say.

MCP.

  • A new tool workflows-request-email-template-change in the workflows email template tools, under the same flag, with scope task:write, not read-only, not idempotent. Its description calls it the fallback for agents that cannot edit the template's file.
  • For a code-managed template, the update, patch and design tool descriptions and the fix of 403 immutable say, in this order:
    1. edit <path> in <repository> yourself when you can reach the repository; the next send uploads it;
    2. otherwise call workflows-request-email-template-change.
  • The retrieve tool description says to read managed_by and change_prompt_context before changing a template.

Run with PostHog Code in the popover (stack B)

  • "Run with PostHog Code" sits next to "Copy prompt" and sends the same sentence to the route.
  • Disabled reasons:
    • "Describe the change first";
    • "PostHog doesn't know this template's GitHub repository";
    • "Connect GitHub to run PostHog Code".
  • The button is absent when PostHog Code isn't available to the project.
  • In flight: disabled and loading.
  • Success: a toast and the notice link the task: "PostHog Code is working on it. It opens a pull request when it's done." The sentence is cleared.
  • Errors show detail and fix. The sentence and Copy prompt stay.

Agents: edit the file when you can, request a change when you can't (stack B)

PostHog AI and the Slack bot run the same sandbox agent with PostHog's MCP tools and skills. The Slack bot starts its runs as PostHog Code tasks with the Slack origin. A user's own coding agent reaches the same tools over MCP. So one skill change, one page-context change and the tool texts cover every surface.

The rule is about access, not about which client runs it. An agent that can reach the repository edits the file there. An agent that can't asks PostHog Code.

  • Skill. The designing-email-templates skill gets a "Code-managed templates" section before any edit instruction:
    • check managed_by after reading a template;
    • for code, never call update, patch or design;
    • first choice: when you can reach the template's repository, edit the file at source_path following change_prompt_context. The next send uploads it. Commit or open a pull request the way the person works;
    • only without access to the repository: call workflows-request-email-template-change with the person's request, and share the task link;
    • treat 403 immutable the same way;
    • start the task without an extra confirmation, because the person asked for the change.
  • Page context. On a code-managed template page, the context the sidebar attaches includes managed_by: code, the key and the source.
  • Slack's tool set. The ticket confirms that Slack-started runs load the workflows skills and email template tools. If they don't, the gap goes back to this spec; the ticket doesn't change Slack's tool loading.

Source metadata in posthog-node

This changes automatic uploads only: the upload after template_outdated, and an explicit upload. The send route and the server stay unchanged.

Precedence.

  • Each of repository, path and ref resolves independently: the explicit source option first, then the environment, then the git checkout.
  • The first source that yields a value wins.
  • With no value, the field is omitted, as today.

Repository, stored as host/owner/name.

Order Source Variables
1 source.repository option
2 GitHub Actions GITHUB_SERVER_URL (host) and GITHUB_REPOSITORY
3 GitLab CI CI_SERVER_HOST and CI_PROJECT_PATH
4 Vercel VERCEL_GIT_PROVIDER (github → github.com, gitlab → gitlab.com, bitbucket → bitbucket.org), VERCEL_GIT_REPO_OWNER and VERCEL_GIT_REPO_SLUG
5 Railway RAILWAY_GIT_REPO_OWNER and RAILWAY_GIT_REPO_NAME, on github.com
6 Git checkout The origin URL in the config of the nearest .git directory above the template file
  • Git remote URLs. SSH (git@host:owner/name.git, ssh://git@host/owner/name) and HTTPS forms normalize to host/owner/name.
  • Credentials. User info and any credentials in the URL are dropped before the value leaves the process.
  • No hosting service. A URL with no recognizable host, owner and name yields nothing.

Ref.

Order Source Variables
1 source.ref option
2 GitHub Actions GITHUB_SHA
3 GitLab CI CI_COMMIT_SHA
4 Vercel VERCEL_GIT_COMMIT_SHA
5 Railway RAILWAY_GIT_COMMIT_SHA
6 Git checkout HEAD of the same .git: a commit hash, or the hash its branch points to, through the loose ref file or packed-refs

Path.

Order Source
1 source.path option
2 The template file's path relative to the directory that holds the nearest .git
3 The path relative to GITHUB_WORKSPACE or CI_PROJECT_DIR, when the file is inside it
4 The path relative to the working directory (today's behavior)

Rules for the checkout and the environment.

  • Files only. Detection reads files and never starts a process (no git binary).
  • Worktrees. A .git file that points elsewhere (a worktree) is followed once.
  • Cache. Results are cached per process and per .git directory.
  • Node only. Detection lives in the Node file adapter, next to the existing path handling. Runtimes without a file system skip the checkout step.
  • Never fails a send. Any error in detection yields nothing for that field and never fails or slows a send noticeably.
  • No version-control host. Variables from a provider that the platform doesn't name produce nothing.
  • Documented. The README and the type docs on EmailTemplateSource name the variables and the precedence.

Where it ships.

The skill's MCP upload

PostHog/context-mill#433 tells the agent to pass source on every upload through the MCP upload tool:

  • repository from git remote get-url origin, normalized to host/owner/name without credentials;
  • path relative to the repository root;
  • ref from git rev-parse HEAD.

It also says that posthog-node fills these on its own.

The change is a commit on #433's branch.

Usage events

Frontend posthog.capture. No prompt text, path or repository name in properties.

  • message template agent prompt opened {template_id, has_path, has_repository}
  • message template agent prompt copied {template_id, has_path, has_repository, variable_count}
  • message template html file copied {template_id, dropped_text_part}
  • message template change run requested {template_id} and message template change run failed {template_id, code} (web button)
  • Server side, in the route: message template change requested {template_id, surface}, with surface from the request's event source (web, MCP, API). This is the one event that counts the PostHog AI and Slack path.
  • Adjacent features without tracking get an event in the same slice: message template duplicated {template_id, code_managed} and message template released from code {template_id}. They are flag-gated and unreleased, so the adjacent-usage check has nothing to measure yet.

Schema and data

No migration, no new model, no new scope, no new flag, no CDP change.

  • New: two read-only serializer fields, one route, one MCP tool.
  • Changed: the descriptions of three MCP tools and the fix text of 403 immutable.

Decisions

Michael decided on 2026-10-17:

  • Card 1: PostHog doesn't edit the template. "Edit with your agent" hands the person a prompt.

  • Card 2 → b: the prompt is primary, and the HTML file is secondary.

  • Card 3: the source metadata carries the repository and path. posthog-node and the skill fill them. Without a repository, the prompt and the copy still work, with no dialog.

  • Card 4 → b, widened (revision 3): "Run with PostHog Code" ships, and PostHog AI and the Slack bot route changes to code-managed templates to a PR instead of updating them through MCP.

This spec decided on its own:

  • One server action and one MCP tool serve the web button, PostHog AI, Slack and any MCP client.
  • The prompt block is built once, on the server, and returned on the template. Stack A therefore starts with a small backend slice.
  • (Revision 4, Michael) An agent that can reach the repository edits the file itself. "Request a change" is the fallback for agents and people without access. Tool texts and skills steer the choice; the route doesn't try to detect the caller.
  • PostHog's own send-transactional-email skill for coding agents (Skill: pass the template source on MCP uploads #511) says to change a code-managed template in its file, never through PostHog.
  • No extra confirmation before an agent starts the task.
  • The task uses the existing generic origin, so the tasks model needs no migration. Attribution comes from the server event.
  • The prompt UI is a popover with one text area and the starter text, not a modal and not a multi-field form.
  • The prompt names the key and the upload-by-key route, because a template the app sends by key does not upload on send.
  • The file never carries a text part. A stored text part warns but never blocks, as Spec: code-managed email templates #485 put a text part from a file reference out of scope.
  • Detection order: explicit option, then CI or hosting variables, then the git checkout. The environment wins over the checkout because it names the deployed commit.
  • The SDK change ships as a follow-up PR on top of Think through the Elements tab PostHog/posthog#5312, not inside it.

Testing Decisions

A good test calls a public seam and asserts what a caller sees:

  • the clipboard text;
  • the requests that leave the page;
  • the source fields in the upload request the SDK makes.

It never asserts how a module did it.

Stack B adds:

  • the task arguments the route hands to the tasks facade, mocked at that one function boundary;
  • the tool an agent calls in an eval. Each ticket proves its slice at one or two seams and adds no test an existing one already covers (/writing-tests).

Seams, highest first:

  1. The template page logic (kea logic tests at the API boundary).
    • Covers: the starter text, the disabled copy, the clipboard content with and without source fields, the file copy, and no write request on any copy.
    • Prior art: messageTemplateLogic.test.ts.
  2. The prompt builder and the HTML file encoder, pure functions, against tables of cases.
  3. posthog-node emails.send against the fake PostHog API of Think through the Elements tab PostHog/posthog#5312. It asserts the source the upload sends:
    • for each environment row;
    • for a git checkout in a temporary directory: a branch, a detached head, packed-refs, a worktree, and SSH and HTTPS remotes, one of them with credentials;
    • for the explicit option;
    • for nothing available.
  4. Storybook stories for the popover (with a path, without a path, the expanded full prompt) and the copy toast with the text-part note. Screenshots at 1280px with the side panel open and at full width.
  5. The request-change route (Django APIBaseTest with session, OAuth, personal API key and project secret API key callers). Covers every error code, the exact task arguments, the server event, and the template and workflow unchanged.
  6. Agent evals with the real sandbox agent (/writing-evals), on a seeded project with one code-managed and one GUI template:
    • sidebar on the code-managed page;
    • a Slack-style message naming the template;
    • the GUI template still edited through the patch tool;
    • a coding agent in the repository editing the file itself, without calling workflows-request-email-template-change;
    • a chat agent with no checkout calling workflows-request-email-template-change.
  7. Local proof, not automated:
    • Stack A:
      1. Upload a template through posthog-node from a git checkout.
      2. Open it, complete the sentence, and copy the prompt.
      3. Paste it into a coding agent session in that checkout. The agent edits the file.
      4. Send through the SDK, and see the change in the dev mail catcher.
    • SDK: the stored source of a template uploaded from a checkout and from a simulated Vercel environment, read back through the template API.
    • Stack B: a real PostHog Code task against the private scratch repository Silthus/posthog-workflows-github-scratch, started once from the page and once from the sidebar. The PR changes only the template's file, and the template's hash is unchanged. If PostHog Code can't run on the dev stack, the ticket records the gap.

Out of Scope

  • Editing a code-managed template inside PostHog, visually or as code.
  • Saving or publishing anything from the template page. Upload stays the only publisher of a code-managed template.
  • PostHog writing to the repository itself. PostHog Code owns the branch, the commit and the PR.
  • An idempotency key for "request a change".
  • Changing how the Slack bot loads tools or picks repositories.
  • A dedicated tasks origin for templates.
  • A plain-text part in the file, or a sidecar file format.
  • Asking the person for a repository or path in a dialog.
  • Source detection in SDKs other than Node, and for hosts other than those listed.
  • Changing the server's source fields or their validation.

Further Notes

Tickets

Stack A, PostHog/posthog:

  1. #512 The change prompt on the template API (backend). No blockers.
  2. #503 Edit with your agent: copy the prompt (frontend). Blocked by Code-managed templates: the change prompt on the template API #512.
  3. #509 Copy the template as its HTML file (frontend). Blocked by Edit with your agent: copy a prompt for a code-managed template's file #503.

Stack B, PostHog/posthog, on top of A:
4. #513 Request a change: route, PostHog Code task, MCP tool, lock fix text. Blocked by #512.
5. #514 Run with PostHog Code from the popover. Blocked by #503 and #513.
6. #515 Agents edit the file when they can reach the repository, otherwise request a change: skill, page context, evals. Blocked by #513.

Source metadata:
7. #510 posthog-node fills source.repository, path and ref on upload (PostHog/posthog-js, follow-up PR on PostHog#5312). No blockers.
8. #511 The skill passes source on MCP uploads (PostHog/context-mill#433). Blocked by #510.

Docs, PostHog/posthog.com:
9. #507 Docs section, error anchors and source detection. Blocked by #503, #510 and #513.

Closed as no longer applicable: #504, #505, #506.

Open decisions

None.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions