Skip to content

Map: send code-managed email templates with one PostHog call #461

Open
57 of 72 issues completed
Open

Description

@Silthus

Origin: Slack thread, the Workflows team page, and Resend's developer experience.

Problem

PostHog grows through engineers and their coding agents. Workflows is built for marketers and product leads, so engineers rarely find it, and their agents never suggest it. Sending one transactional email from code (welcome, signup confirmation, password reset) takes several API calls today, while services like Resend need one call and one key. PostHog already runs the sending infrastructure: SES, queues, retries, suppression, metrics and delivery tracking. What is missing is a one-call entry point that an agent can find and wire into a user's code.

Destination

A draft PR stack on PostHog/posthog and posthog-node, behind the transactional email flag, where an agent uploads a local HTML template from the user's repo as a code-managed template, the template shows as managed by code (later read-only) in the Workflows template library, and posthog.emails.send({ template, to, data }) sends it: proven locally through the dev mail catcher, then by a real send through SES from a deployed project after merge.
Agents are the primary audience, humans second. The way in for an agent is one skill that teaches it to verify a sender and send the first email.

Extension (Michael, 2026-10-20): Resend parity. Transactional email from code mirrors Resend closely enough that a team switches with one skill: React Email templates work directly, the SDK send returns OK only when the email is durably queued for delivery and an error otherwise, and keys can be limited to senders so staging never sends from the production domain. Shape pending #525.

Impact

Pays into Grow Workflows through Orgs over the free tier (a team that switches from Resend brings its transactional volume) and Started orgs (agents wire email into new codebases). Expected move: large, up, if the attack lands (Michael, 2026-10-20: "My plan is to do a full on attack and get a ton of orgs over to us"). Not measured yet: no count of organizations that send through the API or the SDK; it needs a send source (api, sdk, SDK version) on the send's invocation or event, and an insight on dashboard 2180756.

Notes

Execution. Execution stays inside this map. Conductor: /orchestration-wayfinder, running in T3.
Conductor thread: 2bb35c7d-9114-4ff9-bf80-56350d798ed0
A resolved spec publishes implementation tickets as children of this map.
Backend branches push to the Silthus fork; draft PRs open on PostHog/posthog against master from the fork branch. SDK work opens a draft PR on PostHog/posthog-js.
Shipped means a reviewed draft PR. No merge authority: Michael reviews and marks PRs ready himself. The flag stays off until Michael rolls it out.

