Repository navigation
Conversation
🤖 CI reportℹ️ MCP UI apps size — 34 app(s), 27969.1 KB JSBuilt size of each MCP UI app (
ℹ️ MCP agent API — agent-facing tool changesWhat this PR changes for agents, from the tool schema snapshots and tool definitions. Tools changed (1):
|
Silthus
force-pushed
the
email-v2/07-send-policy
branch
from
October 25, 2026 11:39
4c04d3c to
5e57bdf
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks
branch
from
October 25, 2026 11:39
c4fc6e3 to
c55794e
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y, not in it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…address Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
email-v2/08-admission-checks
branch
from
October 25, 2026 18:50
c55794e to
66e7f10
Compare
…does Admission rendered the email step's cc and bcc with HTML escaping, with only the request's variables, and left Hog templates unevaluated. The worker renders them as plain text, merges the workflow's variable defaults, and evaluates Hog. A copy that took a display name, an apostrophe address or a default from a variable, or that came from a Hog template, could name a suppressed address admission did not see. Admission now renders the copies through the worker's input formatter with the invocation's merged variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An accepted send refused as `recipient_suppressed` because of the email step's stored cc or bcc answered with the copy for a suppressed `to`. The refusal body now carries `attr`: `to` when the request's `to` is suppressed, null when only a stored copy is, with a why and fix that name the workflow's email step. Every send-check refusal body carries `attr`, so Django can rely on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recipient_suppressed rejection now names the suppressed addresses it found, so the refusal picks its attr and copy from them instead of looking the recipients up again and counting each suppressed_hit twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the code-first rebuild of the email send stack; replaces parts of PostHog#121850 (refuse a send the send checks would skip).
Problem
send/email/can answer202and never go out. The email worker skips it later for a suppressed or undeliverable recipient, a paused email, a suspended project, or a sender that can't send. The caller sees success.202, withwhyandfix.Impact
Pays into Grow Workflows through billed
workflows_emailsactions per paying org: a developer who sees why a send failed fixes it, rather than losing the email. In scope: CDP email sends.Merge Danger
Warning
🟡 Merge risk: medium · two-way door: a revert lets the worker skip these sends again behind a
202400 {error, code, detail, why, fix}before it queues, and releases the idempotency key. Onlysend/email/uses that route, behindworkflows-email-from-code. The template route is unchanged.503"did not confirm the send" and nothing is sent. A valid send delivers. The live run below shows both.Skipping send:prefix, which the broadcast test-send modal reads.Changes
invalid_recipient,recipient_suppressed,template_paused,sending_suspendedorsender_not_verified) withdetail,whyandfix.to, and theccandbccstored on the email step.sourceglobals, and a copy that fails to render, stay with the worker.attr. A suppressedtonamesto. A suppressed stored copy answersattr: null, since the caller sent no cc, and itswhyandfixpoint at the workflow's email step.suppressed_hit. The policy's rejection names the suppressed addresses, so the refusal takes itsattrand copy from them without a second lookup.invalid_recipient) runs ontobeside the send policy, as the worker's check does. The email validation service returns why an address can't receive email, so the route and the worker share one check.detailloses itsSkipping send:prefix; the worker adds it back to its log line.How did you test this code?
Test seams: cdp-api's accepted-send route over HTTP (
cdp-api.serial.test.ts), the send policy (email-send-policy.test.ts) and the email service (email.service.test.ts).Test rationale:
send checksrows incdp-api.serial.test.ts, run red first: a send the worker would skip answering202.leaves the checks to the worker on the template route: the template route starting to refuse.refuses a keyed send when …, then sends it on the same key once fixed: a refusal that holds its key, for a paused workflow and for a suppressedccorbccstored on the workflow, rendered from a display name, an apostrophe, a default variable and Hog.attrand hit columns of those rows: a stored-copy refusal namingto, and a refusal that counts its suppression hit twice.email-validation.service.test.tsandemail.service.test.tspin theSkipping send:prefix the broadcast test-send modal matches.Proofs: live runs saved with
fleet proof, curl into maildev. A suppressed recipient is a manual suppression row. The no-mail-server domain isnomx.invalid.8b150c5, Django at #579a5d494f)809dec8, Django before #579recipient_suppressed,attr: "to", with why and fix; nothing sentdelivery_unavailable; nothing sentinvalid_recipient,attr: "to"; nothing sentdelivery_unavailable; nothing sentrecipient_suppressed,attr: null, why names the email step's cc; nothing sentdelivery_unavailable; nothing sent{{ variables.ops }}, whose workflow default is a display-name address on the suppression list.CI: the fork has no Depot runners, so those jobs never start, and no secret for the project-board app, so
flags-project-boardfails; the other GitHub-hosted checks pass. The lane check passes: against master the PR changes onlynodejs/.Release status
Docs update
None in this PR. The send API docs follow when the flag opens.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)
srv2of map Map: send code-managed email templates with one PostHog call #461 from the reviewed fork feat(messaging): refuse a transactional email the send checks would skip #556, with its review fixes kept.tdd,writing-tests,pr-description,writing-pr-descriptions.workflows-email-from-code; the template route is unchangedInvolvement: one brief from Michael with decisions; review rounds 1 and 2 (GPT-6.1 Sol team review, Opus fresh eyes) fixed; final read 1 by GPT-6.1 Sol fixed, final read 2 found nothing; live and cross-version proof captured.
🤖 Generated with Claude Code