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..e99c4a9d --- /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 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 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 +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..3118d200 --- /dev/null +++ b/context/skills/omnibus/send-transactional-email/description.md @@ -0,0 +1,165 @@ +# 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 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 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. + - 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`, 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. + - 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. + - 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: 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: 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. }}` 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 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. + - 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`. 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 { data, error } = await posthog.emails.send({ + to: user.email, + 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()`. + - 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. + - 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` only with an address on a verified sender, such as `'Example '`. + - 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: + + ``` + POST /api/projects/@current/messaging/send/email/ + Idempotency-Key: welcome:user_42 + {"to": "jane@example.com", "identifiers": {"distinct_id": "user_42"}, "template_key": "welcome", "subject": "Welcome to Example", "html": "

Hi Jane

"} + ``` + + - That body sends rendered content. A local template sends `"template": {"key": "welcome", "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 /api/projects/@current/messaging_templates/by_key//` and `if_code_managed: true`, then send again with the same `Idempotency-Key`. + - A template made in PostHog sends `"template": {"id": "