From 599b4cd931931b68ccdbd2dc15d19b1d0fc4d4de Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:26:33 +0000 Subject: [PATCH 1/2] feat(transactional-email): add a command that sets up the first email send `npx @posthog/wizard transactional-email` runs the context-mill skill omnibus-send-transactional-email: the setup agent checks for a verified sender, writes the project secret API key to the env file, and adds the first send with posthog-node or the HTTP API. The intro spell book of a plain run lists it, and the skill's two [ABORT] reasons end the run with a next step. Co-Authored-By: Claude Opus 5.5 --- README.md | 14 +++++ src/cli/__tests__/programs-cli.test.ts | 7 +++ src/cli/commands/transactional-email.ts | 9 ++++ src/cli/index.ts | 2 + .../__tests__/program-registry.test.ts | 1 + .../program-runner/program-registry.ts | 3 ++ .../__tests__/transactional-email.test.ts | 15 ++++++ src/programs/transactional-email/index.ts | 48 +++++++++++++++++ .../transactional-email/tsconfig.json | 4 ++ src/programs/tsconfig.json | 1 + .../__snapshots__/flow-traces.test.ts.snap | 53 +++++++++++++++++++ .../__tests__/intro-menu.test.ts | 1 + .../posthog-integration/intro-menu.ts | 1 + 13 files changed, 159 insertions(+) create mode 100644 src/cli/commands/transactional-email.ts create mode 100644 src/programs/transactional-email/__tests__/transactional-email.test.ts create mode 100644 src/programs/transactional-email/index.ts create mode 100644 src/programs/transactional-email/tsconfig.json diff --git a/README.md b/README.md index b9fff3902..61b94358f 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,20 @@ npx @posthog/wizard@latest revenue-analytics Requires PostHog and Stripe SDKs already installed. Supports `--ci` with the same flags as the main wizard. +### Transactional email + +Send your first transactional email from your server code, such as a welcome +email after signup: + +```bash +npx @posthog/wizard@latest transactional-email +``` + +The setup agent checks for a verified sender, asks for a project secret API key +and writes it to your env file, and adds the send with `posthog-node` or the +HTTP API. The transactional email API is in beta and must be enabled for your +project. + ### Data Warehouse Detect data sources your project already uses (Postgres, MySQL, MongoDB, diff --git a/src/cli/__tests__/programs-cli.test.ts b/src/cli/__tests__/programs-cli.test.ts index f90d72959..cbec1ba81 100644 --- a/src/cli/__tests__/programs-cli.test.ts +++ b/src/cli/__tests__/programs-cli.test.ts @@ -23,6 +23,7 @@ import { migrateCommand } from '../commands/migrate'; import { mcpAnalyticsCommand } from '../commands/mcp-analytics'; import { replayVisionCommand } from '../commands/replay-vision'; import { revenueCommand } from '../commands/revenue'; +import { transactionalEmailCommand } from '../commands/transactional-email'; import { warehouseCommand } from '../commands/warehouse'; import { uploadSourcemapsCommand } from '../commands/upload-sourcemaps'; import { selfDrivingCommand } from '../commands/self-driving'; @@ -202,6 +203,12 @@ describe('flat skill commands', () => { expect(config.skillId).toBe('revenue-analytics-setup'); }); + test('transactional-email dispatches with omnibus-send-transactional-email skillId', () => { + transactionalEmailCommand.handler!(makeArgv({ debug: true })); + const [config] = mockRunWizard.mock.calls[0] as [{ skillId?: string }]; + expect(config.skillId).toBe('omnibus-send-transactional-email'); + }); + test('mcp-analytics dispatches with mcp-analytics skillId', () => { mcpAnalyticsCommand.handler!(makeArgv({ debug: true })); const [config] = mockRunWizard.mock.calls[0] as [{ skillId?: string }]; diff --git a/src/cli/commands/transactional-email.ts b/src/cli/commands/transactional-email.ts new file mode 100644 index 000000000..11e374d96 --- /dev/null +++ b/src/cli/commands/transactional-email.ts @@ -0,0 +1,9 @@ +import { config as transactionalEmailConfig } from '@programs/transactional-email'; + +import type { Command } from './command'; +import { nativeCommandFactory } from './factories/native-command-factory'; + +/** `wizard transactional-email`: send the first transactional email from the project's server code. */ +export const transactionalEmailCommand: Command = nativeCommandFactory( + transactionalEmailConfig, +); diff --git a/src/cli/index.ts b/src/cli/index.ts index 4fc62d033..220548474 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -17,6 +17,7 @@ import { slackCommand } from './commands/slack'; import { uploadSourcemapsCommand } from './commands/upload-sourcemaps'; import { errorTrackingCommand } from './commands/error-tracking'; import { featureFlagsCommand } from './commands/feature-flags'; +import { transactionalEmailCommand } from './commands/transactional-email'; import { skillCommand } from './commands/skill'; /** Register every command and run the one `process.argv` names. */ @@ -38,6 +39,7 @@ export function runCli(): void { .use(uploadSourcemapsCommand) .use(errorTrackingCommand) .use(featureFlagsCommand) + .use(transactionalEmailCommand) .use(skillCommand) .init(); } diff --git a/src/programs/program-runner/__tests__/program-registry.test.ts b/src/programs/program-runner/__tests__/program-registry.test.ts index c72801147..57a2d4bde 100644 --- a/src/programs/program-runner/__tests__/program-registry.test.ts +++ b/src/programs/program-runner/__tests__/program-registry.test.ts @@ -90,6 +90,7 @@ describe('program bindings', () => { 'error-tracking': orchestratorOnPi, 'feature-flags': orchestratorOnPi, 'agent-skill': linearOnSol, + 'transactional-email': linearOnSol, }); }); }); diff --git a/src/programs/program-runner/program-registry.ts b/src/programs/program-runner/program-registry.ts index af3ce5f1b..a3cb66c72 100644 --- a/src/programs/program-runner/program-registry.ts +++ b/src/programs/program-runner/program-registry.ts @@ -25,6 +25,7 @@ import { config as sourceMaps } from '@programs/error-tracking-upload-source-map import { config as errorTracking } from '@programs/error-tracking'; import { config as featureFlags } from '@programs/feature-flags'; import { config as agentSkill } from '@programs/agent-skill'; +import { config as transactionalEmail } from '@programs/transactional-email'; const [audit, eventsAudit] = auditPrograms; @@ -45,6 +46,7 @@ export const PROGRAM_REGISTRY = [ replayVision, aiObservability, metrics, + transactionalEmail, ] as const satisfies readonly ProgramConfig[]; /** @@ -69,6 +71,7 @@ export const Program = { ReplayVision: replayVision.id, AiObservability: aiObservability.id, Metrics: metrics.id, + TransactionalEmail: transactionalEmail.id, } as const; /** diff --git a/src/programs/transactional-email/__tests__/transactional-email.test.ts b/src/programs/transactional-email/__tests__/transactional-email.test.ts new file mode 100644 index 000000000..857f5424f --- /dev/null +++ b/src/programs/transactional-email/__tests__/transactional-email.test.ts @@ -0,0 +1,15 @@ +import { TRANSACTIONAL_EMAIL_ABORT_CASES } from '@programs/transactional-email'; + +describe('TRANSACTIONAL_EMAIL_ABORT_CASES', () => { + // The exact `[ABORT] ` strings of the send-transactional-email skill's + // "Inside the PostHog Wizard" section (context-mill), prefix stripped. + it.each(['transactional email api not enabled', 'no server code found'])( + 'matches the "%s" abort reason exactly once', + (reason) => { + const matched = TRANSACTIONAL_EMAIL_ABORT_CASES.filter((c) => + c.match.test(reason), + ); + expect(matched).toHaveLength(1); + }, + ); +}); diff --git a/src/programs/transactional-email/index.ts b/src/programs/transactional-email/index.ts new file mode 100644 index 000000000..2d4be0095 --- /dev/null +++ b/src/programs/transactional-email/index.ts @@ -0,0 +1,48 @@ +import type { AbortCase } from '@agent/types'; +import { createSkillProgram } from '../shared/skill-program'; +import type { ProgramConfig } from '../program-step'; + +const SKILL_ID = 'omnibus-send-transactional-email'; +const REPORT_FILE = 'posthog-transactional-email-report.md'; + +/** The `[ABORT]` reasons of the skill's "Inside the PostHog Wizard" section (context-mill). */ +export const TRANSACTIONAL_EMAIL_ABORT_CASES: AbortCase[] = [ + { + match: /^transactional email api not enabled$/i, + message: "Transactional email isn't enabled for this project", + body: + 'The transactional email API is in beta, and PostHog turns it on per ' + + 'project. Ask PostHog in the app to enable it for your project, then ' + + 'run this command again.', + }, + { + match: /^no server code found$/i, + message: 'No server code found', + body: + 'A transactional email is sent from server code, because the send needs ' + + "a secret key. The setup agent didn't find server code in this project. " + + 'Run this command again from the project that runs your server.', + }, +]; + +/** `wizard transactional-email`: verify a sender, set the secret key, and write the first send from code. */ +export const config: ProgramConfig = createSkillProgram({ + skillId: SKILL_ID, + command: 'transactional-email', + id: 'transactional-email', + description: 'Send your first transactional email from code', + integrationLabel: 'transactional-email', + customPrompt: + `Set up transactional email in this project with the \`${SKILL_ID}\` ` + + 'skill: verify a sender, set the project secret API key, and write the ' + + 'first send. Follow the skill\'s "Inside the PostHog Wizard" section. ' + + `Write the final report to ./${REPORT_FILE}: the email the app sends, the ` + + 'files you changed, the environment variables every deployment needs, ' + + 'and what the person still has to do.', + successMessage: `Transactional email is set up. View the report at ./${REPORT_FILE}`, + reportFile: REPORT_FILE, + docsUrl: 'https://posthog.com/docs/workflows/transactional-email-api', + spinnerMessage: 'Setting up transactional email...', + estimatedDurationMinutes: 5, + abortCases: TRANSACTIONAL_EMAIL_ABORT_CASES, +}); diff --git a/src/programs/transactional-email/tsconfig.json b/src/programs/transactional-email/tsconfig.json new file mode 100644 index 000000000..79601a848 --- /dev/null +++ b/src/programs/transactional-email/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.no-tui.json", + "references": [{ "path": "../tsconfig.core.json" }] +} diff --git a/src/programs/tsconfig.json b/src/programs/tsconfig.json index 336303790..d9bc2c54c 100644 --- a/src/programs/tsconfig.json +++ b/src/programs/tsconfig.json @@ -9,6 +9,7 @@ { "path": "mcp-analytics" }, { "path": "metrics" }, { "path": "migration" }, { "path": "posthog-integration" }, { "path": "replay-vision" }, { "path": "revenue-analytics" }, { "path": "self-driving" }, + { "path": "transactional-email" }, { "path": "warehouse-source" }, { "path": "web-analytics-doctor" } ] } diff --git a/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap b/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap index bf151471a..f5ecfa962 100644 --- a/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap +++ b/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap @@ -1016,6 +1016,59 @@ exports[`flow traces per program > slack (node) 1`] = ` } `; +exports[`flow traces per program > transactional-email (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "agent-skill-intro", + "program": "transactional-email", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "transactional-email", + }, + { + "event": "screen run", + "from": "auth", + "program": "transactional-email", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "transactional-email", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "transactional-email", + }, + { + "event": "screen outro", + "from": "run", + "program": "transactional-email", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "transactional-email", + }, + ], + "program": "transactional-email", + "screens": [ + "agent-skill-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + exports[`flow traces per program > warehouse-source (node) 1`] = ` { "events": [ diff --git a/src/tui/programs/posthog-integration/__tests__/intro-menu.test.ts b/src/tui/programs/posthog-integration/__tests__/intro-menu.test.ts index fd4674551..9aa8c162a 100644 --- a/src/tui/programs/posthog-integration/__tests__/intro-menu.test.ts +++ b/src/tui/programs/posthog-integration/__tests__/intro-menu.test.ts @@ -132,6 +132,7 @@ describe('introEntries', () => { 'ai-observability', 'metrics', 'revenue-analytics-setup', + 'transactional-email', ]); }); diff --git a/src/tui/programs/posthog-integration/intro-menu.ts b/src/tui/programs/posthog-integration/intro-menu.ts index 677670b54..3d7fff56e 100644 --- a/src/tui/programs/posthog-integration/intro-menu.ts +++ b/src/tui/programs/posthog-integration/intro-menu.ts @@ -31,6 +31,7 @@ const INTRO_ENTRIES = [ 'ai-observability', 'metrics', 'revenue-analytics-setup', + 'transactional-email', ]; /** One spell book row: the id the intro hands off to, the words that run it, and its help line. */ From ac0bf64db02edb92d1cdb6500b910c28f25ed1da Mon Sep 17 00:00:00 2001 From: "Reichenbach, Michael" <755327+Silthus@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:53:56 +0000 Subject: [PATCH 2/2] fix(transactional-email): follow main's program contract and skill cleanup Import `ProgramConfig` from `@programs/program-contract`, where the services refactor moved it, and take the outro's `exit` step from the skill cleanup in the flow trace. Co-Authored-By: Claude Opus 5.5 --- src/programs/transactional-email/index.ts | 2 +- src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/programs/transactional-email/index.ts b/src/programs/transactional-email/index.ts index 2d4be0095..0885ab97e 100644 --- a/src/programs/transactional-email/index.ts +++ b/src/programs/transactional-email/index.ts @@ -1,6 +1,6 @@ import type { AbortCase } from '@agent/types'; import { createSkillProgram } from '../shared/skill-program'; -import type { ProgramConfig } from '../program-step'; +import type { ProgramConfig } from '@programs/program-contract'; const SKILL_ID = 'omnibus-send-transactional-email'; const REPORT_FILE = 'posthog-transactional-email-report.md'; diff --git a/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap b/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap index f5ecfa962..c5010df0c 100644 --- a/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap +++ b/src/tui/__tests__/__snapshots__/flow-traces.test.ts.snap @@ -1050,7 +1050,7 @@ exports[`flow traces per program > transactional-email (node) 1`] = ` "program": "transactional-email", }, { - "event": "screen keep-skills", + "event": "screen exit", "from": "outro", "program": "transactional-email", }, @@ -1063,7 +1063,7 @@ exports[`flow traces per program > transactional-email (node) 1`] = ` "ai-opt-in", "run", "outro", - "keep-skills", + "exit", ], "stoppedOn": null, }