Skip to content

feat(workflows): show draft and sending banners on the first-run workflow - #112076

Closed
Silthus wants to merge 10 commits into
PostHog:masterfrom
Silthus:feat/workflows-first-run-banners
Closed

Silthus wants to merge 10 commits into
PostHog:masterfrom
Silthus:feat/workflows-first-run-banners

Conversation

@Silthus

@Silthus Silthus commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #112053.

Problem

  • A new Workflows user who opens the first-run workflow as a draft gets no hint that nothing sends until they press Enable.
  • Once it sends, nothing points them to the Metrics tab, where they can see what it sent.
  • This is the "Draft and sending banners" slice of the Workflows first run (spec, ticket), behind workflows-first-run.

Note

🔵 Merge risk: low (merge-risk rubric)

  • The banner mounts in the workflow scene for every workflow, so a crash in it would break the workflow page.
  • It renders nothing unless workflows-first-run is on and the workflow is the first-run one; it only reads data and writes browser storage. Revert is clean.

Changes

Kind Files Lines
Production 3 +205 −0
Tests 2 +243 −0
Total 5 +448 −0
  • A draft first-run workflow shows "This workflow is not sending yet." above the tabs, and the Enable button pulses once through the existing setup highlight.
  • An enabled first-run workflow shows "Your workflow is sending." with a "View metrics" button that opens the Metrics tab.
  • Pressing Enable in the header turns the draft banner into the sending banner without a reload, because the banner reads the saved workflow from workflowLogic.
  • Both banners say what the workflow does from its own steps: the trigger event and how many emails each person gets. The header already shows the workflow description, so the banner does not repeat it.
  • The banner closes, and the browser remembers that per workflow. A closed draft banner stays closed after Enable, so no sending banner appears for that workflow either.
  • Any other workflow, or the flag off, shows nothing. The first-run workflow is the id that Make it yours stored in the browser.
  • Mechanical: one mount line (plus its import) after the email pause banner in WorkflowScene.tsx. Everything else is new files under products/workflows/frontend/firstRun/.
  • Not in this PR: "Enable engagement events" (Workflows first run: capture engagement events by default Silthus/posthog#263) and who the first sends reach (Workflows first run: say who the first sends reach, and the own-domain line Silthus/posthog#264). The banner component is the place they extend.
State Full width 520px
Draft, Enable highlighted
Sending

"View metrics" in the Sending story, clicked in a headless browser, opens the Metrics tab:

New surface, so there is no before.

How did you test this code?

Test seams: a kea logic test, firstRunWorkflowBannerLogic.test.ts, mounts the real workflowLogic with the hog flow API mocked at the HTTP layer. Stories in FirstRunWorkflowBanner.stories.tsx cover both states at full width and at 520px.

Test rationale: no existing test covers this banner. The logic test catches:

  • the wrong banner for a status, or a banner on any workflow other than the first-run one, or with the flag off;
  • the banner not flipping after the header's enable save (driven through saveWorkflowPartial, as the header does);
  • the Enable highlight missing on a draft, or firing on a sending workflow;
  • a dismissal that comes back after enabling, or is not persisted across a remount;
  • "View metrics" not routing to the Metrics tab.

Evidence:

Red: the new test on a stub logic that never shows a banner
✕ shows the draft banner on the first-run draft and highlights Enable
✕ turns into the sending banner once the header enables the workflow
✕ does not highlight Enable on a sending workflow
✓ shows nothing on another workflow
✓ shows nothing on the first-run workflow with the flag off
✓ keeps a dismissed banner dismissed when the workflow opens again
✕ shows the sending banner after the draft banner was dismissed
✕ opens the Metrics tab from the sending banner
Tests: 5 failed, 3 passed, 8 total

The dismissal test passed on the stub because it did not yet assert the draft banner first. It now does, and removing persist from the dismissal reducer makes it fail.

  • Green: all 7 tests pass, and the first-run suites plus workflowSceneLogic.test.ts pass (7 suites, 44 tests).
  • Review fix: the rewritten dismissal test failed on the earlier per-state dismissal, then passed after the fix.
  • Gate on the final tree: typescript:check clean, oxlint --quiet and oxfmt --check clean on the touched files, pre-push ci:preflight --strict clean.
  • Browser: Storybook rendered headless. Clicking Enable inside the story does not flip the banner, because Storybook has no email function template and the editor blocks the enable. The logic test covers the flip instead.
  • Not checked: the running app against a real backend.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None. The first run is behind a flag and has no docs yet.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

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

  • Built test-first from the ticket's seams as one lane of the first-run map (Silthus/posthog#253).
  • Skills: tdd, /writing-user-facing-copy, /writing-pr-descriptions. Repo conventions from /writing-kea-logics, /writing-tests and /writing-ui-components were followed from the existing first-run files.
  • CodeRabbit: skipped, the CLI is signed out on this machine.
  • Codex adversarial review (GPT-6.1 Sol) found one issue: dismissal was remembered per state, so enabling brought a closed banner back. Fixed: one dismissal per workflow, as the spec says.
  • Screenshots live on the fork branch assets/fr-262-banners, because the PR asset uploader refused this account.
  • Public artifact: all fixtures come from the repo's own template files. No session material is in the diff.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Oct 5, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes add managed sandbox email sender provisioning, recipient checks, fixed-identity delivery, and sender controls across workflow email surfaces. They also add start metadata to workflow templates and introduce a feature-flagged first-run gallery and workflow customization flow. The first-run flow selects templates based on observed events, supports email edits and test sends, and creates workflows in draft or active status.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to c90a9

The sandbox sender and first-run workflow paths are behind feature flags, and recipient checks fail closed. Two small follow-ups remain. One template describes a start condition that differs from its actual trigger. A misconfigured sandbox sender would fail its sends instead of being caught at startup. Neither issue blocks merging, but both are worth fixing.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to c90a9

Sender ownership and recipient restrictions are enforced on the server. However, activating the shared sender depends on separate abuse-containment protections and coordinated email-provider configuration. Delivery is disabled by default, which substantially limits immediate exposure.

Retained concerns

  • Medium · security · inferred: Shared sandbox delivery is exempt from project trust-tier sending budgets. Independent sandbox volume caps and pause handling are documented activation prerequisites, but their effective implementation and deployment were not established. Enabling delivery before those prerequisites are satisfied could let one eligible project consume shared sending capacity or damage shared sender reputation. This is a conditional rollout concern, not an observed exploit; the default-disabled switch and recipient restrictions materially reduce exposure.
Security review details

Security Blast Radius

  • inferred — An authorized workflow author can supply email content and recipients using their project's managed sender. Recipient checks constrain delivery to verified members of that project's organization, while the sender identity and configured sandbox tenant are shared resources. Sender reputation and capacity therefore have a wider potential failure scope than an individual workflow.

Security Findings and Attack Paths

  • observed — Direct invocation requests can bypass browser disabled states, but the inspected server path requires workflow write authority, preserves the request's project identity, checks sender ownership, and blocks ineligible sandbox recipients before SES. No successful cross-project sender or outside-organization recipient bypass was established in this trace.

Trust Boundaries and Controls

  • observed — Public update, verification, and deletion paths protect managed sandbox integrations. Native creation rejects the configured sandbox domain. Runtime fixes the sender address, removes Reply-To overrides, disables tracking, and appends an organization-identifying footer.

Resilience and Maintainability Implications

  • observed — Failed post-save reload publication can temporarily leave worker configuration stale, but access-triggered cache expiry provides recovery rather than permanent drift. Rollback removes explicit managed-sender guards, yet the base already rejects the sandbox provider for update/verification and excludes it from SES identity cleanup; this does not establish shared-identity takeover or deletion.

Hardening Proposals

  • proposed — Make sandbox activation depend on demonstrable independent volume containment, shared-tenant pause handling, and consistent SES identity/configuration-set readiness. Retain the delivery kill switch until those prerequisites are satisfied, including under partial deployment and rollback.
🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has the required sections and detailed test evidence, but it does not match the supplied change summary. It describes a five-file banner-only change, while the summary includes extensi… Update the description to cover the full scope of the supplied changes, including the first-run workflow experience, sandbox sender provisioning and delivery, validation, and template start conditions. Correct the file and line counts, expl…
Full details: Description check

Explanation

The description has the required sections and detailed test evidence, but it does not match the supplied change summary. It describes a five-file banner-only change, while the summary includes extensive first-run workflow and sandbox email sender functionality.

Resolution

Update the description to cover the full scope of the supplied changes, including the first-run workflow experience, sandbox sender provisioning and delivery, validation, and template start conditions. Correct the file and line counts, explain the user-visible changes and release status for the full PR, and revise any statements that say these features are outside the PR. Alternatively, split the changes so this PR contains only the banner work described.

✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/workflows-first-run-banners
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
products/workflows/backend/templates/onboarding_started_but_not_completed_template.json-4-8 (1)

4-8: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

starts_on.detail does not match the trigger filter.

detail says "on /onboarding". The trigger filters $current_url icontains "messaging/campaigns" on lines 39 and 133. The gallery tells users that the template starts on onboarding pageviews. The workflow actually fires only on campaign-page URLs. Align the trigger URL value with /onboarding, or change detail to describe the real filter.

🧹 Nitpick comments (4)
nodejs/src/cdp/cdp-services.ts (1)

464-472: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fail fast when the sandbox sender is enabled but not fully configured.

WORKFLOWS_SANDBOX_SENDER_ENABLED=true can be set while SES_SANDBOX_FROM_ADDRESS, SES_SANDBOX_TENANT_NAME, or SES_SANDBOX_CONFIGURATION_SET stays at its '' default. In that case resolveFromSender returns email: '' and sesRouteFor returns an empty tenant and configuration set. Every sandbox send then reaches SES with "Name" <> and fails as email_failed. Nothing reports the misconfiguration at startup. Fix: validate the three settings here, and either throw or force enabled to false with an error log.

♻️ Proposed guard
+        const sandboxConfigured = !!(
+            config.SES_SANDBOX_TENANT_NAME &&
+            config.SES_SANDBOX_CONFIGURATION_SET &&
+            config.SES_SANDBOX_FROM_ADDRESS
+        )
+        if (config.WORKFLOWS_SANDBOX_SENDER_ENABLED && !sandboxConfigured) {
+            logger.error('📧', 'Sandbox sender enabled without tenant, configuration set, and from address; disabling it')
+        }
         new SandboxEmailSender(
             {
-                enabled: config.WORKFLOWS_SANDBOX_SENDER_ENABLED,
+                enabled: config.WORKFLOWS_SANDBOX_SENDER_ENABLED && sandboxConfigured,
products/workflows/frontend/firstRun/firstRunSenderLogic.ts (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import IntegrationType through the ~/types alias.

'../../../../frontend/src/types' climbs out of the product package to reach the same module that every other file imports as ~/types. This adds a second import path for one symbol, and the path breaks if the folder moves. Use import type { IntegrationType } from '~/types'.

♻️ Proposed fix
-import type { IntegrationType } from '../../../../frontend/src/types'
+import type { IntegrationType } from '~/types'

As per coding guidelines: "Every symbol has exactly one home. No re-export shims creating a second import path".

Source: Coding guidelines

products/workflows/backend/services/sandbox_sender.py (1)

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Define the workflows-sandbox-sender flag key once. Provisioning and save-time validation each hardcode the flag key. If one copy is renamed, the two checks read different flags.

  • products/workflows/backend/services/sandbox_sender.py#L14-L14: remove SANDBOX_SENDER_FLAG here. Move it to posthog/models/integration/email.py next to SANDBOX_EMAIL_PROVIDER, export it from posthog/models/integration/__init__.py, and import it here.
  • posthog/cdp/validation.py#L148-L155: replace the "workflows-sandbox-sender" literal with the shared constant.
products/workflows/backend/presentation/views/hog_flow.py (1)

2270-2270: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Exclude sandbox integrations from the sharer query too.

Line 2270 removes sandbox integrations from this team's domain list only. The sharer query below (lines 2280-2284) has no matching exclusion. The sandbox domain is shared by every team, so it does not reach domains today. If a team's own verified domain ever matches the sandbox domain, every team with a provisioned sandbox sender counts as a sharer. The domain then goes into withheld for most callers. Add the same .exclude(config__contains={"provider": SANDBOX_EMAIL_PROVIDER}) to the sharer query so both queries treat sandbox senders the same way.

Proposed fix
         Integration.objects.filter(kind="email", config__verified=True, config__domain__in=domains)
         .exclude(team_id=team.id)
+        .exclude(config__contains={"provider": SANDBOX_EMAIL_PROVIDER})
         .values_list("config__domain", "team_id")

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
  • Review profile: QUIET
  • Plan: Enterprise
  • Run ID: b35b7f74-5c64-4071-8ec6-5f4545b0e37c
📥 Commits

Reviewing files that changed from the base of the PR and between 9295706 and c90a96e.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • products/integrations/frontend/generated/api.ts is excluded by !**/generated/**
  • products/workflows/frontend/generated/api.schemas.ts is excluded by !**/generated/**
📒 Files selected for processing (104)
  • docs/internal/workflows-email-sending-tiers.md
  • frontend/src/lib/components/CyclotronJob/CyclotronJobInputs.tsx
  • frontend/src/lib/constants.tsx
  • frontend/src/lib/integrations/IntegrationsList.tsx
  • frontend/src/lib/integrations/integrationsLogic.test.ts
  • frontend/src/lib/integrations/integrationsLogic.ts
  • frontend/src/lib/integrations/utils.ts
  • frontend/src/scenes/hog-functions/email-templater/EmailTemplater.test.tsx
  • frontend/src/scenes/hog-functions/email-templater/EmailTemplater.tsx
  • frontend/src/scenes/hog-functions/email-templater/NativeEmailIntegrationChoice.stories.tsx
  • frontend/src/scenes/hog-functions/email-templater/emailTemplaterLogic.test.ts
  • frontend/src/scenes/hog-functions/email-templater/emailTemplaterLogic.tsx
  • frontend/src/scenes/hog-functions/email-templater/selectSenders.test.ts
  • frontend/src/scenes/hog-functions/email-templater/selectSenders.ts
  • nodejs/package.json
  • nodejs/src/cdp/cdp-services.ts
  • nodejs/src/cdp/config.ts
  • nodejs/src/cdp/services/managers/organization-members.service.ts
  • nodejs/src/cdp/services/messaging/email.service.test.ts
  • nodejs/src/cdp/services/messaging/email.service.ts
  • nodejs/src/cdp/services/messaging/sandbox-email-sender.ts
  • nodejs/src/cdp/types.ts
  • nodejs/src/cdp/utils.ts
  • posthog/api/integration.py
  • posthog/cdp/test/test_validation.py
  • posthog/cdp/validation.py
  • posthog/models/integration/__init__.py
  • posthog/models/integration/email.py
  • posthog/settings/ses.py
  • products/integrations/mcp/tools.yaml
  • products/workflows/backend/facade/api.py
  • products/workflows/backend/facade/contracts.py
  • products/workflows/backend/presentation/views/hog_flow.py
  • products/workflows/backend/presentation/views/hog_flow_template.py
  • products/workflows/backend/services/sandbox_sender.py
  • products/workflows/backend/templates/announce_a_new_feature_template.json
  • products/workflows/backend/templates/celebrate-milestone.json
  • products/workflows/backend/templates/feature-adoption-tips.json
  • products/workflows/backend/templates/heavy-usage-detected.json
  • products/workflows/backend/templates/high-intent-notification.json
  • products/workflows/backend/templates/negative-survey-response.json
  • products/workflows/backend/templates/onboarding_started_but_not_completed_template.json
  • products/workflows/backend/templates/re-engagement-workflow.json
  • products/workflows/backend/templates/repeated-failure-alert.json
  • products/workflows/backend/templates/support-ticket-notification.json
  • products/workflows/backend/templates/ticket-unresolved-alert.json
  • products/workflows/backend/templates/trial-ending-reminder.json
  • products/workflows/backend/templates/trial_started_upgrade_nudge_template.json
  • products/workflows/backend/templates/unlocking_advanced_features.json
  • products/workflows/backend/templates/unused-features-education.json
  • products/workflows/backend/templates/user-stuck-onboarding.json
  • products/workflows/backend/templates/user-upgrade-webhook.json
  • products/workflows/backend/templates/welcome_email_sequence_template.json
  • products/workflows/backend/test/test_sandbox_sender_lifecycle.py
  • products/workflows/backend/tests/api/test_email_reputation.py
  • products/workflows/backend/tests/api/test_hog_flow_template.py
  • products/workflows/backend/tests/api/test_sandbox_sender.py
  • products/workflows/backend/tests/api/test_sandbox_sender_validation.py
  • products/workflows/frontend/Broadcasts/broadcastTestSendLogic.ts
  • products/workflows/frontend/Broadcasts/steps/BroadcastContentStep.tsx
  • products/workflows/frontend/Channels/MessageChannels.stories.tsx
  • products/workflows/frontend/Channels/MessageChannels.tsx
  • products/workflows/frontend/Channels/SandboxEmailSenderRow.tsx
  • products/workflows/frontend/TemplateLibrary/SendTestEmailModal.stories.tsx
  • products/workflows/frontend/TemplateLibrary/SendTestEmailModal.test.tsx
  • products/workflows/frontend/TemplateLibrary/SendTestEmailModal.tsx
  • products/workflows/frontend/TemplateLibrary/messageTemplateTestSendLogic.test.ts
  • products/workflows/frontend/TemplateLibrary/messageTemplateTestSendLogic.ts
  • products/workflows/frontend/Workflows/WorkflowScene.tsx
  • products/workflows/frontend/Workflows/hogflows/findTestSendSkipReason.test.ts
  • products/workflows/frontend/Workflows/hogflows/findTestSendSkipReason.ts
  • products/workflows/frontend/Workflows/hogflows/steps/components/HogFlowFunctionConfiguration.tsx
  • products/workflows/frontend/Workflows/templates/WorkflowTemplateCard.tsx
  • products/workflows/frontend/WorkflowsScene.tsx
  • products/workflows/frontend/emptyState/workflowsEmptyState.tsx
  • products/workflows/frontend/firstRun/FirstRunWorkflowBanner.stories.tsx
  • products/workflows/frontend/firstRun/FirstRunWorkflowBanner.tsx
  • products/workflows/frontend/firstRun/GalleryTemplateCard.tsx
  • products/workflows/frontend/firstRun/MakeItYoursActions.tsx
  • products/workflows/frontend/firstRun/MakeItYoursEmailList.tsx
  • products/workflows/frontend/firstRun/TemplateStartsOnLine.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRun.stories.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRunGallery.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRunMakeItYours.stories.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRunMakeItYours.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRunTab.test.tsx
  • products/workflows/frontend/firstRun/WorkflowsFirstRunTab.tsx
  • products/workflows/frontend/firstRun/buildFirstRunTestSend.ts
  • products/workflows/frontend/firstRun/buildFirstRunWorkflow.ts
  • products/workflows/frontend/firstRun/firstRunGalleryLogic.test.ts
  • products/workflows/frontend/firstRun/firstRunGalleryLogic.ts
  • products/workflows/frontend/firstRun/firstRunMakeItYoursLogic.test.ts
  • products/workflows/frontend/firstRun/firstRunMakeItYoursLogic.ts
  • products/workflows/frontend/firstRun/firstRunSenderLogic.test.ts
  • products/workflows/frontend/firstRun/firstRunSenderLogic.ts
  • products/workflows/frontend/firstRun/firstRunStoryFixtures.ts
  • products/workflows/frontend/firstRun/firstRunWorkflowBannerLogic.test.ts
  • products/workflows/frontend/firstRun/firstRunWorkflowBannerLogic.ts
  • products/workflows/frontend/firstRun/firstRunWorkflowStorage.ts
  • products/workflows/frontend/firstRun/templateEmails.ts
  • products/workflows/frontend/firstRun/withFirstRunSender.test.ts
  • products/workflows/frontend/firstRun/withFirstRunSender.ts
  • products/workflows/package.json
  • services/mcp/src/api/generated.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 6 remain after this review.

@Silthus

Silthus commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 GPT-6.1 Sol responding on behalf of Michael

Replaced by the draft fork PR #335 in its native fork stack. @meikelmosby will start migrating the stack from the fork into PostHog/posthog for upstream review and CI. Closing this master-targeted copy to keep the handoff clear.

@Silthus Silthus closed this Oct 7, 2026
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