Skip to content

Agent skill: verify a sender, upload the template from the repo, send by key #476

Closed

Description

@Silthus

Map: Map: send email from code with one PostHog call
Spec: Spec: code-managed email templates, section "Agent skill" (retargeted from the superseded #467)

Cast: Opus 5.5, high
Repository: PostHog/context-mill (plus a one-line name reservation in PostHog/posthog)

Question

Can a coding agent that is asked to "send a welcome email" find PostHog, verify a sender through the PostHog MCP, upload the template from the user's repository, and wire a working send by key into the user's code?

Delivers end to end. A skill send-transactional-email in PostHog/context-mill next to the instrument-* skills (context/skills/omnibus/send-transactional-email/ with config.yaml and description.md, shared docs from the docs page #475), built into the context-mill release and served through the PostHog MCP. Triggers: send an email, welcome email, password reset, signup confirmation, transactional email, email from code. It walks the nine steps in the spec's "Agent skill" section, in that order:

  1. verify a sender first through the PostHog MCP 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, behind workflows-email-domain-agent-setup), and make it the default sender; fall back to the PostHog UI when those tools are missing or refused;
  2. write the template as an HTML file in the repository, with every value from {{ variables.* }} and no person.*;
  3. upload it by key with the MCP upload tool (#487), passing the source;
  4. stop on the plain 404 that means the transactional email flag is off for the project;
  5. write the send: in Node, emails.send with emails.templateFile(...) (#474), so later edits reach PostHog on their own; in other languages, the send route (#488) by key, with an upload wherever the project deploys. data for every value, distinctId for attribution, the secret key from an environment variable on the server.
    No idempotency key: the skill says sends are not idempotent. A companion one-line change adds the name to the context-mill skill names in PostHog/posthog's bundled skills check (products/skills/backend/bundled_skills.py and the build's reserved names).

Test seams. The context-mill build: the bundle contains the skill and its shared docs. A small set of prompts from the trigger list that the MCP skill search ranks this skill first for.

Write scope. context/skills/omnibus/send-transactional-email/ in PostHog/context-mill; the two name lists in PostHog/posthog.

Proof.

  • Red: the MCP skill search for "send a welcome email from my Node app" does not return this skill.
  • Green: it ranks first, and the built bundle carries it.
  • Local: an agent session with the built skill and the PostHog MCP pointed at the local dev stack, in a scratch Node project with no sender: it checks for a sender, walks verification, uploads emails/welcome.html through the MCP, writes a send by key, and the email lands in the dev mail catcher. Transcript excerpt in the PR.

Acceptance criteria

  • The skill tells the agent to verify a sender through the PostHog MCP before the first upload.
  • Upload from the repository, then send, in that order; Node sends from the file reference.
  • It covers the transactional email flag off and workflows-email-domain-agent-setup off.
  • Templates read variables only; no idempotency promised.
  • Its name cannot be shadowed by a product skill in PostHog/posthog.
  • Local proof: an agent's first send lands in the dev mail catcher.

How to build (map #461)

  • /tdd red-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.
  • Repo skills apply where the diff touches their area: /writing-tests, /improving-drf-endpoints, /django-migrations, /adding-api-scopes, /implementing-mcp-tools, /writing-ui-components, /writing-kea-logics, /writing-user-facing-copy, /writing-code-comments.
  • Read code from upstream/master (PostHog/posthog). The fork's master is stale.
  • Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use example.com. The private note the map points at is background only; never quote it.
  • Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge.

Activity

  1. changed the title [-]Agent skill: send transactional email, verifying a sender first[/-] [+]Agent skill: verify a sender, upload the template from the repo, send by key[/+] on Oct 16, 2026
  2. self-assigned this
    on Oct 17, 2026
  3. Silthus commented on Oct 17, 2026

    @Silthus
    OwnerAuthor

    Runtime: T3 child task
    Task: node:delegated-task:command%3Amcp%3A955e36b3-3986-469c-b816-09cf656780af%3Adelegate-task%3Awf-email-impl-476-2
    Cast: Opus 5.5, high. Cap 240 minutes.

  4. Silthus commented on Oct 17, 2026

    @Silthus
    OwnerAuthor

    Resolution

    Shipped: two draft PRs, both linked to the T3 thread, neither marked ready.

    • PostHog/context-mill#433, branch Silthus:feat/send-transactional-email-skill against main. Reviewed head 965a656ea1d4f7b6612186d2a8b0575842302dee. fleet proof gate passes (HEAD reviewed in final read 3).
    • PostHog/posthog#118927, branch Silthus:chore/reserve-send-transactional-email-skill against master. Reviewed head 5845c44a0de2921942814f9e10b05e679450e928. fleet proof gate passes (HEAD reviewed in final read 1).

    What shipped

    • The skill, at context/skills/omnibus/send-transactional-email/ (config.yaml, description.md). It ships the docs page as references/transactional-email-api.md, and consumers see it as send-transactional-email. It walks the spec's nine steps in order:
      • The MCP check and the platform.
      • A sender check with integrations-list. An unknown sender is verified once rather than guessed.
      • Sender setup with the feat(workflows): let agents set up an email sending domain via mcp PostHog/posthog#116863 tools, with polling bounded and the SPF, DMARC and 403 rules. When those tools are missing or refused, the person goes to the PostHog UI.
      • An HTML template with a <title> subject and {{ variables.* }}.
      • The upload with workflows-upload-email-template and source. sender_unavailable leads to a verify, then Make default.
      • A stop when the flag is off: the tools are hidden, or a 404 without a code or with not_found.
      • A phs_ key read from POSTHOG_PROJECT_SECRET_API_KEY.
      • The send: posthog.emails.send with templateFile(..., { name }), including Next.js and edge. Other languages use the send route by key plus an upload where the project deploys.
      • Errors handled by error.code.
      • It promises no idempotency, and the agent asks before any test send.
    • Name reservation: the name is added to reserved_names.py (build lint) and bundled_skills.py (skills store). The store list also gains instrument-metrics, which had drifted. The three docs that list the omnibus skills are updated.

    Proof

    Bundle seam. scripts/lib/tests/send-transactional-email-skill.test.js builds the skill with fetch stubbed for the docs URL. It asserts the name, the reference listing and the reference content.

    red:   AssertionError: expected undefined to be defined   (no omnibus-send-transactional-email)
    green: Tests  1 passed (1)
    mutation (shared_docs removed): expected '---\nname: omnibus-send-transactional…' to contain '`references/transactional-email-api.m…'
    

    On 965a656: pnpm test 220 passed, and pnpm build emits omnibus-send-transactional-email.zip.

    Ranking seam. It uses the PostHog MCP's own SkillCatalog.searchResults (services/mcp), over the released agent-skills-latest skills.zip with context-mill overlaid the way download-context-mill-skills does it.

    Prompt Red (context-mill v1.57.0) Green (965a656)
    send a welcome email from my Node app not returned rank 1 (152 vs 75)
    send a password reset email not returned rank 1
    send a signup confirmation email not returned rank 1
    transactional email not returned rank 1
    send an email from code not returned rank 1

    Local agent session. The stack:

    • Django and the CDP worker from spec/code-managed-templates e52feb3ccd9, with the flag forced on, private personhog, and an SES Valkey.
    • The local MCP dev server, with FEATURE_FLAG_OVERRIDES.
    • posthog-node from posthog-js#5312 5a896e92.
    • Claude Code on Sonnet 5.5, with a clean config and only this skill and the MCP.

    The project started with no sender, and the MCP had no sender tools.

    Transcript excerpt, skill at 5a1f8e1
    Turn 1: "Send new users a welcome email when they sign up. We use PostHog, and its MCP server is connected."
    > Skill send-transactional-email
    > exec search workflows-upload-email-template|workflows-send-email|integrations-   < upload and send tools present, no integrations-email-*
    > exec call integrations-list {"kind":"email"}                                      < count: 0
    > Write emails/welcome.html, Edit src/posthog.js (secretKey), Edit src/signup.js (templateFile + emails.send), .env.example
    Reply: "... the MCP here doesn't have the tools to create one. In PostHog, go to Workflows → Channels ... Tell me when it shows as verified."
    
    (seed script stands in for the UI verification of hello@example.com; phs_ key set in .env)
    
    Turn 2: "Done ... you can run the signup demo once as the test."
    > exec call workflows-upload-email-template {"key":"welcome","name":"Welcome",...,"source":{"repository":"github.com/example/acme-app","path":"emails/welcome.html",...}}
      < result: created, managed_by: code
    > npm run signup-demo   (run twice: see below)
    
    PUT  /api/projects/7/messaging_templates/by_key/welcome/ -> 201
    POST /api/projects/@current/messaging/send/email/        -> 202
    POST /api/projects/@current/messaging/send/email/        -> 202
    
    Dev mail catcher:
    {"from": ["hello@example.com"], "to": ["jane@example.com"], "subject": "Welcome to Acme, Jane!"}
    {"from": ["hello@example.com"], "to": ["jane@example.com"], "subject": "Welcome to Acme, Jane!"}
    
    • The agent's first send landed in the dev mail catcher. The upload matched the file byte for byte, so each send was one request.
    • The agent reran the demo to read output it had truncated, which sent a second email, and it reported this. 3d85758 adds the rule that prevents it.
    • The full evidence is in ~/.local/state/fleet/proof/PostHog/context-mill/433/965a656…/live-run/.

    Name reservation, with a scratch product skill named send-transactional-email:

    upstream/master: hogli lint:skills -> OK: 164 product skill(s) ... passed; store refuses send-transactional-email: False, instrument-metrics: False
    this branch:     hogli lint:skills -> ERROR: 'send-transactional-email' is owned by PostHog/context-mill ...; store refuses both: True
    

    test_build_skills.py and test_bundled_skills.py pass, and the pre-push hogli ci:preflight --strict reported 0 failures.

    Review

    Opus 5.5 at high effort, in fresh subagents, recorded with fleet proof review-start and review-finish.

    • Round 1, FAIL: one high (the merge order on the unpublished docs page, now in the PR's risk callout) and five mediums, all fixed: a disconnected MCP read as flag-off, the sender_unavailable contradiction, unbounded polling, a test send without asking, and the Next.js path. Of the lows, five were fixed. Two were rejected: the version number can't be named until Think through the Elements tab PostHog/posthog#5312 releases, and a test that the two name lists match would cross the isolated products.skills boundary.
    • Final read 1, FAIL: one medium, fixed. It asked instead of verifying an unknown sender.
    • Final read 2, FAIL: one medium, fixed. A member's 403 on verify had no route. One low was rejected, because feat(workflows): expose read-only email sending readiness over mcp PostHog/posthog#116865 adds config.verified to integrations-list.
    • Final read 3: PASS. Two optional lows were left as they are, with a reason.

    Calls made

    • Ranking measure: the MCP learn catalog (skills.zip) carries no context-mill skill today, instrument-* included. The PostHog MCP serves context-mill skills only as resources. So the ranking ran the MCP's own scorer over the overlaid catalog that agent hosts load. It is a proof script, not a committed test, because the scorer lives in PostHog/posthog.
    • Default sender: no MCP tool sets one. The first verified sender becomes the default, and a successful verify sets the default when none is set, so sender_unavailable leads to a verify and then Make default.
    • The upload passes the <title>, preheader and whole file, so posthog-node's first send needs no re-upload. templateFile(..., { name }) keeps the name from drifting to the subject.
    • Local docs: the build fetches the docs URL, which still answers 404. The local build used a seeded doc cache built from posthog.com#21306's c86c7a6.
    • The proof agent ran on Sonnet 5.5 as a typical user agent. The run is at 5a1f8e1. The later commits are wording the session did not reach.
    • Harness bug: pnpm 12 ignored the package.json overrides and pulled @posthog/core from npm. A first run hit network_error, and the agent stopped without retrying. The install was fixed and the proof rerun.
    • Scope: besides the two name lists, the posthog PR updates the three docs that list the names and adds instrument-metrics to the store list.
    • Assignee: setting it on chore(skills): reserve the send-transactional-email skill name PostHog/posthog#118927 failed. Silthus lacks the permission upstream.
    • context-mill uses pnpm (pnpm-lock.yaml), although its AGENTS.md shows npm commands.

    Gaps

    Follow-ups

    • MCP learn catalog: the published skills.zip doesn't overlay context-mill skills, so learn -s in the PostHog MCP never returns instrument-* or this skill. Overlaying them in ci-agent-skills.yml would make the ranking proof hold in production.
    • posthog-node version: name the release that ships posthog.emails in the skill once Think through the Elements tab PostHog/posthog#5312 releases.
    • Ownership: context-mill's README ownership table and CODEOWNERS could name the Workflows team for this skill.
    • Docs page: outputFileTracingIncludes is under experimental in Next.js 14 and earlier, and the docs page doesn't say so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions