Repository navigation
Map: send code-managed email templates with one PostHog call #461
Description
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.
- Still owed from Build strand: cc, bcc, headers, tags and attachments on code sends (Resend parity) #543 on the restack: fingerprint rows for
cc,headersandattachments, and slice 07's admission check countingccandbccas recipients. - Rollout precondition: the
workflows-transactional-email-apiflag stays off until tracking code v3 minting is on and feat(messaging): send each email once per idempotency key on send/email PostHog/posthog#121867 has deployed on both Django and cdp-api. - A send with an unknown field answers 400
invalid_requestwithattrnaming the field, so a newer SDK against an older server fails loudly. Release the SDK fields only after feat(messaging): send cc, bcc, headers and tags with a code send PostHog/posthog#121851 and feat(messaging): send attachments with a code send PostHog/posthog#121852 deploy. - Mixed deploy: new Django with an old cdp-api answers an inactive workflow with 503 instead of 400. Nothing is sent.
- Open: the
GET messaging/emails/?to=route. The facadelist_email_statuses_by_recipientis in feat(messaging): read a transactional email's status by id or recipient PostHog/posthog#121898; no route exposes it yet.
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):
- The contract adds: an answer without a code takes the name and retry of its status; a disabled client fails with
client_disabled; andclient_closed. - posthog-python gets our own contract-alignment PR on top of Toolbar doesn't work on custom domains PostHog/posthog#1101, which aligns Scrolling issue with toolbar PostHog/posthog#1093's template form and our code form after Scrolling issue with toolbar PostHog/posthog#1093 merges. Harley gets no requests.
- In the sandbox stack, the copy for an unverified member email changes, the From address is read from one setting, and feat(workflows): offer a switch from the sandbox sender #456's typegen moves out. The remaining follow-ups get one ticket on this map.
- Production-shape gaps are accepted until 2026-11-02. Live-run and old-server proofs stay open for the end-to-end lane.
- Lifecycle forwarding (Customer.io, Loops, Brevo) leaves this map. It is a revenue question, now researched in its own private map.
- feat(workflows): offer a switch from the sandbox sender #456 removes the cause of its inline typegen instead of moving the types out. A migrated send with a suppressed
ccorbccaddress keeps failing, with a report note. The migration skill's two smaller gaps are #564.
Decisions (Michael, 2026-10-25):
- Rebuild the send stack code-first. Slice 1 is a
curlwith inline HTML onmessaging/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.mdin 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-changeonly where Django and cdp-api must land together. capture_engagementdefaults totrueon 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_bystays nullable; the read-only preview shows the sender; the create schema keeps refusingtransactional_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 onworkflows-transactional-email-api.workflows-email-domain-agent-setupis 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_reachedwith no retry. One refusal,sandbox_recipient_not_allowed, covers both non-members and unverified members. Sends ship with the synchronous checks; a skip after the202shows once the status read lands. - Slices: Node admission in cdp-api (sandbox checks before enqueue), then the Django fallback (an omitted
fromwith no verified sender of its own; a namedfromor areply_tois 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 shownand... 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 itsmigrate resendoffer. It ships ahead of themigratefamily. 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):
- Started: Build strand: cc, bcc, headers, tags and attachments on code sends (Resend parity) #543 (cc, bcc, headers, tags, then attachments from
spike/email-attachments), Build: onboard new senders to transactional email through the Wizard (Resend parity) #544 (onboarding through the Wizard, with slice 20), research Research: extend the migration skill beyond Resend (SendGrid, Postmark, SES, Loops, Brevo) #545 (migration beyond Resend), and private research before a grilling on a faster sending-tier start for migrating teams. - Separate maps, on other boxes: an email performance view (click to replay, revenue per email) that starts by listing what we can already check against production, and restricted send keys, which builds on Research how a project secret key can be limited to email senders #524 and Grill Resend parity: React templates, delivery guarantee, sender-scoped keys #525.
- Side fix: look up sent emails by recipient, matching the slice 08 record shape.
- Already built: the sandbox sender stack, drafts feat(workflows): provision a managed sandbox email sender #426 to feat(workflows): offer a switch from the sandbox sender #456 on this fork. It is waiting for upstream and gets modernized in the 2026-11-01 run.
- Side fix: a warning before email sending pauses, so a team can act while bounce or complaint rates climb.
- Covered already: email engagement events reach any webhook through destinations, with no extra handling.
- Parked in Michael's to-dos: a per-email
scheduled_aton the send API, andemails.batchin the SDKs backed by broadcasts. - Contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921 now has
cc,bcc,headers,tagsandattachmentson the code form (Michael, 2026-10-22); both SDK strands add them. - Migration: one
migrate-emailskill group with one variant per provider. After Resend: SendGrid, Mailgun, Postmark (Research: extend the migration skill beyond Resend (SendGrid, Postmark, SES, Loops, Brevo) #545). Build strand: the Resend migration skill (Resend parity) #539 started, with a suppression import before cutover. - Charted: Build strand: swap provider webhook steps for native email steps (Resend parity) #546, swapping provider webhook steps in Workflows for native email steps.
- Side fix: the Wizard honors a skill's
[ABORT], found on feat(transactional-email): add a command that sets up the first email send PostHog/wizard#1487.
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
-
Charting (Michael, 2026-10-14):
- Surfaces: a REST endpoint, a
posthog-nodeemails.sendmethod plus documented cURL, and one agent skill. No browser SDK, no other languages. - Agent skill: sits next to PostHog's
instrument-*skills, served through the PostHog MCP, and triggers on "send an email / welcome email / password reset / transactional email". It tells the agent plainly to verify a sender first, through the PostHog MCP, before the first send. - Auth: a project secret API key (
phs_) throughposthog-node's existingsecretKey, with a new dedicated email send scope. The public project token never sends. - Call shape: Resend-like fields (
from,to,subject,html,text,replyTo,cc,bcc), an idempotency key, an optionaldistinctIdthat ties delivery events to the person. Never throws on API errors: returns{ data, error }. Follows theposthog.messagingpattern of feat(node): set email messaging preferences from the server PostHog/posthog-js#5173. - Errors are written for agents: each carries a stable code, a message that says what happened, why it happened, and how to fix it, with a link, so an agent can repair the call without a human.
- Content: inline
subject,html,text. The shape leaves room for a latertemplatefield. - Mail type: API sends are always transactional (fast lane, no opt-out, no unsubscribe headers); suppression still applies.
- Sender:
fromis optional and defaults to the project's default sender; it must be a verified sender. Examples always showfromexplicitly, on the user's own domain. A project with no verified sender gets an error that names the next step. - Execution: one hidden "API sends" workflow per team, the way broadcasts and loops work, so billing, quota, metrics and pausing work unchanged and sends show up in Workflows. A later refactor stays possible.
- Flag: backend only. With the flag off the endpoint returns a clear "not enabled for this project" error. The SDK has no flag.
- Sending tiers stay unchanged in this map.
- Proof: a scratch Node project sends through the local dev stack into the dev mail catcher; a real SES send from a deployed project follows after merge.
- Surfaces: a REST endpoint, a
-
Which MCP tools let an agent verify an email sender on its own?: none on master; five sender tools, route scopes and a domain-setup skill exist only as fork drafts (feat(workflows): let agents set up an email sending domain via mcp #343, fix(workflows): keep one mail from label per email sending domain #345, feat(workflows): expose read-only email sending readiness #351), behind
workflows-email-domain-agent-setup; PostHog never writes DNS itself, the agent hands over a Domain Connect URL; the slice-1 skill must handle that flag next to the email send flag. Landing them is #466. -
Shape how an API send becomes an invocation on the hidden API sends workflow:
POST /api/projects/@current/emails/with aphs_key andemail_send:write; Django resolves the sender, finds or creates the one hiddenapi_sendsworkflow (newapi_sendtrigger) and calls a new CDP route over a scoped service JWT that checks quota and pause and enqueues one invocation; returns{ id, url }; one error catalog with code, what/why/fix and docs link; no Workflows UI, only a direct/workflows/api-sendslink. Brief onprototype/email-api-send-contract. -
Spec the email send API and slice it into PRs: Spec: email send API, nine implementation tickets (Email API: API sends origin and the one-flow-per-team index #468–Agent skill: verify a sender, upload the template from the repo, send by key #476) across PostHog/posthog, posthog-js, posthog.com and context-mill; skill
send-transactional-emailin context-mill; a plain docs reference page is in the map; the SDK takes the idempotency key as a second argument and retries only with one; with the agent domain-setup flag off, the skill sends the person to the UI. Shipping: Email API: API sends origin and the one-flow-per-team index #468 is a migration-only PR; Email API: Django creates and owns the hidden API sends workflow #469–Email API: /workflows/api-sends redirect route #473 land on integration branchspec/email-send-apistacked on it and ship through #477; posthog-node: emails.send from a key or an HTML file, synced by content hash, returning { data, error } #474–Agent skill: verify a sender, upload the template from the repo, send by key #476 open their own PRs in their repos. Before the flag turns on, the new JWT signing secret needs a value in every environment. -
Bring the agent sender-verification tools to PostHog/posthog: three reviewed draft PRs on PostHog/posthog, all behind
workflows-email-domain-agent-setup: #116863 five MCP sender tools, scopes, skill and evals; #116864 one MAIL FROM label per domain, stacked on it; #116865 readiness tools andconfig.verifiedin the integrations list. -
Email API: API sends origin and the one-flow-per-team index: migration-only draft #116915, reviewed head
ce0843adff0; create endpoints refuse theapi_sendsorigin with a 400. Integration branchspec/email-send-apicreated on the fork from that tip. -
Email API: Django creates and owns the hidden API sends workflow: landed
38d8566764f;get_or_create_api_sends_flow(team)builds trigger, transactional email step and exit, converges under concurrency, rewrites a drifted graph; graph edits answer 400, name, status and rate limit stay editable. -
Email API: /workflows/api-sends redirect route: landed
64ab79c5a68; the link replaces itself with the API sends workflow's Invocations tab, or shows a read-only empty state with a docs link; it never creates the flow. Screenshots onassets/email-api-473(Storybook, mock data). -
Email API: CDP api_sends route and the Django client that calls it: landed
ca53fe7972d;create_hog_flow_api_sendsigns a 2-minute token for its own audience, pinned to team and flow; the CDP route answers 404/409/402/423 or queues one invocation with Django's id, and a replayed id queues nothing; proven into maildev.WORKFLOWS_API_SEND_JWT_SECRETneeds one value in Django and Node per environment. -
Email API: POST /emails behind workflows-email-api, with the error catalog: landed
2ad89c19962; onlyphs_keys withemail_send:writesend; flag off returns 403email_api_not_enabled; one module owns all 16 error codes withattranddocs_url; returns200 {id, url}; proven by cURL into maildev with verbatim error responses onassets/email-api-471. -
Re-chart onto the upstream stack (Michael, 2026-10-16). Supersedes the inline-content call shape, the
/emailsroute and the hidden API sends workflow of the 2026-10-14 charting.- Base: the transactional email API on PostHog/posthog master: a message template made transactional gets one linked API-trigger workflow, and
POST /messaging_templates/{id}/send/sends it with aphs_key andhog_flow:write, behindworkflows-transactional-email-api. Content is Liquid; caller data arrives as{{ variables.* }}. - Metadata on the message template:
key(unique per team, set on create),managed_by(guiorcode),source_repository,source_path,source_ref. Salvaged from the closed workflows-as-code PRs (feat(workflows): add the key column and its unique index to workflows PostHog/posthog#105958, feat(workflows): mark code-managed workflows and keep their edits local PostHog/posthog#103540). - Upload: an idempotent upsert by key,
PUT /messaging_templates/by_key/{key}/, open tophs_keys withhog_flow:writeand to normal auth. Body{name, subject, html, text?, preheader?, source?}. It publishes at once (clearing any draft), makes the template transactional on first upload with the default verified sender, and answers 201 created or 200 unchanged. The serializer declarespreheader. - Send by key:
POST /messaging_templates/by_key/{key}/send/reuses the existing send service, flag, scope and throttles. - Lock timing: the first slice ships without a lock. The library and editor show a "Managed by code" tag, the source line, and a notice that the next upload overwrites edits. A later slice adds the server lock (403
immutablewith why and fix on update, design and draft writes), a read-only HTML preview, no draft autosave, and duplicate as an editable copy. - Only the template is locked in this map. Direct edits to the linked workflow stay possible: a known gap.
- Agent surface: REST plus generated MCP tools for upsert and send by key. The skill teaches upsert, then send by key. No CLI.
- SDK: posthog-node
emails.send({ template, to, data, distinctId }).templateis a union: a key string, or a direct HTML template reference from which the SDK infers the key, the way Resend takes a template inline. The exact shape is the spec's job. - Errors stay agent-friendly: a stable code with what, why and fix.
- Old lanes: feat(workflows): add the api sends origin and one flow per team index PostHog/posthog#116915 closed; Send a real email through SES from a deployed project #465, Email API: idempotency keys and throttles for safe retries #472 and Ship the email send API as one reviewed draft PR #477 closed; posthog-node: emails.send from a key or an HTML file, synced by content hash, returning { data, error } #474–Agent skill: verify a sender, upload the template from the repo, send by key #476 retarget onto this contract; the sender tools (feat(workflows): let agents set up an email sending domain via mcp PostHog/posthog#116863–feat(workflows): expose read-only email sending readiness over mcp PostHog/posthog#116865) stay.
- fix(workflows): keep one mail from label per email sending domain PostHog/posthog#116864: one MAIL FROM label per user, rolled out by organization.
- The billing quota gap in the workflow worker (non-event invocations skip the quota check) goes to its own side fix.
- Base: the transactional email API on PostHog/posthog master: a message template made transactional gets one linked API-trigger workflow, and
-
Spec code-managed email templates and slice them into PRs: Spec: code-managed email templates. One send route
POST /api/projects/{id}/messaging/send/email/withtemplate: {key, hash?} | {id}; the UUID route stays. The SDK sends a file reference as{key, hash}: a hash PostHog never published answers409 template_outdated, then the SDK uploads and retries once; a superseded hash sends the current content with atemplate_supersededwarning. Adoption is a set-oncekeythrough PATCH; a deleted template releases its key. Stack: Email templates: migration for key, managed_by, source and content hash columns #486 migration-only PR, then Email templates: upload a template by key from code #487–Email templates: read-only view of code-managed templates #492 on integration branchspec/code-managed-templates, shipped by #498; posthog-node: emails.send from a key or an HTML file, synced by content hash, returning { data, error } #474–Agent skill: verify a sender, upload the template from the repo, send by key #476 open their own PRs. -
Migration: key, ownership and content hash columns: draft PostHog/posthog#118769, reviewed head
a5dbb10460b; migrations0007and0008(concurrent partial unique indexunique_message_template_key_per_team). Whichever of it and feat(workflows): draft and publish edits to transactional email templates PostHog/posthog#118664 lands second renumbers. Integration branchspec/code-managed-templatesstarts at its tip. -
Landed on
spec/code-managed-templates(tip7a117081e1f): #487 upload by key, content hash, shared save service; #491 server lock,403 immutable, release bymanaged_by: gui; #488 send route/messaging/send/email/with the hash rule; #489 "Managed by code" tag, source line, notice, Duplicate; #490 adoption by a key-only PATCH. -
posthog-node
emails.send: draft PostHog/posthog-js#5312, reviewed head363b0e332a9; proven live against the integration branch into the dev mail catcher (first send uploads, an edit re-uploads, an older file keeps the newer content with a warning). Its upload-race follow-up is #499. -
Landed on
spec/code-managed-templates(tip7845707ba33): #492 read-only preview, "Stop managing in code", guard that also holds against feat(workflows): draft and publish edits to transactional email templates PostHog/posthog#118664's autosave; #499if_not_supersededupload option, set by posthog-node on automatic uploads (SDK commit on feat(node): send transactional email with the flat SDK contract PostHog/posthog-js#5312). -
Docs page: draft PostHog/posthog.com#21306 at
/docs/workflows/transactional-email-api, 27 anchored error sections quoting the server's strings. -
Agent skill: draft PostHog/context-mill#433 and the name reservation PostHog/posthog#118927; ranks first for all five trigger prompts; an agent session's first send landed in the dev mail catcher. Sender verification through the MCP is unproven until feat(workflows): let agents set up an email sending domain via mcp PostHog/posthog#116863–feat(workflows): expose read-only email sending readiness over mcp PostHog/posthog#116865 land. Merge order: posthog.com#21306 published and posthog-js#5312 released before context-mill#433.
-
Follow-up stacks (Michael, 2026-10-17). Two stacks on top of feat(messaging): upload, send and lock code-managed email templates PostHog/posthog#118924. Stack A, copy as code: edit a code-managed template visually in PostHog, then copy the result back into the repository file (salvaging feat(workflows): copy a workflow as @posthog/workflows code PostHog/posthog#104452 and the "keep edits local" design of feat(workflows): mark code-managed workflows and keep their edits local PostHog/posthog#103540). Stack B, on top of A: with GitHub connected, the same button opens a PR that updates the template's file from its stored source. Research #500, spec #501.
-
Follow-up answers (Michael, 2026-10-17). "Edit locally" becomes edit with your agent: PostHog hands the person a prompt for their coding agent that points at the template's file, with starter text they complete; no visual local editing. Copy formats: the agent prompt first, the HTML file second. The template's source metadata must carry repository and path: posthog-node and the skill fill
source.repositoryandsource.ref. A release claimed back by an upload reloads read-only and offers "Duplicate with my changes" (#508).rewrite_email_asset_urlgets a side fix through the shared save-and-sync path. -
Spec copy-as-code and open-a-PR: Spec #502, revision 4. No editing inside PostHog: "Edit with your agent" copies a prompt for the template's file; "Copy HTML file" is secondary. "Run with PostHog Code" and
POST messaging_templates/{id}/request_change/(MCPworkflows-request-email-template-change) start a PostHog Code task that opens the PR, only for agents and people who can't reach the repository. posthog-node fillssource.repository,pathandref. Stack A (Code-managed templates: the change prompt on the template API #512, Edit with your agent: copy a prompt for a code-managed template's file #503, Copy as code: copy a code-managed template as its HTML file #509) onspec/template-agent-edit, shipped by #516; stack B (Request a change: start a PostHog Code task that opens a PR for a code-managed template #513, Run with PostHog Code from the Edit with your agent popover #514, Agents edit a code-managed template's file when they can, and request a change through PostHog Code when they can't #515) onspec/template-request-change, shipped by #517 throughqa-swarm; posthog-node: fill the template source repository, path and ref on upload #510, Skill: pass the template source on MCP uploads #511 and Docs: change a code-managed template with an agent, PostHog Code or PostHog AI, and where source details come from #507 land on the open SDK, skill and docs PRs. -
Sandbox rule (Michael, 2026-10-17).
request_changekeeps the tasks product's rule: a call from inside a PostHog Code sandbox is refused. PostHog AI and the Slack bot hand the person the template link; "Run with PostHog Code" opens the PR. Stack A shipped as draft PostHog/posthog#118980, stacked on feat(messaging): upload, send and lock code-managed email templates PostHog/posthog#118924. -
Resend parity (Michael, 2026-10-20). New direction on top of the shipped stacks: React Email templates from code, a send that answers OK only once the email is durably queued, sender-scoped keys against staging sending from the production domain, and a Resend-to-PostHog migration skill; no 100% mirror. Research #522 (SDK and React parity), #523 (send acknowledgement), #524 (sender-scoped keys), then grilling #525 and the architecture prototype #526.
-
Research what a switch from Resend needs from posthog-node and a skill: report on branch
research/resend-parity-sdk. The draft shares Resend's{ data, error }return but not its input: nofrom,subject,html,text,react,replyToor idempotency key. A mechanical rewrite needs inline content (a decision),from,replyTo, anidempotencyKeyargument anderror.name. React Email renders once into a stored Liquid template only for substitution-only emails; every provider checked renders it at send time. Resend keys aresending_accessplus adomain_id; PostHog'sphs_key has scopes only. Both providers' DNS records can coexist if the MAIL FROM label differs. -
Full attack on Resend (Michael, 2026-10-20). The goal is many Resend organizations moving to PostHog, not parity alone. Added research #527 (Resend's product, most loved features, what users miss) and prototype #528 (React Email: render once with Liquid, per send, or a hybrid). Grilling #525 starts now and takes the research results as they land.
-
Wizard run and attack plan (Michael, 2026-10-20). The PostHog Wizard gets an easy run that migrates a codebase from Resend; Resend only for now, other email services maybe later. Research #529. The go-to-market plan (reach, speed to first send, marketing tracking, impact) is decided with Michael in a private grilling ticket in
Silthus/personal; grilling Grill Resend parity: React templates, delivery guarantee, sender-scoped keys #525 keeps the product shape. -
Research how a project secret key can be limited to email senders: report compares selected-sender keys, exact-domain keys, project guardrails and sandbox-only keys, with API-to-worker enforcement seams; the product promise remains for grilling.
-
Research whether a send answer means the email is durably queued: source-linked report on
research/email-send-ack; 202 already waits for Postgres enqueue, but ambiguous retries and best-effort later signals need a durable idempotency receipt and message-status contract. -
Research what a PostHog Wizard run needs to migrate a codebase from Resend: report on
research/wizard-resend-migration;wizard migrateexists (Statsig, flat) and becomes a family withresend; a skill-driven run, two person hand-offs (DNS,phs_key), no commit; new context-millmigrate-emailskill linking tosend-transactional-email; first PRs in context-mill and wizard; waits on Grill Resend parity: React templates, delivery guarantee, sender-scoped keys #525 (inline content) and Prototype React Email templates: render once with Liquid or render per send #528 (React path). -
Prototype React Email templates: branch
prototype/react-email-templatesatebd8ae65c10. React path (Michael, 2026-10-20): posthog-node ships the hybrid in two PRs. First, rendered HTML is sent inline with a requiredtemplatekey, rendered by posthog-node itself onreact-dom/server(optional peer, no Resend package). Second, thePreviewPropsrender uploads as a "Managed by code" template that each send links to. Render once with Liquid does not ship for React; Liquid stays for HTML template files. A React send needs an explicitsubjectand key; a send by key alone is refused for a preview-render template. Open for the first PR: the plain-text source, and whether a non-React inlinehtmlsend needs a key. -
Grill Resend parity: handoff with every decision line; prototypes
prototype/email-send-bodyandspike/email-attachments. In short: "landed" means one migration skill moves a Resend team with React components unchanged; count Code-sending orgs and Switched orgs through send attribution (source,sdk_version,migrated_from, org propertyemail_migrated_from). A send takes one renderedbody{ html?, text }from renderershtml(),text(),react(), with a requiredtemplatekey andsubject, optionalfromandreplyTo; it runs on the one-workflow-per-template path. A 202 means stored in the Postgres queue; checks run before queueing; later failures surface through a status record read withemails.get(id). Idempotency adopts Harley's contract. Restricted keys list domains and/or sender addresses. Attachments are in scope (512KB first slice). Skill: context-millmigrate-email/resend, v0 before the SDK; Wizardmigrate resendas a family. Order: send attribution, body route, skill v0, posthog-node PR 1, idempotency, skill v1 and Wizard; then attachments and checks before queueing; then key limits, status record, posthog-node PR 2. No dates on the plan. No reputation-isolation ticket. -
Send attribution: draft PostHog/posthog#120628, reviewed head
ed6b4b59bdf.source,sdk_version,migrated_fromon thetransactional email api sendevent ofmessaging_templates/{id}/send/; headerX-PostHog-Migrated-From; aposthog-node/orposthog-edge/User-Agent token meanssdk; the first migrated send setsemail_migrated_fromon the organization once. The body route reusesSendSourceandmark_organization_migrated(products/messaging/backend/presentation/send_source.py); posthog-node and skill v0 send the header. Fork CI waits for maintainer approval. -
Research what makes every email from code tracked on the person by default: report. The claim holds only for "send with the same user ID you use in PostHog, and see your email events next to that user's product events". The body route keeps
distinctIdand a capture policy scoped to sends from code; posthog-node requiresdistinctIdfor that mode, with an explicit email-only fallback; skill v0 reuses the app's ID; skill v1 proves send, engagement and product event on one person; UTMs on for new code templates with an opt-out. A click-to-new-browser bridge is a separate, privacy-reviewed follow-up. -
Shape the architecture of Resend parity: handoff, architecture on
prototype/resend-parity-architecture@0f0bc0f. Sends stay on HogFlow: Django builds oneEmailSendCommand, cdp-api checks and queues each send once per idempotency key and stampsstate.send, the worker delivers without rendering again, andEmailStatuswrites a ClickHouse table thatGET emails/{id}reads. Refactors land first, feature PRs stack on them. -
Research whether the recipient's distinct ID can be inferred from the email address:
distinctIdstays optional with the address fallback; the email lookup ships as its own PR after the production proofs. -
Spec Resend parity and slice it into PRs: spec, 20 slices, strand drafts and lane prompts on
spec/resend-parity@8e5dd3d. Built per strand (trial): #535 send-api (lane running), then #536 posthog-node, #537 posthog-python, #538 docs-api, #539 skill, #540 wizard, #541 docs-migrate, and the HITL task #542. Duplicate limit of the idempotency store accepted for now (Michael, 2026-10-21); a side fix thread works on closing it. -
Engagement capture on by default, billed (Michael, 2026-10-21). Capture turns on by default for new sends from code, with a per-send opt-out. The events bill as normal analytics events: the email meter bills the send, and opens and clicks are real tracking events. What we sell is tracking that works out of the box. Existing workflows keep their setting. Research #533 found free events possible but needing Billing and Ingestion approval; Michael dropped the free part.
-
Split with Harley (Michael, 2026-10-21). Harley builds the send API and posthog-python directly in PostHog/posthog, on his idempotency stack and feat(messaging): upload, send and lock code-managed email templates PostHog/posthog#118924. Michael builds posthog-node, the skill and the Wizard run. The shared SDK contract is PostHog/posthog#120921. Spec #531 keeps the server slices as Harley's and charts only our tickets. Updated by the flat-parameters line below: Michael now also builds the code-content send API.
-
Flat send parameters (Michael and Harley huddle, 2026-10-21). Replaces the content helpers and the required top-level
key(and with them Shape the architecture of Resend parity #526's "Library key" decision and ADR 0004).sendtakes one of two flat forms:template_id+template_data(templates made in PostHog have no key), ortemplate_key+subject+html/text+from/reply_to. Python raises typed exceptions, Node returns{ data, error }. New client optionsapp_hostandmax_retries;429 quota_limitedis never retried. Harley builds posthog-python template sends now; Michael builds the code-content send API and posthog-node. Contract: PostHog/posthog#120921, Harley's notes. -
Builders per strand for Spec Resend parity and slice it into PRs #531 (Michael, 2026-10-21). Trial of the retro's slicing method: the approved slices group into strands (the slices that edit the same files, here roughly one per repo). Each strand is one issue, a sub-issue of this map with native blocking between strands, listing its slices in order as a checklist that links the spec. One Opus 5.5
highlane per strand builds its slices in order, with the PRs the spec lists, and one GPT-6.1 Sol review covers the strand's whole diff. The PR count stays as specified; only the worker count changes.
Not yet specified
-
Overlay context-mill skills in the PostHog MCP
learncatalog (skills.zip), solearnsearch returnssend-transactional-emailand theinstrument-*skills. -
Name the posthog-node release that ships
posthog.emailsin the skill once feat(node): send transactional email with the flat SDK contract PostHog/posthog-js#5312 releases. -
Switching the default to the trial sender once the sandbox sender ships: examples move to
onboarding@trial.posthog.com, and sends without a verified domain follow the sandbox recipient rule (verified organization members only). -
A client plugin.
-
Locking the linked workflow's content for code-managed templates.
-
Idempotency keys for sends: owned by Harley upstream, not this map. A shared Valkey-backed layer, feat(cdp): add a shared idempotency key store for workflows PostHog/posthog#120446, used by transactional emails in feat(messaging): add an idempotency key to the transactional email api PostHog/posthog#120392 and by webhooks in feat(workflows): idempotency key on the webhook trigger PostHog/posthog#117287 (Slack). The SDK follows his contract. Open: whether a Valkey key with a TTL meets Research whether a send answer means the email is durably queued #523's durable receipt, and if not, a follow-up for Harley.
-
A dry-run check of an upload, and a CLI.
-
A docs page with a copyable agent prompt, and a line in the llms.txt instructions for AI coding assistants.
-
An in-app onboarding page like Resend's: key, snippet, "Copy for AI", live send button.
-
Sending tier caps versus time-critical transactional mail such as password resets.
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
48 remaining items
Silthus commentedon Oct 29, 2026
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):
emails.get, now in feat(node): send transactional email from a template PostHog/posthog-js#5418.Restack before Nov 2:
eb01f45cbcand keeps the spec'sinvalid_template_id.5abcf294e0and takesmigrated_fromand code-form capture from py Model list for the picker #3.