Skip to content

feat(transactional-email): add a command that sets up the first email send - #1

Closed
Silthus wants to merge 7 commits into
mainfrom
feat/transactional-email-program
Closed

Silthus wants to merge 7 commits into
mainfrom
feat/transactional-email-program

Conversation

@Silthus

@Silthus Silthus commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Moved here from PostHog#1487, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.

This revision

Restacked on main on 2026-10-31, with no conflicts. 6654b675 adds migrated_from to setup wizard finished for a migrate-email run, test-first. 1c077af6 links the posthog.com transactional emails page. a90f038b (review fix) sets migrated_from on every skill change, so a later non-migration skill, or none, clears it. 6da6fc9d (QA swarm fix) links the transactional emails docs from both abort outros, test-first, and drops the comments this PR added. df5cfbd3 spells out the program's binding.

Problem

A team that has never sent email from code has no guided way to start with PostHog. Wizard can add PostHog to a project, but the setup agent has no path to send a transactional email, such as a welcome email after signup.

With this PR, npx @posthog/wizard transactional-email checks for a verified sender, writes the project secret API key to the env file, and adds the first send to the app's server code. A plain run that finds PostHog already installed lists the command in its intro spell book.

Part of Silthus/posthog#544.

Impact

Pays into Grow Workflows through organizations that send email from code (not measured yet: it needs the send source on each send, PostHog/posthog#120628). In scope: transactional email. Expected move: medium, up. Run cost: n/a.

Merge Danger

Warning

🟡 Merge risk: medium · one-way door: a release publishes the transactional-email command name (merge-risk rubric)

  • The skill this runs, omnibus-send-transactional-email, is not in any skill menu. Its context-mill PR is closed, because PostHog/posthog#125415 adds the sending-transactional-emails skill in PostHog/posthog instead. Released as is, the command fails when it downloads the skill. Point it at the new skill before it leaves draft (see Open).
  • The transactional email API is in beta and on per project. Where it is off, the skill emits [ABORT] transactional email api not enabled. On the default Pi binding no program's [ABORT] ends the run yet (see Gaps), so such a run can still end on the success outro.
  • migrated_from is a new property on the events of a run whose skill ID is migrate-email-<provider>, setup wizard finished included. Other runs send the same events as before.
  • Merge after the skill it runs is in the published menu. posthog-node with emails.send is out (PostHog/posthog-js#5418). A revert removes the command, the spell book row, and the property.

Changes

  • New program transactional-email (src/programs/transactional-email/), built with createSkillProgram. It names its binding: Pi, the linear sequence, GPT-5.6 Sol at medium thinking, the same as the default today.
  • New command transactional-email (nativeCommandFactory), registered in runCli.
  • The intro spell book of a plain run lists it last: transactional-email Send your first transactional email from code.
  • Two abort cases match the skill's "Inside the PostHog Wizard" section: the API is not enabled, and no server code was found. The linear sequence matches them against the run's abort reason, and each error outro links https://posthog.com/docs/workflows/transactional-emails.
  • No extra OAuth scopes. The PostHog MCP hides tools whose scopes the login lacks: the integrations-* sender tools and the workflows-* email tools. The skill's Wizard section treats a missing tool as inconclusive, sends the person to the PostHog UI to verify a sender, and takes only a send's 404 as "API not enabled".
  • README: a "Transactional email" section.
  • migrated_from on setup wizard finished (src/services/session/session-store.ts): when a run's skill ID is migrate-email-<provider>, such as migrate-email-resend, the session sets the analytics tag migrated_from to the provider, here resend. Every event after the run picks its skill carries it, the existing setup wizard finished event included. No new event.
  • The program's docs link is now https://posthog.com/docs/workflows/transactional-emails, the page of PostHog/posthog.com#21564.

The skill's Wizard section (ask through wizard_ask, write the key with set_env_values, reach PostHog tools through posthog_exec, the [ABORT] reasons) was in PostHog/context-mill#433, now closed. The skill that replaces it must carry the same section.

Test plan

Test seams:

  • CLI dispatch (src/cli/__tests__/programs-cli.test.ts): transactional-email runs with skill omnibus-send-transactional-email. Red before the command existed (module not found), green after.
  • Intro spell book (intro-menu.test.ts): the curated list resolves and ends with transactional-email, and the row fits 80 columns. Red, then green.
  • Abort cases (src/programs/transactional-email/__tests__): each [ABORT] reason from the skill matches exactly one case in the program's run config, and that case links the transactional emails docs. Red before 6da6fc9d (no link), green after.
  • migrated_from (src/services/session/__tests__/migrated-from.test.ts): a migrate-email-resend run tags migrated_from: resend, and other skill IDs set no tag. A later skill or null after migrate-email-resend clears the tag. Red before the session set it, and red again before a90f038b (the tag stayed resend), green after.
  • Bindings and flow: program-registry.test.ts pins the linear Pi binding. The flow-traces snapshot records intro, health check, auth, run, and outro for the program.

Lane gate on df5cfbd3: pnpm typecheck green, pnpm lint 0 errors, pnpm exec vitest run 261 files and 3,866 tests green. CI: the fork runs no workflows, so this PR has no checks; the lane gate is the check.

Rebase fixes, both from changes on main:

  • The program imports ProgramConfig from @programs/program-contract, where main moved the program contract.
  • The flow-traces snapshot for this program ends on the exit screen, which replaced keep-skills on main.
$ npx tsx bin.ts --help
  bin.ts revenue-analytics    Set up PostHog for Revenue Analytics
  bin.ts transactional-email  Send your first transactional email from code

Proofs per qa-swarm PROOF.md (all gaps):

  • Live run (gap): see the last bullet below.
  • Prod shape (gap, accepted by standing rule until 2026-11-02): the share of real runs that reach a migrate-email skill or this command needs read-only production access. No migrate-email skill is published yet, so no run takes the migrated_from branch today.
  • Cross-version (gap): migrated_from is one added string property on existing events, which ingestion stores as is. No PostHog stack on this box to feed both builds through.
  • Post-deploy watch: owed after merge and a Wizard release.

Gaps:

  • The Pi harness ends a run only on [posthog:abort], the gateway refusal marker. It ignores a skill's [ABORT], so these abort cases, like those of revenue-analytics and mcp-analytics, don't fire on the default binding yet. Fixing that is a separate PR.
  • No end-to-end run of the setup agent. It needs the skill in a published menu (or --local-context-mill), Wizard CI credentials, and a project with the transactional email API on.
  • No rendered screenshot of the spell book row. The intro-menu test covers the row's text.

Decisions

Question Answer Who Where
Offer transactional email setup in Wizard Yes, as a draft, in the core run where it fits 👤 Human Silthus/posthog#544
Where the entry hooks into the core run A native command plus a spell book row. An install-run offer needs a signal for "sends no email yet", and a seeded orchestrator task needs a flag and runner support 🤖 Agent pending approval
OAuth scopes for sender setup None now. The skill treats hidden tools as inconclusive and uses the UI path. Add integration:read and integration:write once the OAuth app's scope ceiling holds them, together with the migrate family 🤖 Agent pending approval
How the Wizard records a migration migrated_from on the existing setup wizard finished event, from the skill ID; no new event 🤖 Agent Silthus/posthog#461
Where migrated_from is derived In SessionStore.setSkillId, the one point every skill run passes: migrate-email skills run from the menu with no Wizard program of their own. The alternative, tagging the raw skill_id and dropping migrated_from, renames the property the send attribution names 🤖 Agent pending approval

Open: which skill transactional-email runs. omnibus-send-transactional-email is gone with PostHog/context-mill#433, so today the command fails when it downloads the skill. Recommended: run sending-transactional-emails from PostHog/posthog#125415 once the skill menu publishes it, move this skill's Wizard section there, and keep this PR a draft until then.

Open: the Pi harness ignores a skill's [ABORT], so on the default binding a run where the API is off can end on the success outro. Recommended: fix [ABORT] handling in the Pi harness as its own PR, for every program on Pi (revenue-analytics and mcp-analytics have the same gap), before this command leaves draft, rather than moving this program to the deprecated Anthropic binding.

Follow-ups:

  • The "API not enabled" outro says to ask PostHog to enable the beta; it names the exact way once the docs page names one.
  • The linear sequence shows a matched abort's own docs link and never falls back to the program's; a fallback in linear.ts would cover mcp-analytics too and let this program drop its per-case links.
  • An outside-in CLI test harness for the flat commands, in place of the runner-argument checks every sibling test in programs-cli.test.ts uses.

Involvement: no grilling rounds, no prototypes, one GPT-6.1 Sol review round over this PR and PostHog/context-mill#433 (four findings: three fixed in PostHog#433, one deferred as above) and one final read (one finding, fixed in PostHog#433), one GPT-6.1 Sol review of the rebase fix (no findings), and one GPT-6.1 Sol round on the restack (one finding on a stale migrated_from tag, fixed test-first) with a clean read of the fix. QA swarm 2026-11-01: one two-family panel round (9 lenses, GPT-6.1 Sol and Claude Opus 5.5) and a fixes read; 5 findings fixed, 1 call, 2 open decisions, 3 follow-ups, 0 rejected. Proof captured as command output.

LLM context

Written by Claude Opus 5.5 (claude-opus-5-5) as a build lane of a planning map. The setup agent copy follows the "Wizard once, then the setup agent" rule.

🤖 Generated with Claude Code

Silthus and others added 4 commits October 31, 2026 09:54
… 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 <[email protected]>
…eanup

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 <[email protected]>
A run of a migrate-email-<provider> skill sets migrated_from, so the
setup wizard finished event names the provider.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus
Silthus force-pushed the feat/transactional-email-program branch from ac0bf64 to 1c077af Compare October 31, 2026 10:14
Silthus and others added 3 commits November 1, 2026 13:03
…migration

The tag stayed on the process after the skill changed, so a later
skill's events still said the run migrated from the email provider.
Every skill change now sets or clears it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both abort cases carry the transactional emails docs link, so the error
outro of a matched abort shows where to go next. The test reads the cases
through the program's run config, so dropping them from the config fails
it. The program sets its binding explicitly, and the added comments go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The program names Pi, the linear sequence and its model, so a change to the
default binding no longer moves it off the sequence its abort cases need.
The abort test drops a check the type already makes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Moved to PostHog#1507.

@Silthus Silthus closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant