Skip to content

feat(workflows): the domain wizard prompts for send once verified - #520

Closed
Silthus wants to merge 57 commits into
feat/email-domain-wizard-suggestionsfrom
feat/send-once-verified-wizard
Closed

Silthus wants to merge 57 commits into
feat/email-domain-wizard-suggestionsfrom
feat/send-once-verified-wizard

Conversation

@Silthus

@Silthus Silthus commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Problem

  • A person who sets up a sending domain in the wizard waits for DNS on the verifying step, and the step gives them nothing else to do.
  • A workflow that waits for that domain (waiting_for_domain, from send once verified) stays waiting when the wizard verifies the domain. The wizard's verify call and its status poll mark the senders verified without taking waiting workflows live. The background sweep checks only unverified senders, so it never picks the workflow up again.
  • In the workflow editor, the core's call to action for a pending domain opens the Channels list. That list does not poll, so a person cannot watch the domain verify there.
  • On the wait for DNS, eight DNS records fill the page before the person reaches the next step (feedback on the preview).
  • The editor's domain links open the wizard in a new tab, so after verification the workflow sits in a tab the person left.

Stacked on #521 (feat/email-domain-wizard-suggestions), the wizard's workflow suggestions, which sits on the email domain wizard stack tip #341. It also merges the send once verified core, #519 (branch spec/send-once-verified), last at 4d367f40265, which refreshes a waiting workflow's status while its page is open. Landing order: the wizard stack, then #521, then PostHog#113091, then #519, then this PR. Once those land, this PR rebases to its own commits.

Impact

Pays into the Workflows activation rate. A person who is waiting for DNS can turn a workflow on from the wizard, and it starts sending when the domain verifies, without a return visit.

Warning

🟡 Merge risk: medium · two-way door (merge-risk rubric)

  • Every email domain verification now goes through one writer: the verify button, the wizard's status poll, Domain Connect and the background sweep. A defect there would stop senders from verifying. That path is not behind the flag.
  • The editor's domain links now open the wizard in a modal over the workflow. Closing it reloads the workflow when the server changed it, through the existing external edit path. A sender created there fills the workflow's empty senders and saves at once. Behind the flag.
  • Every workflow save now carries its own edit version, in place of one shared value. This touches every editor save, flag or not.
  • Limits: the writer keeps the sweep's locking, sender matching and default-sender rules, and the verification suites pass on HEAD. The wizard card and the wizard itself stay behind workflows-email-domain-wizard. A revert restores the two writers.

Changes

Kind Files Lines
Production 8 +107 −79
Tests 2 +34 −3

Counted over this PR's own commits (534a795997b..e45577b8b82). 7e5c7a089a2 adds 1 production file (+4 −2) and 1 test file. The waiting page and the modal over the workflow add 6194282515d..928cc853cce. The sender fill adds 928cc853cce..07c48926720. The merge commits are described below; the latest are 00601129b16 and e21d433d578.

  • The DNS records fold into one line while DNS is checked, the same summary as the last step: "4 of 8 settings in place at Cloudflare", with a spinner while checks run. Expanding it shows each record's live status and copy buttons. It opens by itself when checks stop with records missing.

  • The "While you wait" card offers the last step's first workflow choices from feat(workflows): offer data suggestions on the domain wizard's last step #521, under its message that workflows turned on now start sending once the domain is verified:

    • Workflows suggested from the project's data, which start loading once the domain is picked. Until they load, or with suggestions off, the welcome email template.
    • Always a blank workflow, and "Go to workflows".
    • Its buttons report email-domain-verifying-* data-attrs, and its block email-domain-verifying-workflow-choices, so clicks on this page stay apart from the last step's.
    • The card is absent when verification stopped with a failure.
    Suggestions on, records folded (1600px) Suggestions off (1100px) Records expanded (1100px)
    The wait for DNS with the suggestions The wait for DNS with the welcome template The records expanded
  • The editor's domain links open the wizard in a modal over the workflow, not a new tab, with the wizard's existing modal mode:

    Stage The link opens
    No domain configured (a step has no sender, and the project has no email sender) The modal at the wizard's domain step
    One pending domain, as a draft, while waiting, or the project's only domain The modal at that domain's status, where the DNS records poll
    Several pending domains The Channels list, in a new tab
  • Opened from a workflow, the wizard offers the workflow's own next step in place of the suggestions. The modal says "For the workflow …".

    State The card says Actions
    DNS pending "It starts sending once mail.acme.com is verified." Enable and send once verified, Back to workflow
    Domain verified "mail.acme.com is verified. Turn the workflow on to start sending." Enable workflow, Back to workflow
    Already live or waiting "It's on and starts sending once mail.acme.com is verified." or "It's on." Back to workflow
    Cannot be enabled The reason: no sender in its email step, step errors, unsaved changes, or no editor access Back to workflow
    • Enable saves the active status through the existing enable path, so a workflow on a pending domain waits for it. It skips the Enable confirmation, because the button says what happens, and closes the modal.
    • Back to workflow only closes. On close, the integrations reload, and the workflow's server copy goes through the editor's edit events: when the server changed it meanwhile (a verified domain takes it live), the editor reconciles as it does for an edit from another tab.
    • The edit events' save queue now keeps the newest held event, since the close check can deliver an older copy than an edit already held.
    • Opened from Channels, onboarding or broadcasts, the wizard keeps the suggestions.
    Waiting stage (1600px) Waiting stage, the workflow's card Verified stage, the workflow's card
    The modal over the workflow while DNS is checked Enable and send once verified Enable workflow
    The waiting stage in an 1100px window

    The modal in a narrow window

  • A sender created in the modal fills the workflow's empty sender. When the workflow's email step has no sender, the setup sets the sender it creates on every email step without one. A step that already has a sender keeps it.

    • The fill goes through the editor's state, with any unsaved edits, and saves at once through the auto-save path and its save queue. The close-time check then reads that save as this editor's own. With auto-save off, the fill stays an unsaved change, and the card asks to save the workflow first.
    • The workflow's card then says "Your workflow now sends from hello@mail.acme.com." and offers "Enable and send once verified", so a fresh workflow turns on in one step.
    • The fill sends email sender preselected with surface: 'workflow' and reason: 'created_in_domain_setup', the event the editor already sends when it preselects a sender.

    A fresh workflow in a project with no email sender, after Set up a domain and Continue, at the waiting stage:

    Before (1280px) After (1280px)
    Before: the card says to choose a sender and offers only Back to workflow After: the card names the new sender and offers Enable and send once verified
    The same at 640px, and the entry point
    Before (640px) After (640px) The status bar that opens it
    Before, narrow After, narrow No domain configured. Set up a domain

    After Enable and send once verified, the modal closes over the workflow, which now waits for its domain:

    Waiting for domain. Sending starts once every email domain is verified.

Earlier screens: the While you wait card at e45577b8b82, and the links as new tabs at e8accd5a61a
  • The verifying step shows a "While you wait" card on the wizard page while DNS is pending. It says the person can turn on workflows that send from the domain now, and that they start sending once the domain is verified.

    • "Create the welcome email workflow" opens the Welcome email sequence template. The ready step's next-workflow card now shares this action.
    • "Go to workflows" opens the workflows list.
    • The card is absent when verification stopped with a failure, because the step then asks the person to fix their records.
    • The card is absent in the setup modal. The guided onboarding and the broadcast flow open that modal, and the card would take the person out of them.
  • The editor's domain links open the wizard. The core's status bar notice gives each sender domain stage one link. On this stack:

    Stage Link opens
    No domain configured (a step has no sender, and the project has no email sender) The wizard's first step, /workflows/channels/email/new
    One pending domain: a chosen sender as a draft or while waiting, or a step without a sender in a project with only that pending domain That domain's status page, /workflows/channels/email/<id>, where the DNS records poll
    Several pending domains The Channels list, which lists each domain with its status

    Every stage in the editor status bar, 1600px

    Set up a domain opens Check domain status opens
    The wizard's domain step The domain's status page, checking DNS
    The same stages in a 1100px window

    Every stage in a narrow window

  • A domain verified in the wizard takes its waiting workflows live. EmailIntegration._mark_domain_verified now calls the workflows facade's enable_waiting_workflows inside its transaction, as ADR 0002 requires of every writer of verified = true.
  • The sweep writes through the same function. apply_verification now calls _mark_domain_verified, and the core's second writer is deleted. The one writer locks the senders on the same domain, provider and MAIL FROM subdomain, sets the default sender, and schedules the go-live task.
  • A go-live that cannot be queued no longer fails the verification. The task is queued after the verification commits. A broker error now logs workflows.enable_waiting.failed and counts workflows_enable_waiting_failed_total, the signal a go-live already sends after its last retry. Before, the error failed email/verify, the status poll and the sweep for a domain that was already verified. The core made the same fix, so the merge keeps one queue_enable_waiting_workflows.
Earlier screens: the While you wait card, before and after, at e45577b8b82
Before (520px) After (520px)
Before: the verifying step ends with the DNS records After: a While you wait card below the DNS records
Before (1440px) After (1440px)
Before: the verifying step ends with the DNS records, wide After: the card below the DNS records, wide

The merge commits

00601129b16 brings in #521 at 907d522a6de: the last step says "Your first sender" only for the project's first verified sender, and a new sender's name starts as the organization's name. It had one conflict, in ReadyStep.tsx, where #521 renamed FirstSenderCard to SenderCard and this PR had moved the records into DnsRecordsSummary. The merge keeps this PR's step with #521's SenderCard.

e21d433d578 brings in #519 at 4d367f40265: while a workflow waits for its domain and its tab is visible, the editor checks its status every 10 seconds and shows it going live without a reload, keeping unsaved edits. It had two conflicts, in workflowLogic.ts and workflowLogic.sendOnceVerified.test.ts, where both sides added state, listeners and tests next to each other. The merge keeps both:

ae8a6120f56 brings in #521 at bcee93148d3, which this PR now sits on. It had one conflict, in NextWorkflowCard.tsx: the merge takes #521's FirstWorkflowChoices. a549f43816e brings in #521's fix for a long domain in the ready heading, cleanly.

bb3934f5647 brings in the core's verified sender rule, up to ac56c64e64d: a step without a sender gets no notice when the project has a verified sender. It had two conflicts, in senderDomainNotice.ts and its test. The merge keeps the core's rule and this PR's wizard URLs, and the pending notice for a project's only pending domain links to that domain's page.

b55373ebed9 and 9ad67d0d477 bring in the core's move of the list's enable path to the generated client, up to b8a7c3c10fc. Both merged cleanly.

4a2d7f61e00 and e8accd5a61a bring in the core's call to action polish, up to b233ec8ba71. The first merged cleanly. The second had one conflict in senderDomainNotice.ts: the merge keeps the core's new signature, which reads the existing senders, and this PR's wizard URL for "No domain configured".

6ee3d98019b brings in the core's review fixes, up to its reviewed head 6a5872e04d9. It has two conflicts:

  • email.py: the core's second writer stays deleted. Its last change locked senders ordered by id, which the one writer already does.
  • test_email_sender_verification.py: both sides parametrized the queue-failure tests. The merge keeps this PR's three write paths and the core's database and redis errors.

The broker fix that landed on both sides auto-merged to one implementation. No wizard code calls the names the core renamed.

534a795997b, the first merge: the wizard stack does not carry the sender verification refresh that the core builds on, so both sides rewrote the SES verify path and the verified write. The merge keeps:

  • The core's split of verify_email_domain and get_email_domain_verification, so the sweep reuses one batched SES read. The records come from the wizard's build_email_domain_records.
  • The core's verification_requested_at stamp when verify() does not succeed. The two Domain Connect chaos tests now compare the sender's config without that stamp, because each apply-url call asks SES to verify again.
  • The core's sender matching on the MAIL FROM subdomain, in place of the wizard's "every sender on the domain".

How did you test this code?

On head e21d433d578, after the #521 and #519 merges:

  • Test seam, the workflows kea logic (workflowLogic.sendOnceVerified.test.ts, "while the workflow waits for its domain"): a status check in flight while the domain setup closes and reloads the workflow. The server turned the workflow off in between, and the check's older answer says active. The editor keeps the reloaded copy. With the staleness guard removed, the test fails.
  • Jest: all 108 workflows frontend suites, 1509 tests, passed. tsgo: no errors in the workflows code. The two errors left are outside this PR: AgentPromptButton.tsx and QuillTicketsList.tsx.
  • Live run on the preview box, deployed at e21d433d578, in Chromium through the app's URL, with the demo DNS and fake email backend:

On head 07c48926720, after the sender fill:

  • Test seam 1, the editor to the modal (WorkflowSenderDomainSetupModal.test.tsx): a fresh workflow in a project with no sender. It clicks Set up a domain, types the domain and continues. The fill save carries the new sender on the empty step and no status. At the waiting stage the card shows the sender line and "Enable and send once verified". The enable saves active. The test failed before the fill: nothing was saved.
  • Test seam 2, the workflows kea logic (workflowLogic.sendOnceVerified.test.ts, "a sender created in the domain setup"): only empty steps fill, a chosen sender and a sender rotation stay, and an unsaved rename survives the save. A fill queued behind an in-flight save keeps the sender when its own save fails. That case failed before 07c48926720.
  • Jest: the Workflows, Broadcasts and Channels suites, 92 suites and 1326 tests, passed. tsgo: no errors in the workflows code. The two errors left are outside this PR: AgentPromptButton.tsx and QuillTicketsList.tsx.
  • Live run: a scratch Storybook story of the status bar and the modal over a fresh workflow, on the fake email backend, driven by Playwright. Before and after renders at 1280px and 640px are above. The enable sent one workflow PATCH with status: active, and the status bar then read "Waiting for domain".
  • Not run: the fill against the live stack.

On head 928cc853cce, after the waiting page and modal work:

  • The wizard page (EmailDomainScene.test.tsx): the records fold while DNS is checked and expand on click; the suggestions show on the waiting page under the email-domain-verifying-* data-attrs, and the shown event fires once per page.
  • The editor to the wizard (WorkflowSenderDomainSetupModal.test.tsx, new): clicks the status bar's Check domain status and follows the modal. Enable saves active and closes; Back only closes; the verified stage offers Enable workflow; a missing sender, a viewer and an already waiting workflow get no Enable.
  • Workflows kea logic (workflowLogic.sendOnceVerified.test.ts): the notice carries the domain's sender id; closing reloads the integrations; a server change while open reloads behind the syncing overlay, asks first over edits auto-save cannot flush, and does nothing when the server copy is unchanged; this editor's own save is never read as a server change, whether the check returns before or after the save response.
  • The save queue (resourceSaveQueue.test.ts): an older edit event held during a save no longer replaces a newer one.
  • Jest: the Workflows, Broadcasts and Channels suites, 92 suites and 1321 tests, passed. tsgo: no errors.
  • Renders: committed stories Verifying with suggestions and a scratch story of the whole app with the modal over a workflow, at 1600px and an 1100px window. The full set is in proof storage.
  • Not run: the modal against the live stack. The renders and tests use the fake email backend.

On head bb3934f5647, after the verified sender rule merge:

  • Jest: 12 suites and 198 tests passed (Workflows/workflowLogic*, workflowsLogic, Channels/EmailDomain). The stage cases assert each link, including the project with only a pending domain (/workflows/channels/email/7).
  • tsgo: no errors.
  • The GPT-6.1 Sol review round and the final read of the merge found nothing. fleet proof gate passes.

On head 9ad67d0d477, after the generated client merges:

  • Jest: the workflowsLogic and workflowLogic.sendOnceVerified suites pass, including the core's new current-environment case.
  • tsgo: no errors.
  • Semgrep devex rules against master: no new findings. The pre-push preflight's semgrep step timed out on this branch, so the check ran on its own.
  • The final reads (GPT-6.1 Sol) of the merges found one item on the core, fixed in b8a7c3c10fc, then nothing. fleet proof gate passes.

On head e8accd5a61a, after the call to action merges:

  • Test seam 3, the workflows kea logic. workflowLogic.sendOnceVerified.test.ts asserts the link of each stage: the wizard start for no sender and for a deleted sender, the domain's page for one pending domain, the Channels list for two. The three wizard URL cases failed before 7e5c7a089a2.
  • Jest: 12 suites and 195 tests passed (Workflows/workflowLogic*, workflowsLogic, Channels/EmailDomain).
  • tsgo: no errors. The two errors listed below came from a stale local build of @posthog/quill and are gone after a rebuild.
  • The renders come from a scratch Storybook story of the workflow scene with a mocked API, and from the Email domain setup stories.
  • The GPT-6.1 Sol review and final read covered the link change and the merge resolution. fleet proof gate passes.
  • Not run: a click-through from the editor to the wizard page on the live stack. The links open in a new tab.

On head 6ee3d98019b, after the second merge:

  • pytest: 2404 passed (test_email.py, test_integration.py, test_domain_connect.py, products/workflows/backend).
  • Jest: 70 suites and 1108 tests passed (Channels/EmailDomain, Workflows).
  • tsgo: no errors in files this PR touches. The errors outside it are the two known ones below, plus @posthog/quill module resolution in products/conversations from the local install.
  • ruff and hogli product:lint workflows pass. The strict pre-push preflight had 0 failures.
  • The final read on the merge (GPT-6.1 Sol) found nothing. fleet proof gate passes.

On head e45577b8b824cb862087dafca8270bb561ac746b:

  • Test seam 2, the sender verification write. test_verifying_a_sender_takes_its_waiting_workflows_live gains two cases, wizard_verify and wizard_status_poll, beside the sweep cases. Each case runs the on-commit callbacks and the Celery task, then reads the workflow over the API. Both cases failed on the merge commit (the workflow stayed waiting_for_domain) and pass now.
  • A go-live that cannot be queued. test_a_go_live_that_cannot_be_queued_keeps_the_verification makes the broker refuse the task for the sweep and both wizard writes. It asserts that the sender stays verified and that the failure logs and counts. It raised on 8d33d7bf5a4 and passes now.
  • The card render. EmailDomainScene.test.tsx checks that the welcome action shows while the step checks DNS and is absent after a stopped verification. The first check failed without the card.
  • Live run. An API-level run through the Django test client, with the maildev provider and Celery eager, not a running dev server. Enable returned waiting_for_domain. POST email/verify then returned success, and GET email/status?refresh=true returned verified. On both paths the workflow then read active with went_live_at set.
  • The verification, integration, Domain Connect and workflows backend suites pass on HEAD. The EmailDomain Jest suites pass.
  • The frontend typecheck reports 2 errors, both outside this PR: AgentPromptButton.tsx and QuillTicketsList.tsx.
  • Not run: repo-wide mypy, which timed out locally. The pre-commit ty check and the pre-push type check pass.
  • The renders come from the Email domain setup Storybook stories. The page has no horizontal overflow at 520px or at 1440px.

Test rationale: The seam 2 cases catch a verify or status path that writes verified = true without the facade call, which the sweep cases cannot catch. The queue-failure case catches a broker error that fails a request after the verification committed. The scene check catches the card disappearing from the verifying step, or showing on a stopped verification. No logic test covers the card, because it has no state.

Proofs per the QA swarm's proof rules:

  • Live run: saved on e45577b, see above. It still holds, because the second merge changes no wizard path. The core ran its own live run at 6a5872e04d9.
  • Prod shape: a gap until production read access on 2026-11-02. The persisted input is the stored email sender config, whose MAIL FROM subdomain decides which senders the one writer marks verified.
  • Not checked: real SES and the Domain Connect apply flow against a real DNS host. They go to the post-deploy watch.

Release status

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

The card and the wizard sit behind workflows-email-domain-wizard. The one-writer change in EmailIntegration is not flag-gated. It changes no behavior for senders without waiting workflows.

Docs update

None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code in T3 Code, Claude Opus 5.5 (high). Design pass by GPT-6.1 Sol (high).

  • Skills: tdd, writing-tests, writing-ui-components, writing-user-facing-copy, writing-pr-descriptions, pr-description, merge-risk, adopting-generated-api-types, the QA swarm design pass.
  • The design pass found one item: the merged verify path converted the sweep's SES status into the wizard's polling state just to read it. Fixed in 8d33d7bf5a4.
  • The GPT-6.1 Sol review found two items, both fixed in e45577b8b82: a broker error after commit failed verified requests (major), and the card did not use LemonCard (minor). The final read found nothing.
  • The final read on the second merge (GPT-6.1 Sol) checked the conflict resolutions and the interaction between the two sides. It found nothing.
  • The call to action work: Claude Opus 5.5 (high) wrote the link change and the merges. The GPT-6.1 Sol review found nothing in the link change; its findings on the core are fixed or rejected on feat(workflows): turn a workflow on before its email domain verifies #519. The final read found nothing.
  • The generated client merges: GPT-6.1 Sol's final read found the core's calls routed by the project id, not the current environment id. The fix landed on feat(workflows): turn a workflow on before its email domain verifies #519 and merged here. The final read of the fix found nothing.
  • The waiting page and modal work: Claude Opus 5.5 (high). The design pass (GPT-6.1 Sol) found a workflow object threaded through the modal and no test of the editor-to-modal path; both fixed. The review round found a stale state after close and Enable offered to a viewer; both fixed. Five final reads found the refresh after close racing the save fence, the form and in-flight saves; fixed by sending the server copy through the editor's edit events, and by keeping the newest held event in the save queue. The sixth read found nothing.
  • The sender fill: Claude Opus 5.5 (high). The design pass (GPT-6.1 Sol) proposed a toast in place of the card's line, which was rejected because a toast is gone before the waiting stage; its flag moved onto the open setup's state. It also found a test mock that returned the stale workflow, which was fixed. The review round found a save queued behind another resetting the form without the fill (major), and a sender rotation read as empty (minor). Both were fixed with tests. The final read found nothing.
  • The feat(workflows): offer data suggestions on the domain wizard's last step #521 and feat(workflows): turn a workflow on before its email domain verifies #519 merges: Claude Opus 5.5 (high) resolved the conflicts. The final read (GPT-6.1 Sol) checked the resolutions and how the status check meets the modal close, the sender fill and the save queue. It found nothing.
  • Follow-up: the five verifying stories in Email domain setup gain the card, so their visual review baselines need approval.

Decisions

Question Answer Who Where
Where the editor's domain links go on the wizard stack One pending domain opens its wizard status page, where verification shows live 👤 Human pending approval
Production data shape Gap until production read access on 2026-11-02 👤 Human standing instruction
Waiting page content Fold the DNS records and show #521's first workflow choices 👤 Human pending approval
Domain setup from a workflow A modal over the workflow, with the workflow's own Enable and Back in place of the suggestions 👤 Human pending approval
A workflow's empty sender when its domain setup creates one The created sender fills every email step without a sender; a chosen sender stays 👤 Human pending approval
How many writers of verified = true One: the sweep's apply_verification goes through the wizard's _mark_domain_verified; alternative was a facade call in each 🤖 Agent decision lines
Which senders a verification marks Same domain, provider and MAIL FROM subdomain, the core's rule, in place of every sender on the domain 🤖 Agent decision lines
Where the card shows Only on the wizard page, not in the setup modal that the guided onboarding and broadcasts open 🤖 Agent decision lines
Card on a stopped verification Hidden, so the step keeps asking the person to fix their records 🤖 Agent decision lines
verification_requested_at on Domain Connect apply-url Kept: each call asks SES to verify; the chaos tests compare config without it 🤖 Agent decision lines
Live run shape API through the test client with maildev, not a running dev server 🤖 Agent decision lines
A go-live that cannot be queued Log and count it, do not fail the verification; a reconciler stays the spec's follow-up 🤖 Agent decision lines
Which writer of verified = true survives the second merge The wizard's _mark_domain_verified; the core's second writer stays deleted, and its id-ordered lock is already in the one writer 🤖 Agent decision lines
The broker fix that landed on both sides One queue_enable_waiting_workflows, with the core's RedisError retry 🤖 Agent decision lines
Live run on the second merge Reused from e45577b; the merge changes no wizard path 🤖 Agent decision lines
No sender, and the project only has pending domains The pending notice links to that domain's page when there is one domain, else the Channels list 🤖 Agent pending approval
Labels in the modal "Enable and send once verified", "Enable workflow", "Back to workflow" (replaced the working labels "Enable workflow and send when verified" and "Close and enable workflow") 🤖 Agent pending approval
Enable from the modal Saves active through the existing path and skips the Enable confirmation 🤖 Agent pending approval
Tracking the waiting page email-domain-verifying-* data-attrs; the suggestions' shown event fires once per page with the same surface 🤖 Agent pending approval
After the modal closes Reload integrations, and send the server copy through the editor's edit events, so a server change reconciles like an edit from another tab 🤖 Agent pending approval
Several pending domains The Channels list in a new tab, as before 🤖 Agent pending approval
Links for several pending domains, and for no domain Several open the Channels list; no domain opens the wizard's domain step, now in the modal 🤖 Agent pending approval
Where the fill is announced One line in the workflow's card: "Your workflow now sends from ."; a toast was rejected, because it is gone before the waiting stage 🤖 Agent pending approval
When the fill saves At once, through the auto-save path and its save queue; with auto-save off it stays an unsaved change 🤖 Agent pending approval
Tracking the fill email sender preselected with reason: 'created_in_domain_setup', the event the editor sends for a preselected sender 🤖 Agent pending approval
A step with a sender rotation Counts as a chosen sender, so the fill leaves it alone 🤖 Agent pending approval
The status check of #519 next to the modal's close Keep both; an answer that lands after the close reloaded the workflow is dropped 🤖 Agent pending approval
The last step after #521's sender card This PR's folded records with #521's SenderCard 🤖 Agent pending approval

Involvement: no grilling rounds, no prototypes; design passes, review rounds and a final read after every fix (GPT-6.1 Sol), six final reads for the waiting page and modal; for the sender fill, a design pass, one review round and a final read; a final read of the #521 and #519 merges; renders and live runs captured.

🤖 Generated with Claude Code

Silthus and others added 26 commits October 19, 2026 09:46
Background sweeps check pending SES email senders and save the verified
flag, so Channels and MCP reads see a verified sender without the user
opening the verification UI. Senders whose verification was requested in
the last hour are checked every minute, older ones up to 7 days every 5
minutes. A resolved SES VDM advisor finding marks the pending senders on
its domain as requested again.

SES status reads are batched per 100 senders and paced in background
providers. Matching senders are row-locked while marked verified, and
the first verified sender becomes the project's default when none exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enabling a workflow whose email sender's domain is not verified yet now
saves `waiting_for_domain` instead of `active`, behind
`workflows-email-domain-wizard`. A waiting workflow does not trigger.

- One enable rule in the HogFlow writes module (`enable`, `disable`),
  used by API create and PATCH with `status: active` and by
  `set_workflow_enabled`. It returns the usage events and the view sends
  them after commit.
- `HogFlow` stores `enable_requested_at`, `enable_requested_by` and
  `went_live_at`; the API returns the two timestamps read-only.
- Disable, archive and delete of a waiting workflow report how long it
  waited.
- The workflows list and header show a "Waiting for domain" badge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the verify button or the background sweep verifies an email sender,
every workflow waiting for that sender's domain goes `active` within
seconds, as long as none of its other senders is still unverified.

- `enable_waiting` in the HogFlow writes module locks each waiting row,
  skips it unless it still waits, and saves `active` with `went_live_at`.
  It returns `hog_flow_activated` with `activated_by: domain_verified`.
  It ignores the flag, so turning the flag off never strands a workflow.
- The verification write calls the facade's `enable_waiting_workflows`
  inside its transaction. The facade schedules a task after commit that
  retries database errors 5 times with backoff, then logs an error and
  counts `workflows_enable_waiting_failed_total`. The task sends its
  events with `ph_scoped_capture`, credited to the person who enabled.
- Edits to a waiting workflow validate as strictly as an activation and
  save in place. An edit that leaves no unverified sender takes it live.
- Deleting an email sender that a waiting workflow uses is refused.

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

The workflow detail and every single-workflow write response now carry
the workflow's unverified email senders (id and domain). The list
endpoint leaves them out.

With workflows-email-domain-wizard on, the header Enable and the list
toggle ask the person to confirm when a sender's domain is not verified
yet, and name the domains. Confirm sends the usual status: active, so
the server saves waiting_for_domain. Cancel saves nothing. The list
toggle loads the workflow's detail to read the senders.

A workflow that went live after waiting shows a one-time "Started
sending" notice. The dismissal is stored in the browser, keyed by
workflow id and went_live_at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wizard stack does not carry the sender verification refresh the core
builds on, so both sides rewrote the SES verify path and the verified write:

- ses.py keeps the core's split into verify_email_domain and
  get_email_domain_verification (the sweep reuses one batched identity read),
  and builds its records with the wizard's build_email_domain_records.
- email.py keeps the core's apply_verification for the sweep and the wizard's
  verify(on_domain_verified)/status(). verify() still stamps
  verification_requested_at when it does not succeed, and the wizard's
  _mark_domain_verified matches senders the way the core does (same domain,
  provider and MAIL FROM subdomain).
- Both sides add FEATURE_FLAGS.WORKFLOWS_EMAIL_DOMAIN_WIZARD with the same line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rotation sends from integrationIds when the list is set, so the sender
check reads the same senders Node picks from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enable on the API, MCP and create paths share one step, and the
go-live check after an edit runs only when no status is requested.
Transactional email workflows skip the sender check inside that step.

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

The wizard's verify() and status poll write through _mark_domain_verified,
which did not call enable_waiting_workflows, so a workflow waiting on the
domain stayed waiting until the background sweep came by. The sweep's
apply_verification now writes through the same function, so there is one
writer of verified = true (ADR 0002) and it calls the facade inside its
transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…DNS is pending

The wizard page's verifying step now shows a card: workflows that send
from the domain can be turned on now and start sending once it is
verified, with "Create the welcome email workflow" and "Go to workflows".
The welcome action is shared with the ready step's next-workflow card.

The card stays off the setup modal, so the guided onboarding and the
broadcast flow that open it are unchanged, and off a stopped verification.

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

The merge converted SESIdentityStatus into the polling path's
_SESIdentityState only to read its statuses. The verify path now reads
SESIdentityStatus directly, so the conversion goes away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
enable_waiting_workflows queues its task in an on-commit callback. When
the broker refused the task, the error escaped the callback after the
verification had committed, so email/verify, the wizard's status poll
and the sweep failed for a domain that was already verified. The queue
call now logs workflows.enable_waiting.failed and counts
workflows_enable_waiting_failed_total, the same signal as a go-live
that fails after its last retry.

The wizard's send once verified card also uses LemonCard instead of a
hand-built section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Enable locks the sender rows it reads, so a verification that commits
  meanwhile waits for the waiting status and then finds the workflow.
- Graph, email-step and publish edits run the same go-live check as a
  full save, and report its activation event.
- A malformed email step reads as having no sender instead of failing
  the detail read.
- The go-live task retries a failed worker reload like a database error,
  and a waited go-live sweeps parked timing steps like a re-enable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d and search

A waiting workflow goes live by itself, so a realtime cohort it reads
must not change under it, and search must still find it.

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

- The editor status bar shows Waiting for domain instead of Draft.
- The list status filter has a Waiting for domain option.
- The header Enable prompt stays open and loading until the save settles.
- An enable that waits no longer reports workflow enabled on a later save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
enable_waiting_workflows queued its task in an on-commit callback. When
the broker refused the task, the error escaped the callback after the
verification had committed, so email/verify and the sweep failed for a
domain that was already verified. The queue call now logs
workflows.enable_waiting.failed and counts
workflows_enable_waiting_failed_total, the same signal as a go-live that
fails after its last retry. Same fix as the wizard branch.

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

- Enable and verification lock sender rows ordered by id, so they never
  deadlock on a rotation.
- A waited go-live sweeps timing steps after the whole batch, so a failed
  enqueue cannot stop the other workflows or drop their events.
- The enable prompt cannot be cancelled while its save runs.
- The cohort modal and the status bar copy say enabled and every domain.

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

The workflows are already live when their timing sweeps are queued, so a
failed enqueue is logged and the activation events still reach the
sender.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges spec/send-once-verified at 6a5872e into the wizard prompt branch.

- posthog/models/integration/email.py: keeps one writer of verified = true,
  the wizard's _mark_domain_verified. It already locks senders ordered by
  id, which is the core's b4618a8 change to the deleted
  _mark_matching_senders_verified.
- test_email_sender_verification.py: keeps the wizard's write parametrization
  of the queue-failure test and the core's database/redis parametrization of
  the task last-retry test.
- The go-live queueing fix landed on both sides with the same shape and
  auto-merged to one queue_enable_waiting_workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status bar names the sender domain state of the workflow and links to
where the domain's verification shows: no domain configured, a domain that
is not verified yet, and a workflow waiting for its domain. The header no
longer repeats the waiting tag.

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

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

No domain configured opens the email domain wizard. One pending domain opens
its status page, where the DNS records' verification polls live. Several
pending domains keep the channels list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An email step whose senders no longer exist has no domain to send from, so
the status bar now says so once the integrations load. With the flag off, a
waiting workflow keeps its sending-starts explanation.

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

# Conflicts:
#	products/workflows/frontend/Workflows/senderDomainNotice.ts
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus and others added 5 commits October 20, 2026 07:38
…less it has edits

A field-by-field refresh kept the old save fence and the form's status, so the
next save could conflict and a manual trigger stayed disabled. A saved
workflow without unsaved changes now reloads in full. One with unsaved edits
keeps them and refreshes on its next save or load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ain setup was open

Closing the setup checks whether the server saved the workflow meanwhile, as
it does when a verified domain takes the workflow live. If so, the editor
reconciles the way it does for an edit from another tab: a silent reload behind
the syncing overlay while auto-save can flush, else the Reload or Keep mine
banner. Enabling from the setup saves without that check.

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

The check after the setup closes reads the save baseline once its request
returns, and reconciles only a newer server copy, so a save of this editor that
landed meanwhile is not mistaken for a change from the server.

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

The check after the setup closes hands the server's copy to the same path as an
edit from another tab. Its save queue holds the copy while a save of this editor
runs and ignores the echo of that save, so a save that commits before its
response lands is not mistaken for a change from the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit events can arrive out of order, so a held event is replaced only by one
that is not older.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus changed the base branch from feat/email-domain-wizard to feat/email-domain-wizard-suggestions October 20, 2026 08:06
Silthus and others added 6 commits October 20, 2026 10:25
…main setup

When the email domain setup opened from a workflow creates a sender, set
it on every email step of the workflow that has no sender, and save at
once so the setup can offer to enable the workflow. Steps that already
have a sender keep it. The setup says which address the workflow now
sends from, and the fill is tracked as a preselected sender.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fill notice belongs to the setup that made it, so opening or closing
the setup resets it without its own reducer. The logic test now checks
that the filled senders and the unsaved rename survive the save.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A save queued behind another one overwrote the shared edit version, so
the first save's response treated the later edits as saved and reset the
form without them. The sender fill from the domain setup queues behind an
in-flight auto-save this way. Each save now carries the edit version it
was dispatched with. A step that sends through a sender rotation also
counts as having a sender, so the fill leaves it alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus added a commit that referenced this pull request Oct 20, 2026
Silthus and others added 4 commits October 20, 2026 11:05
…ggestions

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The records step and the waiting page get a quiet menu that copies a link
to the domain page for a teammate, or the records as plain text for a DNS
owner outside PostHog. Each copy captures 'email domain records shared'
with the method and step. The ready step shows no hand-off.

Refs Silthus/personal#190

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Domain Connect host the records sit behind 'I'll add them myself',
so a teammate opening the shared link saw the step without them. The
link now carries records=shown, which reveals them without recording a
setup method.

Refs Silthus/personal#190

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs Silthus/personal#190

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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