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/06-template-editor
branch
from
October 25, 2026 14:59
bb9ed7b to
4117342
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 25, 2026 15:00
49cab17 to
24881fa
Compare
Silthus
force-pushed
the
email-v2/06-template-editor
branch
from
October 25, 2026 17:38
4117342 to
dc7683f
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 25, 2026 17:38
24881fa to
801bdfc
Compare
Silthus
force-pushed
the
email-v2/06-template-editor
branch
from
October 25, 2026 22:27
dc7683f to
d321dd8
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 25, 2026 22:28
a5d494f to
1d7fe4c
Compare
Silthus
force-pushed
the
email-v2/06-template-editor
branch
from
October 26, 2026 01:11
d321dd8 to
6730e40
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 26, 2026 01:11
1d7fe4c to
0eeb308
Compare
Silthus
force-pushed
the
email-v2/06-template-editor
branch
from
October 26, 2026 02:34
6730e40 to
d3da69f
Compare
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 26, 2026 02:35
0eeb308 to
25f064a
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template send can't name `from`, so a sender_not_verified refusal on it answers with no attr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as them Django's own refusals on send/email/ answer without why and fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send refused because the workflow's stored cc or bcc is suppressed answered `attr: "to"`, though the caller sent no cc. When the pipeline's refusal body carries `attr`, null included, the API now answers with it. A body without `attr` comes from an older pipeline and keeps the mapped field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send by `template.key` can now name `from`. When an older pipeline refuses it with `sender_not_verified` and no `attr`, the answer now points at `from`, as it does for content your code rendered and as the pipeline does for any send that names a sender. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
email-v2/08-admission-checks-django
branch
from
October 26, 2026 05:29
25f064a to
0f434a3
Compare
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: Messaging, email API sends.Merge Danger
Warning
🟡 Merge risk: medium · two-way door: a revert answers a refusal with
503againsend/email/passes a cdp-api refusal on as400withwhyandfix, behindworkflows-email-from-code. The template-by-id route shares the reader, but its cdp-api route never refuses.202, and the worker skips it, as on master today. A valid send delivers. The live run below shows both.fromon no verified domain answerssender_not_allowed.sender_not_verifiedmeans the sender can't send: the send names nofromand the project has no verified default sender, or the template's or the named address's sender is no longer verified. It namesattr: "from"on a send that names afrom, and on a code send with rendered content. A send by template withoutfromnames no field.Changes
What a caller sees change
400withwhyandfix, and nothing is sent. The codes areinvalid_recipient,recipient_suppressed,template_paused,sending_suspendedandsender_not_verified.fromon no verified domain answerssender_not_allowed. That holds for a send bytemplate.keytoo, which can namefromsince feat(messaging): send a stored template by key on send/email #574.{intoor a template that isn't transactional, still answer400withoutwhyandfix. The400help sayswhyandfixcome when the answer has them.429help says which case it is:throttled(retry later) orquota_limited(a retry sends nothing).workflows-send-emailsays a400withwhyandfixmeans nothing was sent.How it works
code,detail,whyandfix), not by a list of codes. A code cdp-api adds later still reaches the caller as a400.400from cdp-api still maps to503.attrfrom cdp-api when the body has the key,nullincluded. A stored cc or bcc that is suppressed then answersattr: null, since the caller sent no cc. An older cdp-api sends noattr, so Django falls back to its own map from code to field. That map namesfromfor a sender refusal whenever the send names afrom, as cdp-api does.How did you test this code?
Test seams:
send/email/and the template-by-id route through the DRF client with cdp-api mocked (test_transactional_email.py).Test rationale:
whyandfix, a code Django does not know yet dropped, and the template-by-id route reading a refusal.fromanswerssender_not_allowed, and a missing default sender answerssender_not_verified. A cdp-apisender_not_verifiednamesfromon a code send and on a key send that namesfrom, and no field on a template send without one.attrrows: a cdp-api refusal withattr: nullanswersnull, and one without the key keeps Django's mapped field.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.a5d494f, cdp-api at #5771e23a26)8ce1e9b, cdp-api before #577recipient_suppressed,attr: "to", with why and fix; nothing sentinvalid_recipient,attr: "to"; nothing sentrecipient_suppressed,attr: null, why names the email step's cc; nothing sentdc7683f), then switched Django to8ce1e9b. Sends under the base's key, upload and template answered 202. The suppressed and no-mail-server sends answered 400 withattr: "to". A template send with an unverified sender answeredsender_not_verifiedwithattr: null, and a code send with nofromanswered it withattr: "from".0eeb308is1d7fe4crebased onto feat(messaging): send a stored template by key on send/email #574's two fixes for local templates. Only the OpenAPI409description gained feat(messaging): send a stored template by key on send/email #574's wording, so the runs at1d7fe4cstand.1d7fe4cdiffers froma5d494fin two things: feat(messaging): send a stored template by key on send/email #574 now refusesreply_toandfromon a send bytemplate.idand takesfromon a send bytemplate.key, and this PR namesfromon that key send's sender refusal. No case above sends those fields, so the runs hold at this head.8ce1e9bdiffers froma5d494fonly in readingattrfrom cdp-api. The cdp-api before feat(cdp): refuse an accepted send the send checks would skip #577 never refuses, so that window answers the same at this head.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 only the Python and frontend side.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,improving-drf-endpoints,implementing-mcp-tools,writing-user-facing-copy,pr-description,writing-pr-descriptions.workflows-email-from-code; the template route is unchanged400?Involvement: one brief from Michael with decisions; review rounds 1 and 2 (GPT-6.1 Sol team review, Opus fresh eyes) fixed; a final read by GPT-6.1 Sol found nothing; live and cross-version proof captured.
🤖 Generated with Claude Code