Orchestration blocker (2026-10-14, decided 2026-10-16). The upstream transactional email stack landed on PostHog/posthog master on 2026-10-16 (#116188, #117663, #117670, #117785, #117786, #117726, #117673); PostHog#116160 and PostHog#116187 closed in its favor. Michael chose to converge: this map builds on that stack. The spec/email-send-api branch and #468–#473 are parked. The map is being re-charted around code-managed templates (upsert from code by key, read-only in the UI, send by key). No new lanes dispatch until that charting round is answered.

Trial: builders per strand (Michael, 2026-10-21): one issue and one lane per strand, slices as commits and PRs, one review per strand. From #536 on, each lane runs as a nested subagent of the conductor, not a separate thread (Michael, 2026-10-21). Applies to spec #531. When the last strand hands off, the per-strand Trial notes go to the private note workflows-email-sdk-research/trial-builders-per-strand.md in Silthus/personal, without numbers here.

Compute lease: one box per lane (Michael, 2026-10-22). Each lane leases map-posthog-461-<ticket>; the shared heavy lock is retired. Review rounds past two go to the fleet proof arbiter, not to Michael.

Merge window (Michael, 2026-10-22): merging starts on 2026-11-02. Until then every strand opens its slices as draft PRs and waits for no merge. The SDK strands #536, #537 and #538 started on 2026-10-22; #537 stacks on PostHog/posthog-python#1093 until it merges. A conductor run on 2026-11-01 restacks and modernizes every PR of the map.

Restack notes from #535 (2026-10-23): the send-api stack is PostHog#121272 → PostHog#121850 (07) → PostHog#121867 (06) → PostHog#121896, PostHog#121897, PostHog#121898 (08). Slice 05 needed no PR.

Fork stacks (Michael, 2026-10-23): every stack moves into draft PRs in Michael's forks, on synced default branches, so it moves upstream without another restack. Lanes started 2026-10-23: srv (send API: restack, owed code, the ?to= route, QA swarm, live proof), js and py (SDKs: contract alignment, QA swarm), small (docs and Wizard), and sandbox (sandbox sender stack #426-#456: restack, QA swarm, proof). An end-to-end SDK-to-server lane follows once srv, js and py hand off.

Decisions (Michael, 2026-10-24):

Decisions (Michael, 2026-10-25):

  • Rebuild the send stack code-first. Slice 1 is a curl with inline HTML on messaging/send/email/; the MCP tool and posthog-node follow in parallel; the template work, send checks, fields and status stack on top. Research: research-minimal-slice.md in the private notes.
  • Code sends get their own cdp-api route, so an old cdp-api answers 404 and Django answers 503 with nothing sent. A cross-version run showed that new Django with an old cdp-api answers 202 and delivers nothing, or the stored template under the same key.
  • The cross-lane CI check: split by service where a slice allows, label cross-lane-change only where Django and cdp-api must land together.
  • capture_engagement defaults to true on every form, per send in code; a project with capture off captures nothing (contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921, 2026-10-25).
  • managed_by stays nullable; the read-only preview shows the sender; the create schema keeps refusing transactional_email_keys; a template uploaded under a used key takes over its sends (help text now, reuse later); a reused idempotency key with a changed template hash replays.
  • One rollout flag: everything this map adds is gated by workflows-email-from-code, which starts on the PostHog org only. Master's existing surfaces stay on workflows-transactional-email-api. workflows-email-domain-agent-setup is retired, and feat(workflows): let agents set up an email sending domain via mcp #568–feat(workflows): expose read-only email sending readiness over mcp #570 move to the new flag in the 2026-10-31 restack round.
  • No code comments in added code.

Decisions (Michael, 2026-10-26):

  • API sends use the sandbox sender. A project without a verified domain can send from code at once, through the send API and the SDKs, from the sandbox address. This reverses the sandbox charting rule "test sends and live workflows only" (#225). Every other sandbox rule holds for API sends:
    • Recipients are verified organization members over To, CC and BCC, checked at send time.
    • The From address and the " via PostHog" name are fixed. There is no From or Reply-To override.
    • The fail-closed daily caps are shared with test sends.
    • The tenant pause gate, the organization blocks and the blocked reasons apply.
    • Sandbox sends earn no sending-tier history.
    • A blocked API send answers the send API's error contract with a code, a reason and the next step, such as verifying a domain. It never reroutes to another recipient.
    • Send attribution marks sandbox sends, so time to first send counts them.
    • Answers (Michael, 2026-10-26): the caps stay refilling buckets, and a used-up cap answers 400 sandbox_limit_reached with no retry. One refusal, sandbox_recipient_not_allowed, covers both non-members and unverified members. Sends ship with the synchronous checks; a skip after the 202 shows once the status read lands.
    • Slices: Node admission in cdp-api (sandbox checks before enqueue), then the Django fallback (an omitted from with no verified sender of its own; a named from or a reply_to is refused). Both get a security-focused QA swarm. The lane starts after the 2026-10-31 restack, and the contract change waits for Michael's approval.
  • Fields (cc, bcc, headers, tags, attachments) rely on deploy order and the flag: callers get them only after every Node consumer runs the field slices. A cdp-api rollback while the flag is on can drop those fields behind a 202, and each field PR states that limit.
  • Status reads have the same rates as sends, in their own buckets (contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921, 2026-10-26).
  • The Wizard asks about the email provider. An interactive install run whose scan finds an email provider asks one question after the outro and captures the answer (wizard: email provider offer shown and ... answered), in a new slice 18a stacked on feat(analytics): set detected email providers on the person PostHog/wizard#1493. Slice 18 reuses the question for its migrate resend offer. It ships ahead of the migrate family. It's never asked in CI, in headless runs, or with telemetry off.
  • The reload-after-commit fix is its own PR, fix(workflows): reload a saved workflow only after its commit #581, and lands before feat(messaging): send each email once per idempotency key #578 and feat(cdp): replay a keyed send after its workflow or default sender changes #580 (2026-10-25).
  • Merge plan: restack on 2026-10-31, push upstream as native stacks on 2026-11-02, then merge in waves.

Upstream PRs closed (Michael, 2026-10-25): the 22 upstream drafts that had a fork copy are closed, and the fork stacks in Silthus/posthog, posthog-js, posthog-python, posthog.com, wizard and context-mill are the only copies. Work opens upstream again after review. PostHog/context-mill#433 stays open: it has no fork copy.

Next research (2026-10-22): which providers teams reach from PostHog by webhook, and the competitor landscape around Resend. Both are tracked privately in Silthus/personal because they use production data. Their synthesis decides the next draft PRs.

Follow-ups from the competitor research (Michael, 2026-10-22):

Prior art, Resend parity (2026-10-20). Issue and PR inventory; chat inventory in the private note.

Public repo. This fork is public. Issues, comments, branches and PRs never carry usage or revenue numbers, customer names, internal metrics, Slack quotes, or security reasoning. The research context lives in a private note in Silthus/personal (workflows-email-sdk-context.md). Tickets may point at it but never quote it.

Casting. Per the shared model selection policy: research as a Survey (Sonnet 5.5) or Deep research (GPT-6.1 Sol) for how code behaves; implementation on Opus 5.5; reviews on GPT-6.1 Sol; every dispatch names model and effort.

Skills. /research for research tickets, /prototype for the architecture prototype, /to-spec + /to-tickets for the spec, /tdd for implementation, /pr and a cross-family review for shipping.

Vocabulary.

  • API send: one email sent by a direct API call, not by a workflow step.
  • Default sender: the project's preselected verified email sender.
  • Verified sender: an email sender on a domain the project has verified. It delivers to anyone.
  • Trial sender: the future PostHog-owned sender (onboarding@trial.posthog.com) for projects without a verified domain. In code it is the sandbox sender of the sandbox sender map. Not part of this map's slice.

Decisions so far

Not yet specified

Out of scope

  • Marketing mail through the API: Workflows and broadcasts already cover it, with opt-outs and unsubscribe links.
  • Batch sends, outbound delivery webhooks, scheduled sends: delivery status comes from events on the person instead.
  • SDKs other than Node.
  • Workflows as code, and triggering workflows from code.

Activity

  1. 48 remaining items

  2. Silthus commented on Oct 29, 2026

    @Silthus
    OwnerAuthor

    2026-10-29: plan update after Harley's takeover

    Harley shipped the template form and email status end to end, and merged PostHog/sdk-specs#159 as the SDK spec. The spec leaves out the code form and capture_engagement, so those stay with this map.

    Closed as superseded (branches kept):

    Restack before Nov 2:

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions