From fa8aeb187b27a885684b74f841f241f0dab50017 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:02:06 +0000 Subject: [PATCH 01/14] feat(skills): add send-transactional-email skill Teaches a coding agent to send one transactional email from server code with PostHog: verify a sender through the PostHog MCP first, keep the template as an HTML file in the repository, upload it by key, and send it with posthog-node or the HTTP API. Ships the transactional email API docs page as its reference. Co-Authored-By: Claude Opus 5.5 --- .../send-transactional-email/config.yaml | 12 ++ .../send-transactional-email/description.md | 116 ++++++++++++++++++ .../send-transactional-email-skill.test.js | 57 +++++++++ 3 files changed, 185 insertions(+) create mode 100644 context/skills/omnibus/send-transactional-email/config.yaml create mode 100644 context/skills/omnibus/send-transactional-email/description.md create mode 100644 scripts/lib/tests/send-transactional-email-skill.test.js diff --git a/context/skills/omnibus/send-transactional-email/config.yaml b/context/skills/omnibus/send-transactional-email/config.yaml new file mode 100644 index 00000000..9c30fee1 --- /dev/null +++ b/context/skills/omnibus/send-transactional-email/config.yaml @@ -0,0 +1,12 @@ +# Transactional email skill - one email to one person, from a template file in the repository +type: skill +template: description.md +category: workflows +description: Send transactional email from code with PostHog, such as a welcome email, a password reset, a signup confirmation, or a receipt. Use when asked to send an email from a Node.js app or other server code. It verifies a sender through the PostHog MCP, writes the email as an HTML template file in the repository, uploads it by key, and writes the send with posthog-node or the HTTP API. +tags: [workflows] +shared_docs: + - https://posthog.com/docs/workflows/transactional-email-api.md +variants: + - id: all + display_name: all supported platforms + tags: [] diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md new file mode 100644 index 00000000..84d76337 --- /dev/null +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -0,0 +1,116 @@ +# Send transactional email with PostHog + +Use this skill to send one email to one person from server code, such as a welcome email, a password reset, or a signup confirmation. The email lives in the repository as an HTML template file. PostHog stores it as a message template under a key, such as `welcome`, and sends it through Workflows. + +The transactional email API is in beta. PostHog enables it per project. + +## Instructions + +Follow these steps IN ORDER. Do not upload a template before STEP 3 is done: the first upload needs a verified default sender. + +STEP 1: Detect the platform. + - Find the server code that should send the email, such as the signup handler for a welcome email. + - Node.js (including Next.js route handlers and server actions) uses `posthog-node`. Any other server language uses the HTTP API. + - Never send from browser or mobile code. The send needs a secret key, and the key stays on the server. + - Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb) to pick the package manager. + +STEP 2: Check for a verified sender through the PostHog MCP. + - Call `integrations-list` with `kind=email`. A sender with `config.verified: true` can send. When the list shows senders without `config`, ask the person whether one is verified, or let the upload in STEP 5 tell you: `sender_unavailable` means there is no verified default sender. + - The first sender that verifies becomes the project's default sender, and the first upload uses the default sender. + - When a verified sender exists, go to STEP 4. + +STEP 3: Set up a sender when none is verified. + - First check that the PostHog MCP has the sender tools: `integrations-email-create`, `integrations-email-verify-create`, `integrations-domain-connect-check-retrieve`, and `integrations-domain-connect-apply-url-create`. When they are missing, go to the last bullet of this step. + - When the `setting-up-an-email-domain` skill is available, follow it. It covers the details. + - Ask which domain to send from. Recommend a subdomain such as `mail.example.com`. Use the person's own domain, never a free mailbox domain such as gmail.com. + - Create the sender, then get its DNS records with `integrations-email-verify-create`. + - PostHog never writes DNS. When the domain supports Domain Connect, give the person the URL from `integrations-domain-connect-apply-url-create`: they approve the records at their DNS host. Otherwise, give them the records as a table with type, name, value, and priority. + - Call `integrations-email-verify-create` again after the records are published, at least a minute apart, until `status` is `success`. + - **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue. + +STEP 4: Write the template as an HTML file in the repository. + - Put it next to the server code, for example `emails/welcome.html`. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. + - The `` is the subject. `<meta name="preheader" content="...">` is the optional inbox preview text. + - Use Liquid with `{{ variables.<name> }}` for every value the email shows, such as `{{ variables.first_name }}` or `{{ variables.reset_url }}`. + - Never use `{{ person.* }}`. A send doesn't load person properties, so those placeholders render empty. + +STEP 5: Upload the template with the PostHog MCP. + - Call `workflows-upload-email-template` with the key, a `name` such as "Welcome", the `<title>` text as `subject`, the `<meta name="preheader">` content as `preheader` (if any), and the whole file as `html`. + - Pass `source` with `repository` (such as `github.com/acme/app`), `path` (the file path in the repository), and `ref` (the current commit), so the person sees the file in PostHog next to the template. + - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: edits happen in the file, not in PostHog. + - On `sender_unavailable`, the project has no verified default sender. Ask the person to click **Make default** next to their verified sender in the email channel settings, then upload again. + +STEP 6: Stop when the API is not enabled for the project. + - When the PostHog MCP has no `workflows-upload-email-template` or `workflows-send-email` tool, or an upload or send answers a plain `404` with the code `not_found`, the transactional email API is not enabled for this project. + - Stop and tell the person: the API is in beta, and they can ask PostHog in-app to enable it for their project. Do not retry, and do not work around it with other tools. + +STEP 7: Have the person create a project secret API key. + - Ask the person to create a project secret API key (it starts with `phs_`) with the `hog_flow:write` scope in their PostHog project settings. + - Read it from the `POSTHOG_PROJECT_SECRET_API_KEY` environment variable. Add the variable name to the project's env example file. Never hardcode the key, never commit it, and never ask the person to paste it into the chat. + +STEP 8: Write the send. + **Node.js:** install or upgrade `posthog-node` to a version that has `posthog.emails`. Reference the file once when the module loads, then send it: + + ```js + import { PostHog } from 'posthog-node' + + const posthog = new PostHog(process.env.POSTHOG_PROJECT_TOKEN, { + host: process.env.POSTHOG_HOST, + secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY, + enableLocalEvaluation: false, + }) + const welcomeEmail = posthog.emails.templateFile(new URL('./emails/welcome.html', import.meta.url)) + + const { data, error } = await posthog.emails.send({ + template: welcomeEmail, + to: user.email, + data: { first_name: user.firstName }, + distinctId: user.id, + }) + ``` + + - Reuse the app's existing `posthog-node` client when it has one, and add `secretKey`. Keep `enableLocalEvaluation: false` unless the key also has `feature_flag:read` and the app uses local evaluation. + - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. + - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. + - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. + - Serverless bundles must ship the HTML file. See "Ship template files with serverless bundles" in the reference. + + **Other languages:** send by key over HTTP, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY`. The host is the app host, `https://us.posthog.com` or `https://eu.posthog.com`: + + ``` + POST <host>/api/projects/@current/messaging/send/email/ + {"template": {"key": "welcome"}, "to": "jane@example.com", "identifiers": {"distinct_id": "user_42"}, "message_data": {"first_name": "Jane"}} + ``` + + - Also add an upload by key wherever the project deploys, such as a CI step or a release script: `PUT <host>/api/projects/@current/messaging_templates/by_key/<key>/` with `name`, `subject`, `html`, `preheader`, and `source`. Otherwise an edited file never reaches PostHog. + + For both: + - Pass every value the template reads in `data` (Node.js) or `message_data` (HTTP). + - Pass the app's user ID as `distinctId` (Node.js) or `identifiers.distinct_id` (HTTP), so the send and its delivery events show on that person. + - Sends are not idempotent: two calls send two emails. Do not retry a send after a timeout or a server error, and never call the send more than once for one event. + - To try it, run the code path once, or ask the person before you send a test email with `workflows-send-email` to an address they own. + +STEP 9: Handle errors by their code. + - `posthog-node` never throws on API errors. It returns `{ data, error }`. Branch on `error.code` and log `error.message`. + - Over HTTP, read `code` from the error body. New codes also carry `fix` and `docs_url`. + - Each code has a section with its fix in the reference. The common ones: + - `sender_unavailable`: no verified default sender. Go back to STEP 3. + - `not_found`: the API is not enabled for the project. See STEP 6. + - `template_not_found`: no template has this key. Upload it (STEP 5). + - `template_file_unreadable`: the deployment doesn't include the HTML file. + - `missing_secret_key`, `authentication_failed`, `permission_denied`: the `phs_` key is missing, or lacks `hog_flow:write`. + - `immutable`: the template is managed by code. Change the file and upload it, instead of editing the template in PostHog. + +## Reference files + +{references} + +The reference is the transactional email API page. It covers the template format, `posthog-node`, both routes, the content hash, and every error code. + +## Key principles + +- **Sender first**: verify a sender before the first upload. +- **The file is the source**: the HTML file in the repository is the template. Change the file, never the copy in PostHog. +- **Values from the send**: templates read `{{ variables.* }}` only. +- **Server only**: the `phs_` key comes from an environment variable on the server. +- **No retries**: sends are not idempotent. diff --git a/scripts/lib/tests/send-transactional-email-skill.test.js b/scripts/lib/tests/send-transactional-email-skill.test.js new file mode 100644 index 00000000..470ae143 --- /dev/null +++ b/scripts/lib/tests/send-transactional-email-skill.test.js @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +const CONFIG_DIR = join(process.cwd(), 'context'); +const DOCS_URL = 'https://posthog.com/docs/workflows/transactional-email-api.md'; +const DOCS_PAGE = '# Transactional email API\n\nSend one email to one person from your server code.\n'; + +async function buildSkill(outputDir) { + vi.resetModules(); + vi.stubEnv('DOCS_CACHE_TTL_MS', '0'); + vi.stubGlobal('fetch', async (url) => { + if (url !== DOCS_URL) return new Response('not found', { status: 404, statusText: 'Not Found' }); + return new Response(DOCS_PAGE, { status: 200 }); + }); + const { expandSkillGroups, generateSkill, loadSkillsConfig } = await import('../skill-generator.js'); + const skill = expandSkillGroups(loadSkillsConfig(CONFIG_DIR), CONFIG_DIR) + .find((candidate) => candidate.id === 'omnibus-send-transactional-email'); + expect(skill).toBeDefined(); + + await generateSkill({ + skill, + version: 'test', + repoRoot: process.cwd(), + configDir: CONFIG_DIR, + outputDir, + skipPatterns: { global: [], examples: {} }, + commandmentsConfig: { commandments: {} }, + skillTemplate: skill._template, + sharedDocs: skill._sharedDocs, + }); + return join(outputDir, 'omnibus-send-transactional-email'); +} + +describe('send-transactional-email skill', () => { + let outputDir; + + afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + if (outputDir) rmSync(outputDir, { recursive: true, force: true }); + }); + + it('ships the transactional email API docs page as its reference', async () => { + outputDir = mkdtempSync(join(tmpdir(), 'send-email-skill-')); + + const skillDir = await buildSkill(outputDir); + + const skillFile = readFileSync(join(skillDir, 'SKILL.md'), 'utf8'); + const referenceFile = join(skillDir, 'references', 'transactional-email-api.md'); + expect(skillFile).toMatch(/^---\nname: omnibus-send-transactional-email\n/); + expect(skillFile).toContain('`references/transactional-email-api.md`'); + expect(existsSync(referenceFile)).toBe(true); + expect(readFileSync(referenceFile, 'utf8')).toBe(DOCS_PAGE); + }); +}); From 5a1f8e193dec583ee83d3ae588b6e9b1c0d63b82 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:12:52 +0000 Subject: [PATCH 02/14] fix(skills): tighten send-transactional-email after review Check the PostHog MCP before any sender work, never recreate an existing sender, bound verification polling, set the default sender through a verify, ask before any test send, and cover Next.js and edge runtimes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../send-transactional-email/description.md | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index 84d76337..35d20373 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -6,16 +6,18 @@ The transactional email API is in beta. PostHog enables it per project. ## Instructions -Follow these steps IN ORDER. Do not upload a template before STEP 3 is done: the first upload needs a verified default sender. +Follow these steps IN ORDER. Do not upload a template before a sender is verified: the first upload needs a verified default sender. -STEP 1: Detect the platform. +STEP 1: Detect the platform and check the PostHog MCP. - Find the server code that should send the email, such as the signup handler for a welcome email. - Node.js (including Next.js route handlers and server actions) uses `posthog-node`. Any other server language uses the HTTP API. - Never send from browser or mobile code. The send needs a secret key, and the key stays on the server. - Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb) to pick the package manager. + - Check which PostHog MCP tools exist. When there are no PostHog tools at all, the PostHog MCP is not connected or not signed in: ask the person to connect it, and wait. + - When other PostHog tools exist but `workflows-upload-email-template` and `workflows-send-email` don't, the transactional email API is not enabled for this project. Go to STEP 6 before the person does any sender work. STEP 2: Check for a verified sender through the PostHog MCP. - - Call `integrations-list` with `kind=email`. A sender with `config.verified: true` can send. When the list shows senders without `config`, ask the person whether one is verified, or let the upload in STEP 5 tell you: `sender_unavailable` means there is no verified default sender. + - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, ask the person whether one is verified. - The first sender that verifies becomes the project's default sender, and the first upload uses the default sender. - When a verified sender exists, go to STEP 4. @@ -23,9 +25,12 @@ STEP 3: Set up a sender when none is verified. - First check that the PostHog MCP has the sender tools: `integrations-email-create`, `integrations-email-verify-create`, `integrations-domain-connect-check-retrieve`, and `integrations-domain-connect-apply-url-create`. When they are missing, go to the last bullet of this step. - When the `setting-up-an-email-domain` skill is available, follow it. It covers the details. - Ask which domain to send from. Recommend a subdomain such as `mail.example.com`. Use the person's own domain, never a free mailbox domain such as gmail.com. - - Create the sender, then get its DNS records with `integrations-email-verify-create`. - - PostHog never writes DNS. When the domain supports Domain Connect, give the person the URL from `integrations-domain-connect-apply-url-create`: they approve the records at their DNS host. Otherwise, give them the records as a table with type, name, value, and priority. - - Call `integrations-email-verify-create` again after the records are published, at least a minute apart, until `status` is `success`. + - Never create a sender for an address that already has one: creating it again marks it unverified. For an existing unverified sender, skip to its records. + - Create the sender with `integrations-email-create`, then get its DNS records with `integrations-email-verify-create`. + - PostHog never writes DNS. Call `integrations-domain-connect-check-retrieve` with the domain. When `supported` is true, give the person the URL from `integrations-domain-connect-apply-url-create`: they approve the records at their DNS host. Otherwise, give them the records that are not `success` as a table with type, name, value, and priority. + - Always include both SPF records, merged into an existing `v=spf1` record when there is one. Never add a second `_dmarc` record when the domain already has one. + - Call `integrations-email-verify-create` again after the records are published, at least a minute apart. `success` means done. `failed` means fix the records, then verify again. After about 15 minutes of `pending` with every record published, stop and tell the person that DNS is still propagating and they can ask you to check again later. + - A 403 that says the person lacks permissions in the project means a project admin must verify the sender. Do not retry. - **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue. STEP 4: Write the template as an HTML file in the repository. @@ -36,12 +41,15 @@ STEP 4: Write the template as an HTML file in the repository. STEP 5: Upload the template with the PostHog MCP. - Call `workflows-upload-email-template` with the key, a `name` such as "Welcome", the `<title>` text as `subject`, the `<meta name="preheader">` content as `preheader` (if any), and the whole file as `html`. - - Pass `source` with `repository` (such as `github.com/acme/app`), `path` (the file path in the repository), and `ref` (the current commit), so the person sees the file in PostHog next to the template. + - Pass `source` with `repository` (such as `github.com/example/app`), `path` (the file path in the repository), and `ref` (the current commit), so the person sees the file in PostHog next to the template. - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: edits happen in the file, not in PostHog. - - On `sender_unavailable`, the project has no verified default sender. Ask the person to click **Make default** next to their verified sender in the email channel settings, then upload again. + - On `sender_unavailable`, the project has no verified default sender: + - When no sender is verified, go back to STEP 3. + - When a sender is verified, call `integrations-email-verify-create` on it once: a successful verify makes it the default when the project has none. Then upload again. + - When that is not possible or the upload still answers `sender_unavailable`, ask the person to click **Make default** next to their verified sender in the email channel settings, then upload again. STEP 6: Stop when the API is not enabled for the project. - - When the PostHog MCP has no `workflows-upload-email-template` or `workflows-send-email` tool, or an upload or send answers a plain `404` with the code `not_found`, the transactional email API is not enabled for this project. + - The transactional email API is not enabled for this project when other PostHog tools exist but the upload and send tools don't, or when an upload or send answers `404` without a code or with the code `not_found`. A `404` with `template_not_found` is different: see STEP 9. - Stop and tell the person: the API is in beta, and they can ask PostHog in-app to enable it for their project. Do not retry, and do not work around it with other tools. STEP 7: Have the person create a project secret API key. @@ -59,7 +67,7 @@ STEP 8: Write the send. secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY, enableLocalEvaluation: false, }) - const welcomeEmail = posthog.emails.templateFile(new URL('./emails/welcome.html', import.meta.url)) + const welcomeEmail = posthog.emails.templateFile(new URL('./emails/welcome.html', import.meta.url), { name: 'Welcome' }) const { data, error } = await posthog.emails.send({ template: welcomeEmail, @@ -71,9 +79,11 @@ STEP 8: Write the send. - Reuse the app's existing `posthog-node` client when it has one, and add `secretKey`. Keep `enableLocalEvaluation: false` unless the key also has `feature_flag:read` and the app uses local evaluation. - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. + - Pass the same `name` as the upload in STEP 5. Without it, the next automatic upload renames the template to its subject. - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. + - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js`. For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. + - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. - - Serverless bundles must ship the HTML file. See "Ship template files with serverless bundles" in the reference. **Other languages:** send by key over HTTP, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY`. The host is the app host, `https://us.posthog.com` or `https://eu.posthog.com`: @@ -88,14 +98,14 @@ STEP 8: Write the send. - Pass every value the template reads in `data` (Node.js) or `message_data` (HTTP). - Pass the app's user ID as `distinctId` (Node.js) or `identifiers.distinct_id` (HTTP), so the send and its delivery events show on that person. - Sends are not idempotent: two calls send two emails. Do not retry a send after a timeout or a server error, and never call the send more than once for one event. - - To try it, run the code path once, or ask the person before you send a test email with `workflows-send-email` to an address they own. + - Every test sends a real email. Ask the person before any test send, whether you run the code path or call `workflows-send-email`, and send only to an address they own. STEP 9: Handle errors by their code. - `posthog-node` never throws on API errors. It returns `{ data, error }`. Branch on `error.code` and log `error.message`. - Over HTTP, read `code` from the error body. New codes also carry `fix` and `docs_url`. - Each code has a section with its fix in the reference. The common ones: - - `sender_unavailable`: no verified default sender. Go back to STEP 3. - - `not_found`: the API is not enabled for the project. See STEP 6. + - `sender_unavailable`: no verified default sender. Follow STEP 5. + - `not_found`, or a `404` without a code: the API is not enabled for the project. See STEP 6. - `template_not_found`: no template has this key. Upload it (STEP 5). - `template_file_unreadable`: the deployment doesn't include the HTML file. - `missing_secret_key`, `authentication_failed`, `permission_denied`: the `phs_` key is missing, or lacks `hog_flow:write`. From 3d85758e330b795355567c1ce81bd7bde89a041d Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:15:12 +0000 Subject: [PATCH 03/14] fix(skills): never rerun a test send to read its output Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- context/skills/omnibus/send-transactional-email/description.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index 35d20373..ef2705f5 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -98,7 +98,7 @@ STEP 8: Write the send. - Pass every value the template reads in `data` (Node.js) or `message_data` (HTTP). - Pass the app's user ID as `distinctId` (Node.js) or `identifiers.distinct_id` (HTTP), so the send and its delivery events show on that person. - Sends are not idempotent: two calls send two emails. Do not retry a send after a timeout or a server error, and never call the send more than once for one event. - - Every test sends a real email. Ask the person before any test send, whether you run the code path or call `workflows-send-email`, and send only to an address they own. + - Every test sends a real email. Ask the person before any test send, whether you run the code path or call `workflows-send-email`, and send only to an address they own. Save the test run's full output to a file and read it from there: never run it again to see more output, because each run sends another email. STEP 9: Handle errors by their code. - `posthog-node` never throws on API errors. It returns `{ data, error }`. Branch on `error.code` and log `error.message`. From 711998df31fcb5fcb021b933bb76dc6c35dc22e5 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:19:31 +0000 Subject: [PATCH 04/14] fix(skills): verify an unknown sender instead of asking Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../skills/omnibus/send-transactional-email/description.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index ef2705f5..de81e33f 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -17,7 +17,7 @@ STEP 1: Detect the platform and check the PostHog MCP. - When other PostHog tools exist but `workflows-upload-email-template` and `workflows-send-email` don't, the transactional email API is not enabled for this project. Go to STEP 6 before the person does any sender work. STEP 2: Check for a verified sender through the PostHog MCP. - - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, ask the person whether one is verified. + - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address the person wants: `success` means it is verified (go to STEP 4), and `pending` or `failed` means STEP 3 from its records. When the sender tools are missing, ask the person whether a sender is verified, and let the upload in STEP 5 confirm it. - The first sender that verifies becomes the project's default sender, and the first upload uses the default sender. - When a verified sender exists, go to STEP 4. @@ -34,7 +34,7 @@ STEP 3: Set up a sender when none is verified. - **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue. STEP 4: Write the template as an HTML file in the repository. - - Put it next to the server code, for example `emails/welcome.html`. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. + - Put it next to the server code, for example `emails/welcome.html`. In Next.js and other bundled code, put the `emails/` folder at the project root. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. - The `<title>` is the subject. `<meta name="preheader" content="...">` is the optional inbox preview text. - Use Liquid with `{{ variables.<name> }}` for every value the email shows, such as `{{ variables.first_name }}` or `{{ variables.reset_url }}`. - Never use `{{ person.* }}`. A send doesn't load person properties, so those placeholders render empty. @@ -81,7 +81,7 @@ STEP 8: Write the send. - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. - Pass the same `name` as the upload in STEP 5. Without it, the next automatic upload renames the template to its subject. - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. - - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js`. For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. + - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. From 965a656ea1d4f7b6612186d2a8b0575842302dee Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 03:23:08 +0000 Subject: [PATCH 05/14] fix(skills): route a refused verify to the person Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../skills/omnibus/send-transactional-email/description.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index de81e33f..c9970afe 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -17,7 +17,7 @@ STEP 1: Detect the platform and check the PostHog MCP. - When other PostHog tools exist but `workflows-upload-email-template` and `workflows-send-email` don't, the transactional email API is not enabled for this project. Go to STEP 6 before the person does any sender work. STEP 2: Check for a verified sender through the PostHog MCP. - - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address the person wants: `success` means it is verified (go to STEP 4), and `pending` or `failed` means STEP 3 from its records. When the sender tools are missing, ask the person whether a sender is verified, and let the upload in STEP 5 confirm it. + - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address the person wants: `success` means it is verified (go to STEP 4), and `pending` or `failed` means STEP 3 from its records. When the sender tools are missing, or the verify answers 403, ask the person whether a sender is verified, and let the upload in STEP 5 confirm it. - The first sender that verifies becomes the project's default sender, and the first upload uses the default sender. - When a verified sender exists, go to STEP 4. @@ -34,7 +34,7 @@ STEP 3: Set up a sender when none is verified. - **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue. STEP 4: Write the template as an HTML file in the repository. - - Put it next to the server code, for example `emails/welcome.html`. In Next.js and other bundled code, put the `emails/` folder at the project root. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. + - Put it next to the server code, for example `emails/welcome.html`. In Next.js and other bundled code, put the `emails/` folder at the app root, next to `next.config.js` or `package.json`. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. - The `<title>` is the subject. `<meta name="preheader" content="...">` is the optional inbox preview text. - Use Liquid with `{{ variables.<name> }}` for every value the email shows, such as `{{ variables.first_name }}` or `{{ variables.reset_url }}`. - Never use `{{ person.* }}`. A send doesn't load person properties, so those placeholders render empty. @@ -81,7 +81,7 @@ STEP 8: Write the send. - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. - Pass the same `name` as the upload in STEP 5. Without it, the next automatic upload renames the template to its subject. - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. - - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. + - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14 and earlier). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. From 5127da72970724912abdd62076c589d68d500cc1 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:34:04 +0000 Subject: [PATCH 06/14] feat(skills): record a template's file on upload and change it in the file The MCP upload passes source.repository from the origin remote without credentials, source.path from the repository root and source.ref from HEAD. A new section sends a change to a code-managed template to its file and leaves the upload to posthog-node or the deploy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../send-transactional-email/description.md | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index c9970afe..96f88ea1 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -41,8 +41,13 @@ STEP 4: Write the template as an HTML file in the repository. STEP 5: Upload the template with the PostHog MCP. - Call `workflows-upload-email-template` with the key, a `name` such as "Welcome", the `<title>` text as `subject`, the `<meta name="preheader">` content as `preheader` (if any), and the whole file as `html`. - - Pass `source` with `repository` (such as `github.com/example/app`), `path` (the file path in the repository), and `ref` (the current commit), so the person sees the file in PostHog next to the template. - - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: edits happen in the file, not in PostHog. + - Pass `source`, so PostHog and the next agent can find the file: + - `repository`: the URL from `git remote get-url origin` as `host/owner/name`. Drop the scheme, any user name or token, and the `.git` ending: `git@github.com:example/app.git` and `https://user:token@github.com/example/app.git` both become `github.com/example/app`. + - `path`: the file's path from the repository root (`git rev-parse --show-toplevel`), such as `apps/web/emails/welcome.html` in a monorepo. + - `ref`: the commit from `git rev-parse HEAD`. + - Leave out a field that git can't give, such as `repository` when there is no `origin`. + - When the app sends with `templateFile()` (STEP 8), posthog-node fills the same three fields on the uploads it makes, from CI and hosting variables or the git checkout. A field it can't find keeps the value this upload stored. + - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: changes go through its file (see "Change an email the app already sends"). - On `sender_unavailable`, the project has no verified default sender: - When no sender is verified, go back to STEP 3. - When a sender is verified, call `integrations-email-verify-create` on it once: a successful verify makes it the default when the project has none. Then upload again. @@ -92,7 +97,7 @@ STEP 8: Write the send. {"template": {"key": "welcome"}, "to": "jane@example.com", "identifiers": {"distinct_id": "user_42"}, "message_data": {"first_name": "Jane"}} ``` - - Also add an upload by key wherever the project deploys, such as a CI step or a release script: `PUT <host>/api/projects/@current/messaging_templates/by_key/<key>/` with `name`, `subject`, `html`, `preheader`, and `source`. Otherwise an edited file never reaches PostHog. + - Also add an upload by key wherever the project deploys, such as a CI step or a release script: `PUT <host>/api/projects/@current/messaging_templates/by_key/<key>/` with `name`, `subject`, `html`, `preheader`, and `source` as in STEP 5. Otherwise an edited file never reaches PostHog. For both: - Pass every value the template reads in `data` (Node.js) or `message_data` (HTTP). @@ -109,7 +114,17 @@ STEP 9: Handle errors by their code. - `template_not_found`: no template has this key. Upload it (STEP 5). - `template_file_unreadable`: the deployment doesn't include the HTML file. - `missing_secret_key`, `authentication_failed`, `permission_denied`: the `phs_` key is missing, or lacks `hog_flow:write`. - - `immutable`: the template is managed by code. Change the file and upload it, instead of editing the template in PostHog. + - `immutable`: the template is managed by code. Change its file, as "Change an email the app already sends" says. + +## Change an email the app already sends + +A code-managed template changes in its file in the repository. You work in the repository, so edit the file yourself: + - Find the file from the template's `source.path`, or from the `templateFile()` call or upload that uses its key. When the person pastes a prompt from **Edit with your agent** on the template's page in PostHog, the prompt names the file and the key. + - Keep the format of STEP 4, and read only `{{ variables.* }}` values that the send passes. + - When the app sends with `templateFile()`, the change goes live with the first send after the person deploys it: posthog-node uploads the changed file then. Leave the upload to it, so the new version goes live with the code that is reviewed and deployed. Upload it yourself as in STEP 5 only when the person asks for the change to go live before that, and tell them that every running copy of the app sends the new version from then on. + - When the app sends by key, the upload step from STEP 8 uploads the file when the project deploys. When the project has no such step, upload the file again as in STEP 5, with a fresh `source`, once the person wants the change live. + - Change the template only through its file. PostHog's template update, patch, and design tools refuse a code-managed template with `403 immutable`. Never call `workflows-request-email-template-change` either: where the PostHog MCP has it, it starts a PostHog Code task for agents that can't reach the repository. + - Leave the template managed by code. Releasing it in PostHog is the person's decision, and the next upload from the file claims it back. ## Reference files From 99eaa9a7821de622ddec1a5171bb75b15c9be914 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:44:21 +0000 Subject: [PATCH 07/14] fix(skills): cover undo, early publish and no-repository changes A reverted file goes live only through an upload, because a send with an older published hash gets the newer content. Any send from the changed code publishes it. A by-key change is uploaded after the change. An agent without the repository tells the person or requests the change. The SDK always sends a path, so only repository and ref keep their stored value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../send-transactional-email/description.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index 96f88ea1..c3e98ced 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -46,7 +46,7 @@ STEP 5: Upload the template with the PostHog MCP. - `path`: the file's path from the repository root (`git rev-parse --show-toplevel`), such as `apps/web/emails/welcome.html` in a monorepo. - `ref`: the commit from `git rev-parse HEAD`. - Leave out a field that git can't give, such as `repository` when there is no `origin`. - - When the app sends with `templateFile()` (STEP 8), posthog-node fills the same three fields on the uploads it makes, from CI and hosting variables or the git checkout. A field it can't find keeps the value this upload stored. + - posthog-node fills the same three fields on its own uploads when the app sends with `templateFile()` (STEP 8). It reads them from CI and hosting variables or the git checkout. It always sends a `path`: without a checkout or CI workspace, the path is relative to the app's working directory. A `repository` or `ref` it can't find keeps the value this upload stored. - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: changes go through its file (see "Change an email the app already sends"). - On `sender_unavailable`, the project has no verified default sender: - When no sender is verified, go back to STEP 3. @@ -118,14 +118,22 @@ STEP 9: Handle errors by their code. ## Change an email the app already sends -A code-managed template changes in its file in the repository. You work in the repository, so edit the file yourself: - - Find the file from the template's `source.path`, or from the `templateFile()` call or upload that uses its key. When the person pastes a prompt from **Edit with your agent** on the template's page in PostHog, the prompt names the file and the key. +A code-managed template changes in its file in the repository. + +When you can edit the repository, change the file yourself: + - Find the file by the template's key: the `templateFile()` call or the upload that uses it. The template's `source.path` points at it too, but posthog-node can store it relative to the app's directory. A prompt from **Edit with your agent** on the template's page in PostHog names the key, and the file when PostHog knows it. - Keep the format of STEP 4, and read only `{{ variables.* }}` values that the send passes. - - When the app sends with `templateFile()`, the change goes live with the first send after the person deploys it: posthog-node uploads the changed file then. Leave the upload to it, so the new version goes live with the code that is reviewed and deployed. Upload it yourself as in STEP 5 only when the person asks for the change to go live before that, and tell them that every running copy of the app sends the new version from then on. - - When the app sends by key, the upload step from STEP 8 uploads the file when the project deploys. When the project has no such step, upload the file again as in STEP 5, with a fresh `source`, once the person wants the change live. - - Change the template only through its file. PostHog's template update, patch, and design tools refuse a code-managed template with `403 immutable`. Never call `workflows-request-email-template-change` either: where the PostHog MCP has it, it starts a PostHog Code task for agents that can't reach the repository. + - Change the template only through its file. PostHog's template update, patch, and design tools refuse a code-managed template with `403 immutable`. - Leave the template managed by code. Releasing it in PostHog is the person's decision, and the next upload from the file claims it back. +How the change goes live: + - **`templateFile()`:** the first send from code with the changed file uploads it. Usually that is the deployed app, after review. A local run or a test send against the same project publishes it too, so say so when you ask before a test send. Leave the upload to posthog-node. + - **Live before the deploy:** upload the file as in STEP 5 only when the person asks for that. Tell them that every running copy of the app sends the new version from then on. + - **Undo:** a change back to content PostHog sent before goes live only through an upload as in STEP 5. A send from the reverted file gets the newer content, and posthog-node doesn't upload it. + - **By key:** a send never uploads. Upload the file again as in STEP 5 after the change, or leave it to the project's deploy step from STEP 8 when it has one. + +Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead: it starts a PostHog Code task that changes the file and opens a pull request. An agent that can edit the file never calls it. + ## Reference files {references} From c128595a959f05da3748b4bc20c889c6bf17d033 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:48:09 +0000 Subject: [PATCH 08/14] fix(skills): scope undo to templateFile and order the by-key upload A revert to published content needs an upload only for templateFile(). A by-key change waits for the deploy step when there is one. A test through workflows-send-email publishes nothing, and an agent without the repository shares the PostHog Code task link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../omnibus/send-transactional-email/description.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index c3e98ced..08d484a6 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -88,7 +88,7 @@ STEP 8: Write the send. - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14 and earlier). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. + - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. A revert to an earlier version is the exception: see "Change an email the app already sends". **Other languages:** send by key over HTTP, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY`. The host is the app host, `https://us.posthog.com` or `https://eu.posthog.com`: @@ -127,12 +127,12 @@ When you can edit the repository, change the file yourself: - Leave the template managed by code. Releasing it in PostHog is the person's decision, and the next upload from the file claims it back. How the change goes live: - - **`templateFile()`:** the first send from code with the changed file uploads it. Usually that is the deployed app, after review. A local run or a test send against the same project publishes it too, so say so when you ask before a test send. Leave the upload to posthog-node. + - **`templateFile()`:** the first send from code with the changed file uploads it. Usually that is the deployed app, after review. A local run of that code against the same project publishes it too. Say so when you ask before a test send from the code. Leave the upload to posthog-node. - **Live before the deploy:** upload the file as in STEP 5 only when the person asks for that. Tell them that every running copy of the app sends the new version from then on. - - **Undo:** a change back to content PostHog sent before goes live only through an upload as in STEP 5. A send from the reverted file gets the newer content, and posthog-node doesn't upload it. - - **By key:** a send never uploads. Upload the file again as in STEP 5 after the change, or leave it to the project's deploy step from STEP 8 when it has one. + - **Undo with `templateFile()`:** a revert to content PostHog published before goes live only through an upload as in STEP 5. A send from the reverted file gets the newer content, and posthog-node doesn't upload it. + - **By key:** a send never uploads. The project's deploy step from STEP 8 uploads the change. Without one, upload the file again as in STEP 5 after the change. -Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead: it starts a PostHog Code task that changes the file and opens a pull request. An agent that can edit the file never calls it. +Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead and share the task link: it starts a PostHog Code task that changes the file and opens a pull request. An agent that can edit the file never calls it. ## Reference files From 8b236c30c6774e3b77163ff626f3c242c041902c Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:52:28 +0000 Subject: [PATCH 09/14] fix(skills): name a published revert in STEP 8 and split the fallback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../skills/omnibus/send-transactional-email/description.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index 08d484a6..d5b813d3 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -88,7 +88,7 @@ STEP 8: Write the send. - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14 and earlier). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. A revert to an earlier version is the exception: see "Change an email the app already sends". + - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. A revert to content PostHog published before is the exception: see "Change an email the app already sends". **Other languages:** send by key over HTTP, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY`. The host is the app host, `https://us.posthog.com` or `https://eu.posthog.com`: @@ -132,7 +132,7 @@ How the change goes live: - **Undo with `templateFile()`:** a revert to content PostHog published before goes live only through an upload as in STEP 5. A send from the reverted file gets the newer content, and posthog-node doesn't upload it. - **By key:** a send never uploads. The project's deploy step from STEP 8 uploads the change. Without one, upload the file again as in STEP 5 after the change. -Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead and share the task link: it starts a PostHog Code task that changes the file and opens a pull request. An agent that can edit the file never calls it. +Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead. It starts a PostHog Code task that changes the file and opens a pull request. Share the task link with the person. An agent that can edit the file never calls it. ## Reference files From c626ddf3f6cf1473509255d8ac1ad5813f6666c3 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:51:19 +0000 Subject: [PATCH 10/14] feat(skills): teach the flat send contract in send-transactional-email The skill now writes a local template in the app's code and sends it with templateKey, subject, html and templateData. The first send uploads it, so the MCP upload, templateFile() and the deploy-time upload by key go. Rendered content and templates made in PostHog (templateId) are the other two forms. Sends carry an idempotency key and retry with it. Job runners pass maxRetries: 0 and a key derived from the email's purpose. STEP 9 lists the contract's codes, including the 409 for a template edited in PostHog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../send-transactional-email/config.yaml | 4 +- .../send-transactional-email/description.md | 124 +++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/config.yaml b/context/skills/omnibus/send-transactional-email/config.yaml index 9c30fee1..e99c4a9d 100644 --- a/context/skills/omnibus/send-transactional-email/config.yaml +++ b/context/skills/omnibus/send-transactional-email/config.yaml @@ -1,8 +1,8 @@ -# Transactional email skill - one email to one person, from a template file in the repository +# Transactional email skill - one email to one person, from a template in the app's code type: skill template: description.md category: workflows -description: Send transactional email from code with PostHog, such as a welcome email, a password reset, a signup confirmation, or a receipt. Use when asked to send an email from a Node.js app or other server code. It verifies a sender through the PostHog MCP, writes the email as an HTML template file in the repository, uploads it by key, and writes the send with posthog-node or the HTTP API. +description: Send transactional email from code with PostHog, such as a welcome email, a password reset, a signup confirmation, or a receipt. Use when asked to send an email from a Node.js app or other server code. It verifies a sender through the PostHog MCP, writes the email as a local template in the app's code, and writes the send with posthog-node's emails.send or the HTTP API. The first send uploads the template. tags: [workflows] shared_docs: - https://posthog.com/docs/workflows/transactional-email-api.md diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index d5b813d3..c47ab563 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -1,12 +1,12 @@ # Send transactional email with PostHog -Use this skill to send one email to one person from server code, such as a welcome email, a password reset, or a signup confirmation. The email lives in the repository as an HTML template file. PostHog stores it as a message template under a key, such as `welcome`, and sends it through Workflows. +Use this skill to send one email to one person from server code, such as a welcome email, a password reset, or a signup confirmation. The email lives in the app's code as a local template: a subject and an HTML string, named by a key such as `welcome`. The first send uploads it to PostHog, and PostHog sends it through Workflows. The transactional email API is in beta. PostHog enables it per project. ## Instructions -Follow these steps IN ORDER. Do not upload a template before a sender is verified: the first upload needs a verified default sender. +Follow these steps IN ORDER. Do not send before a sender is verified: the first send of a local template uploads it, and that upload needs a verified default sender. STEP 1: Detect the platform and check the PostHog MCP. - Find the server code that should send the email, such as the signup handler for a welcome email. @@ -17,8 +17,8 @@ STEP 1: Detect the platform and check the PostHog MCP. - When other PostHog tools exist but `workflows-upload-email-template` and `workflows-send-email` don't, the transactional email API is not enabled for this project. Go to STEP 6 before the person does any sender work. STEP 2: Check for a verified sender through the PostHog MCP. - - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address the person wants: `success` means it is verified (go to STEP 4), and `pending` or `failed` means STEP 3 from its records. When the sender tools are missing, or the verify answers 403, ask the person whether a sender is verified, and let the upload in STEP 5 confirm it. - - The first sender that verifies becomes the project's default sender, and the first upload uses the default sender. + - Call `integrations-list` with `kind=email`, and read every page. A sender with `config.verified: true` can send. When the list shows senders without `config`, call `integrations-email-verify-create` once on the sender for the address the person wants: `success` means it is verified (go to STEP 4), and `pending` or `failed` means STEP 3 from its records. When the sender tools are missing, or the verify answers 403, ask the person whether a sender is verified, and let the first send in STEP 8 confirm it. + - The first sender that verifies becomes the project's default sender. A send without `from` uses the default sender. - When a verified sender exists, go to STEP 4. STEP 3: Set up a sender when none is verified. @@ -33,28 +33,20 @@ STEP 3: Set up a sender when none is verified. - A 403 that says the person lacks permissions in the project means a project admin must verify the sender. Do not retry. - **When the sender tools are missing or refused**, the agent setup for sender domains is not available to this person yet. Do not try other integration tools. Ask the person to verify a sender in PostHog: Workflows, then Channels, then add an email sender on their domain and follow the DNS steps (https://posthog.com/docs/workflows/configure-channels). Wait until they confirm that the sender shows as verified, then continue. -STEP 4: Write the template as an HTML file in the repository. - - Put it next to the server code, for example `emails/welcome.html`. In Next.js and other bundled code, put the `emails/` folder at the app root, next to `next.config.js` or `package.json`. The file name without `.html` is the key, so use letters, digits, `-`, and `_` only. - - The `<title>` is the subject. `<meta name="preheader" content="...">` is the optional inbox preview text. - - Use Liquid with `{{ variables.<name> }}` for every value the email shows, such as `{{ variables.first_name }}` or `{{ variables.reset_url }}`. - - Never use `{{ person.* }}`. A send doesn't load person properties, so those placeholders render empty. +STEP 4: Pick the form of the send. + - **Local template**, the default for a new email: the code holds the subject and the HTML with `{{ variables.* }}` placeholders, and each send passes the values in `templateData`. Write it as STEP 5 says. + - **Rendered content**: the app already renders the whole email for one person, for example a React Email component rendered with `render()` from `@react-email/render`. Send the rendered `html` without `templateData`. PostHog sends it as it is and never stores it. Skip STEP 5. + - **A template made in PostHog**: only when the person names one. Send it by its `templateId` with `templateData`. Skip STEP 5. -STEP 5: Upload the template with the PostHog MCP. - - Call `workflows-upload-email-template` with the key, a `name` such as "Welcome", the `<title>` text as `subject`, the `<meta name="preheader">` content as `preheader` (if any), and the whole file as `html`. - - Pass `source`, so PostHog and the next agent can find the file: - - `repository`: the URL from `git remote get-url origin` as `host/owner/name`. Drop the scheme, any user name or token, and the `.git` ending: `git@github.com:example/app.git` and `https://user:token@github.com/example/app.git` both become `github.com/example/app`. - - `path`: the file's path from the repository root (`git rev-parse --show-toplevel`), such as `apps/web/emails/welcome.html` in a monorepo. - - `ref`: the commit from `git rev-parse HEAD`. - - Leave out a field that git can't give, such as `repository` when there is no `origin`. - - posthog-node fills the same three fields on its own uploads when the app sends with `templateFile()` (STEP 8). It reads them from CI and hosting variables or the git checkout. It always sends a `path`: without a checkout or CI workspace, the path is relative to the app's working directory. A `repository` or `ref` it can't find keeps the value this upload stored. - - Expect `result: "created"` on the first upload, and `updated` or `unchanged` later. The template is now managed by code: changes go through its file (see "Change an email the app already sends"). - - On `sender_unavailable`, the project has no verified default sender: - - When no sender is verified, go back to STEP 3. - - When a sender is verified, call `integrations-email-verify-create` on it once: a successful verify makes it the default when the project has none. Then upload again. - - When that is not possible or the upload still answers `sender_unavailable`, ask the person to click **Make default** next to their verified sender in the email channel settings, then upload again. +STEP 5: Write the local template in the code. + - Put it in a module next to the server code, for example `emails/welcome.ts`, that exports a `subject` and an `html` string. Add a `text` string only when the person wants their own plain-text part: without it, PostHog derives one from the HTML. + - Pick the key that names the email, such as `welcome` or `password-reset`. Use letters, digits, `-`, and `_` only. PostHog groups the email's logs, metrics, and status under it. + - Use Liquid with `{{ variables.<name> }}` for every value the email shows, in the subject too, such as `{{ variables.first_name }}` or `{{ variables.reset_url }}`. + - Never use `{{ person.* }}`. A send doesn't load person properties, so those placeholders render empty. + - Never upload the template, and never create it in PostHog. The first send uploads it, and a send after a change uploads the new content. The template is then managed by code: changes go through the code (see "Change an email the app already sends"). STEP 6: Stop when the API is not enabled for the project. - - The transactional email API is not enabled for this project when other PostHog tools exist but the upload and send tools don't, or when an upload or send answers `404` without a code or with the code `not_found`. A `404` with `template_not_found` is different: see STEP 9. + - The transactional email API is not enabled for this project when other PostHog tools exist but the upload and send tools don't, or when a send by `templateKey` answers `404` with the code `not_found` or without a code. A send by `templateId` answers the same `404` when no template has that id: check the id with the person first. - Stop and tell the person: the API is in beta, and they can ask PostHog in-app to enable it for their project. Do not retry, and do not work around it with other tools. STEP 7: Have the person create a project secret API key. @@ -62,88 +54,98 @@ STEP 7: Have the person create a project secret API key. - Read it from the `POSTHOG_PROJECT_SECRET_API_KEY` environment variable. Add the variable name to the project's env example file. Never hardcode the key, never commit it, and never ask the person to paste it into the chat. STEP 8: Write the send. - **Node.js:** install or upgrade `posthog-node` to a version that has `posthog.emails`. Reference the file once when the module loads, then send it: + **Node.js:** install or upgrade `posthog-node` to a version that has `posthog.emails`. Send the local template from STEP 5: ```js import { PostHog } from 'posthog-node' + import * as welcomeEmail from './emails/welcome' const posthog = new PostHog(process.env.POSTHOG_PROJECT_TOKEN, { host: process.env.POSTHOG_HOST, secretKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY, enableLocalEvaluation: false, }) - const welcomeEmail = posthog.emails.templateFile(new URL('./emails/welcome.html', import.meta.url), { name: 'Welcome' }) const { data, error } = await posthog.emails.send({ - template: welcomeEmail, to: user.email, - data: { first_name: user.firstName }, distinctId: user.id, + templateKey: 'welcome', + subject: welcomeEmail.subject, + html: welcomeEmail.html, + templateData: { first_name: user.firstName }, }) ``` + - Rendered content sends `templateKey`, `subject`, and the rendered `html` without `templateData`: `html: await render(<PasswordReset url={url} />)`. + - A template made in PostHog sends `templateId` and `templateData`, and no `templateKey`, `subject`, or `html`. - Reuse the app's existing `posthog-node` client when it has one, and add `secretKey`. Keep `enableLocalEvaluation: false` unless the key also has `feature_flag:read` and the app uses local evaluation. - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. - - Pass the same `name` as the upload in STEP 5. Without it, the next automatic upload renames the template to its subject. - - `templateFile()` takes a `file:` URL or an absolute path. In CommonJS, use `path.join(__dirname, 'emails/welcome.html')`. - - In Next.js and other bundled or serverless code, `import.meta.url` no longer points next to the file. Use `path.join(process.cwd(), 'emails/welcome.html')`, and make the deployment ship the file: in Next.js, add `{ '/api/signup': ['./emails/**/*.html'] }` (the route that sends) to `outputFileTracingIncludes` in `next.config.js` (under `experimental` in Next.js 14 and earlier). For Vercel and Netlify functions, see "Ship template files with serverless bundles" in the reference. - - Edge runtimes have no file system, so they take a key string only: `template: 'welcome'`. Upload the file from a build step or a Node.js script there. - - The file reference keeps PostHog in step with the file: each send carries the file's content hash, and after an edit the next send uploads the file on its own. No deploy step is needed. A revert to content PostHog published before is the exception: see "Change an email the app already sends". + - When `host` is a reverse proxy or a self-hosted instance, also set `appHost` to the PostHog app URL, such as `https://us.posthog.com`. posthog-node never sends the secret key to a proxy, and without `appHost` the send fails with `missing_app_host`. + - Leave out `from` to send from the default sender. Pass `from`, and `replyTo` if the person wants one, only with an address on a verified sender, such as `'Example <hello@mail.example.com>'`. - **Other languages:** send by key over HTTP, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY`. The host is the app host, `https://us.posthog.com` or `https://eu.posthog.com`: + **Other languages:** send over HTTP to the app host, `https://us.posthog.com` or `https://eu.posthog.com`, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY` and an `Idempotency-Key` header: ``` POST <host>/api/projects/@current/messaging/send/email/ - {"template": {"key": "welcome"}, "to": "jane@example.com", "identifiers": {"distinct_id": "user_42"}, "message_data": {"first_name": "Jane"}} + Idempotency-Key: welcome:user_42 + {"to": "jane@example.com", "identifiers": {"distinct_id": "user_42"}, "template_key": "welcome", "subject": "Welcome to Example", "html": "<p>Hi Jane</p>"} ``` - - Also add an upload by key wherever the project deploys, such as a CI step or a release script: `PUT <host>/api/projects/@current/messaging_templates/by_key/<key>/` with `name`, `subject`, `html`, `preheader`, and `source` as in STEP 5. Otherwise an edited file never reaches PostHog. + - That body sends rendered content. A local template sends `"template": {"key": "welcome", "hash": "<content hash>"}` and `message_data` instead, with the hash computed as the reference says. + - A local template answers `409 template_outdated` when PostHog doesn't hold its content yet, on its first send too. Upload it with `PUT <host>/api/projects/@current/messaging_templates/by_key/<key>/` and `if_code_managed: true`, then send again with the same `Idempotency-Key`. + - A template made in PostHog sends `"template": {"id": "<template id>"}` and `message_data`. For both: - - Pass every value the template reads in `data` (Node.js) or `message_data` (HTTP). - - Pass the app's user ID as `distinctId` (Node.js) or `identifiers.distinct_id` (HTTP), so the send and its delivery events show on that person. - - Sends are not idempotent: two calls send two emails. Do not retry a send after a timeout or a server error, and never call the send more than once for one event. + - Pass every value the template reads in `templateData` (Node.js) or `message_data` (HTTP). + - Pass the app's user ID as `distinctId` (Node.js) or `identifiers.distinct_id` (HTTP), so the send and its delivery events show on that person. The email's engagement events count as analytics events. + - Every send has an idempotency key, and a retry with the same key never sends a second email. posthog-node makes a key when you pass none, and retries network errors, throttling, and server outages with it. + - When a job runner or a queue retries the code that sends, a generated key doesn't survive the retry. Pass `maxRetries: 0` and an `idempotencyKey` derived from the email's purpose, such as `` `welcome:${user.id}` ``. Over HTTP, derive the `Idempotency-Key` the same way. - Every test sends a real email. Ask the person before any test send, whether you run the code path or call `workflows-send-email`, and send only to an address they own. Save the test run's full output to a file and read it from there: never run it again to see more output, because each run sends another email. STEP 9: Handle errors by their code. - - `posthog-node` never throws on API errors. It returns `{ data, error }`. Branch on `error.code` and log `error.message`. - - Over HTTP, read `code` from the error body. New codes also carry `fix` and `docs_url`. + - `posthog-node` never throws on API errors. It returns `{ data, error }`. Branch on `error.code`, and log `error.message`, `error.why`, and `error.fix`. `error.name` follows Resend's error names. + - Over HTTP, read `code`, `why`, `fix`, and `docs_url` from the error body. + - A send with warnings still sent. posthog-node logs each warning once and returns them in `data.warnings`. - Each code has a section with its fix in the reference. The common ones: - - `sender_unavailable`: no verified default sender. Follow STEP 5. - - `not_found`, or a `404` without a code: the API is not enabled for the project. See STEP 6. - - `template_not_found`: no template has this key. Upload it (STEP 5). - - `template_file_unreadable`: the deployment doesn't include the HTML file. - - `missing_secret_key`, `authentication_failed`, `permission_denied`: the `phs_` key is missing, or lacks `hog_flow:write`. - - `immutable`: the template is managed by code. Change its file, as "Change an email the app already sends" says. + - `sender_unavailable`: the first send of a local template found no verified default sender. Verify one as in STEP 2, or ask the person to click **Make default** next to their verified sender in the email channel settings. + - `sender_not_verified`, `sender_not_allowed`: `from` is not a verified sender. Leave `from` out, or verify its domain (STEP 3). + - `invalid_recipient`, `recipient_suppressed`: PostHog won't send to this address. Do not retry. + - `sending_suspended`, `template_paused`: sending is stopped for the project or the template. Tell the person, and do not retry. + - `not_found`: see STEP 6. + - `template_outdated`: posthog-node uploads the template and sends again on its own. Over HTTP, follow STEP 8. + - A `409` that says the template was edited in PostHog: nothing was sent. Ask the person whether to keep the code's version or PostHog's, and never switch to another key to get around it. + - `invalid_idempotent_request`: the key was already used for another email. Make the key name the email's whole purpose. + - `quota_limited`: the project is over its email quota. Do not retry. + - `missing_secret_key`, a `401`, or a `403`: the `phs_` key is missing, or lacks `hog_flow:write`. + - `missing_app_host`: set `appHost` (STEP 8). + - `immutable`: the template is managed by code. Change it in the code, as "Change an email the app already sends" says. ## Change an email the app already sends -A code-managed template changes in its file in the repository. +A local template changes in the code. -When you can edit the repository, change the file yourself: - - Find the file by the template's key: the `templateFile()` call or the upload that uses it. The template's `source.path` points at it too, but posthog-node can store it relative to the app's directory. A prompt from **Edit with your agent** on the template's page in PostHog names the key, and the file when PostHog knows it. - - Keep the format of STEP 4, and read only `{{ variables.* }}` values that the send passes. - - Change the template only through its file. PostHog's template update, patch, and design tools refuse a code-managed template with `403 immutable`. - - Leave the template managed by code. Releasing it in PostHog is the person's decision, and the next upload from the file claims it back. +When you can edit the repository, change the code yourself: + - Find the template by its key: the `templateKey` of the send. + - Keep the format of STEP 5, and read only `{{ variables.* }}` values that the send passes. + - Change the template only in the code. PostHog's template update, patch, and design tools refuse a code-managed template with `403 immutable`. How the change goes live: - - **`templateFile()`:** the first send from code with the changed file uploads it. Usually that is the deployed app, after review. A local run of that code against the same project publishes it too. Say so when you ask before a test send from the code. Leave the upload to posthog-node. - - **Live before the deploy:** upload the file as in STEP 5 only when the person asks for that. Tell them that every running copy of the app sends the new version from then on. - - **Undo with `templateFile()`:** a revert to content PostHog published before goes live only through an upload as in STEP 5. A send from the reverted file gets the newer content, and posthog-node doesn't upload it. - - **By key:** a send never uploads. The project's deploy step from STEP 8 uploads the change. Without one, upload the file again as in STEP 5 after the change. + - The first send with the changed content uploads it. Usually that is the deployed app, after review. + - A local run of that code against the same project publishes it too. Say so when you ask before a test send. + - A send can come back with the warning `template_superseded`, for example after a revert to content PostHog sent before. PostHog sent its newer content: follow the warning's `fix`. -Without access to the repository, tell the person which file to change. Where the PostHog MCP has `workflows-request-email-template-change`, call it instead. It starts a PostHog Code task that changes the file and opens a pull request. Share the task link with the person. An agent that can edit the file never calls it. +Without access to the repository, tell the person which code to change. ## Reference files {references} -The reference is the transactional email API page. It covers the template format, `posthog-node`, both routes, the content hash, and every error code. +The reference is the transactional email API page. It covers the send forms, `posthog-node`, the content hash, idempotency, and every error code. ## Key principles -- **Sender first**: verify a sender before the first upload. -- **The file is the source**: the HTML file in the repository is the template. Change the file, never the copy in PostHog. +- **Sender first**: verify a sender before the first send. +- **The code is the source**: the local template in the code is the email. Change the code, never the copy in PostHog. - **Values from the send**: templates read `{{ variables.* }}` only. - **Server only**: the `phs_` key comes from an environment variable on the server. -- **No retries**: sends are not idempotent. +- **One email per key**: retries reuse the idempotency key, so a retry never sends twice. From 05f2f104da95a39502aedd42eacc580a4f84da34 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:04:41 +0000 Subject: [PATCH 11/14] feat(skills): add a Wizard section to send-transactional-email Inside the Wizard the agent asks through wizard_ask, writes the secret key with set_env_values, reaches PostHog tools through posthog_exec, and ends a blocked run with an [ABORT] reason the Wizard's transactional-email program matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../omnibus/send-transactional-email/description.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index c47ab563..f2108345 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -136,6 +136,18 @@ How the change goes live: Without access to the repository, tell the person which code to change. +## Inside the PostHog Wizard + +Read this section only when the `mcp__wizard-tools__wizard_ask` tool exists. The Wizard has no chat, so the person answers through its tools. + +- The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>` before you decide it is missing. +- Ask every question of this skill with `wizard_ask`, never in plain text. Batch the questions of one step into one call. +- When the person did not say which email to send, ask them. Offer a welcome email after signup as the first choice. +- STEP 7: ask for the project secret API key with `wizard_ask` and `sensitive: true`, then write the answer with `mcp__wizard-tools__set_env_values` as `POSTHOG_PROJECT_SECRET_API_KEY`. The answer is a reference that only `set_env_values` can read, so the key never shows in the run. +- When STEP 6 stops the run, emit `[ABORT] transactional email api not enabled` and stop. +- When the project has no server code that can send, emit `[ABORT] no server code found` and stop. +- Ask before a test send as STEP 8 says. When the person says no, skip it. + ## Reference files {references} From db4baf89d2523f9735c4b5b41981091998609359 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:38:40 +0000 Subject: [PATCH 12/14] fix(skills): treat missing tools as inconclusive inside the Wizard The Wizard's login may lack the workflows and integrations scopes, so a hidden tool there no longer stops the run as "API not enabled". The Wizard section names its tools as the runtime shows them, and replyTo no longer needs a verified sender. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../omnibus/send-transactional-email/description.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index f2108345..703f9131 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -81,7 +81,8 @@ STEP 8: Write the send. - Reuse the app's existing `posthog-node` client when it has one, and add `secretKey`. Keep `enableLocalEvaluation: false` unless the key also has `feature_flag:read` and the app uses local evaluation. - When the project token or the host is not configured yet, get the token from the PostHog MCP `projects-get` tool (`api_token`). The host is `https://us.i.posthog.com` for US Cloud and `https://eu.i.posthog.com` for EU Cloud. Ask the person which one when you can't tell. - When `host` is a reverse proxy or a self-hosted instance, also set `appHost` to the PostHog app URL, such as `https://us.posthog.com`. posthog-node never sends the secret key to a proxy, and without `appHost` the send fails with `missing_app_host`. - - Leave out `from` to send from the default sender. Pass `from`, and `replyTo` if the person wants one, only with an address on a verified sender, such as `'Example <hello@mail.example.com>'`. + - Leave out `from` to send from the default sender. Pass `from` only with an address on a verified sender, such as `'Example <hello@mail.example.com>'`. + - Pass `replyTo` when replies should go to another address, such as a support mailbox. It takes one address or a list, and needs no verified sender. **Other languages:** send over HTTP to the app host, `https://us.posthog.com` or `https://eu.posthog.com`, with `Authorization: Bearer $POSTHOG_PROJECT_SECRET_API_KEY` and an `Idempotency-Key` header: @@ -138,13 +139,14 @@ Without access to the repository, tell the person which code to change. ## Inside the PostHog Wizard -Read this section only when the `mcp__wizard-tools__wizard_ask` tool exists. The Wizard has no chat, so the person answers through its tools. +Read this section only when the Wizard's `wizard_ask` tool exists. Some runtimes name its tools `mcp__wizard-tools__wizard_ask` and `mcp__wizard-tools__set_env_values`: use the names the runtime shows. The Wizard has no chat, so the person answers through its tools. -- The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>` before you decide it is missing. +- The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>`. +- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Skip the last bullet of STEP 1. When the sender tools are missing, take the last bullet of STEP 3. - Ask every question of this skill with `wizard_ask`, never in plain text. Batch the questions of one step into one call. - When the person did not say which email to send, ask them. Offer a welcome email after signup as the first choice. -- STEP 7: ask for the project secret API key with `wizard_ask` and `sensitive: true`, then write the answer with `mcp__wizard-tools__set_env_values` as `POSTHOG_PROJECT_SECRET_API_KEY`. The answer is a reference that only `set_env_values` can read, so the key never shows in the run. -- When STEP 6 stops the run, emit `[ABORT] transactional email api not enabled` and stop. +- STEP 7: ask for the project secret API key with `wizard_ask` and `sensitive: true`, then pass the answer to `set_env_values` as `POSTHOG_PROJECT_SECRET_API_KEY`. The answer is a reference that only `set_env_values` can read, so the key never shows in the run. +- When a send answers the `404` of STEP 6, emit `[ABORT] transactional email api not enabled` and stop. - When the project has no server code that can send, emit `[ABORT] no server code found` and stop. - Ask before a test send as STEP 8 says. When the person says no, skip it. From 161a6fd23a76f8edc2ea8d6dd6333f928a0c599d Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:41:58 +0000 Subject: [PATCH 13/14] fix(skills): let only a send's 404 stop a Wizard run in STEP 6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- context/skills/omnibus/send-transactional-email/description.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index 703f9131..ac9d6a13 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -142,7 +142,7 @@ Without access to the repository, tell the person which code to change. Read this section only when the Wizard's `wizard_ask` tool exists. Some runtimes name its tools `mcp__wizard-tools__wizard_ask` and `mcp__wizard-tools__set_env_values`: use the names the runtime shows. The Wizard has no chat, so the person answers through its tools. - The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>`. -- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Skip the last bullet of STEP 1. When the sender tools are missing, take the last bullet of STEP 3. +- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Skip the last bullet of STEP 1, and in STEP 6 only a send's `404` shows that the API is off. When the sender tools are missing, take the last bullet of STEP 3. - Ask every question of this skill with `wizard_ask`, never in plain text. Batch the questions of one step into one call. - When the person did not say which email to send, ask them. Offer a welcome email after signup as the first choice. - STEP 7: ask for the project secret API key with `wizard_ask` and `sensitive: true`, then pass the answer to `set_env_values` as `POSTHOG_PROJECT_SECRET_API_KEY`. The answer is a reference that only `set_env_values` can read, so the key never shows in the run. From 5f1a05dcd41294c58d43c4bcfdc3ee5a31367137 Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:19:31 +0000 Subject: [PATCH 14/14] fix(skills): let a Wizard run go on past STEP 6 and report a skipped test send Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../skills/omnibus/send-transactional-email/description.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/context/skills/omnibus/send-transactional-email/description.md b/context/skills/omnibus/send-transactional-email/description.md index ac9d6a13..3118d200 100644 --- a/context/skills/omnibus/send-transactional-email/description.md +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -142,13 +142,13 @@ Without access to the repository, tell the person which code to change. Read this section only when the Wizard's `wizard_ask` tool exists. Some runtimes name its tools `mcp__wizard-tools__wizard_ask` and `mcp__wizard-tools__set_env_values`: use the names the runtime shows. The Wizard has no chat, so the person answers through its tools. - The PostHog tools of this skill run through `posthog_exec`. Check that a tool exists with `search <name>`. -- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Skip the last bullet of STEP 1, and in STEP 6 only a send's `404` shows that the API is off. When the sender tools are missing, take the last bullet of STEP 3. +- The Wizard's login may not carry the scopes of the `workflows-*` and `integrations-*` tools, so a missing tool does not mean the API is off. Skip the last bullet of STEP 1. STEP 6 applies only after a send answers its `404`; until then, go on to STEP 7. When the sender tools are missing, take the last bullet of STEP 3. - Ask every question of this skill with `wizard_ask`, never in plain text. Batch the questions of one step into one call. - When the person did not say which email to send, ask them. Offer a welcome email after signup as the first choice. - STEP 7: ask for the project secret API key with `wizard_ask` and `sensitive: true`, then pass the answer to `set_env_values` as `POSTHOG_PROJECT_SECRET_API_KEY`. The answer is a reference that only `set_env_values` can read, so the key never shows in the run. - When a send answers the `404` of STEP 6, emit `[ABORT] transactional email api not enabled` and stop. - When the project has no server code that can send, emit `[ABORT] no server code found` and stop. -- Ask before a test send as STEP 8 says. When the person says no, skip it. +- Ask before a test send as STEP 8 says. When the person says no, skip it, and write in the report that no send has confirmed the API is enabled and the sender is verified. ## Reference files