Skip to content

feat(messaging): send the workflow version a template send graded - #585

Draft
Silthus wants to merge 4 commits into
email-v2/11-first-code-send-eventfrom
email-v2/12-send-graded-template-version
Draft

Silthus wants to merge 4 commits into
email-v2/11-first-code-send-eventfrom
email-v2/12-send-graded-template-version

Conversation

@Silthus

@Silthus Silthus commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Part of the code-first rebuild of the email send stack; new in the rebuild, with no closed PR to replace: a template send delivers the content Django graded (decision D3 on map #461).

Problem

  • A template send on send/email/ right after an upload can deliver the previous content, while Django answered that the caller's template.hash is current.
  • Django grades the hash, but the email goes out from the workflow copy cdp-api and the worker cached, which can lag after a lost reload.
  • This is the Django half: it tells cdp-api which workflow version holds the graded content. The cdp-api and worker half is feat(cdp): send a template from the workflow version Django graded #584.

Impact

Pays into Grow Workflows through billed workflows_emails actions per paying org: a developer who uploads a template and sends it at once gets the new content. In scope: the send/email/ route.

Merge Danger

Note

🟢 Merge risk: low · two-way door: a revert stops sending the version, and delivery works as today

Changes

Kind Files Lines
Production 4 +79 −3
Tests 1 +87 −7
  • A template send posts the version of its workflow that holds the graded content, as send.hogFlowVersion. Django reads it after grading the hash, from the writer database, so it is never older than the graded content.
  • A workflow deleted between the grade and the version read answers as the cdp-api 404 does today (Unavailable to the caller), without a stack trace in the log.
  • A send that the email pipeline can't deliver yet gets an explained 503. When cdp-api still holds an older workflow version than the graded one, the caller gets the contract's 503 delivery_unavailable, with why and fix:
{
  "type": "server_error",
  "code": "delivery_unavailable",
  "detail": "This template's newest version hasn't reached the email pipeline yet.",
  "attr": null,
  "why": "The email pipeline still holds an older version than the one PostHog checked the send against, so nothing was sent.",
  "fix": "Retry in a few seconds. If you sent an Idempotency-Key, use the same key.",
  "docs_url": ".../transactional-email-api#delivery_unavailable"
}
  • Nothing was sent in that case, so a retry is safe with or without a key. Django logs the internal reason, workflow_version_behind, which never reaches the caller.
  • Every other 503 keeps its current body, on both routes. Unavailable gains optional detail, why and fix, and only this case sets them.
  • No serializer or OpenAPI change, so no regenerated types.

How did you test this code?

Test seams: the DRF client on send/email/, asserting the body posted to cdp-api (test_transactional_email.py).

Test rationale, each run red first:

  • A template send that posts no version, or a version from before a save. A PATCH that saves new content is followed by a send that must post the new version.
  • A version read from a lagging read replica.
  • A workflow deleted before the version read that logs an exception.
  • The three rendered-content rows compare the whole send dict, which pins that the field is absent there.
  • A version-behind 503 from cdp-api that answers the generic 503, or that puts workflow_version_behind anywhere in the body. The existing 503 rows pin that their bodies have no why or fix key at all, not even a null one.

Proofs: live and cross-version runs are in #584, with this head as the Django side. Prod shape: a gap until read-only production access exists on 2026-11-02.

CI: the fork has no Depot runners, so those jobs never start, and no secret for the project-board app, so flags-project-board fails; the other GitHub-hosted checks pass. The lane check passes: against master the PR changes only the Python and frontend side.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

None in this PR. The explained 503 links to the #delivery_unavailable anchor of the transactional email API docs, which the docs for this stack need to carry.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

Question Answer Who Where
Does a template send deliver what Django graded? Yes, through #584 and this PR 👤 Human map #461, decision D3 of 2026-10-25
May the code carry comments? No; names carry the reasons 👤 Human map #461, 2026-10-25
What does a send get when the pipeline holds an older version? The contract's 503 delivery_unavailable with the version lag in detail and why; replaces a new public workflow_version_behind code 👤 Human approved (conductor, 2026-10-26)
Key the cache on the hash, or on a version floor? A version floor, read after grading from the writer; replaces D3's "key the cache on the hash" 👍 Accepted default approved (conductor, 2026-10-26)
Must the retry reuse the key? Only if the caller sent one: nothing was sent, so a retry without a key is safe too 👍 Accepted default approved (conductor, 2026-10-26)

Involvement: one decision from Michael and one from the map's conductor; review round 1 (GPT-6.1 Sol, Opus fresh eyes) fixed, then final reads by GPT-6.1 Sol; live and cross-version proof captured in #584.

🤖 Generated with Claude Code

Silthus and others added 3 commits October 26, 2026 04:38
A template send on send/email/ now tells cdp-api which version of the
template's workflow holds the content Django graded the hash against, as
send.hogFlowVersion. A cdp-api that knows the field sends from that
version or a newer one instead of an older cached copy. One that does
not ignores it, because the accepted send schema strips unknown keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graded version must not come from a read replica, or an older copy
could satisfy the floor. A workflow deleted between grading and the
version read now logs a queue rejection, like the 404 from cdp-api,
instead of an exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When cdp-api answers 503 workflow_version_behind, the caller gets the
existing 503 delivery_unavailable, with detail, why and fix saying the
template's newest version has not reached the pipeline yet. The sub-code
stays in the log reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant