Skip to content

feat(workflows): add a pill search bar to the workflows list - #106759

Draft
Silthus wants to merge 4 commits into
PostHog:masterfrom
Silthus:workflows-list-v2/search-bar
Draft

Silthus wants to merge 4 commits into
PostHog:masterfrom
Silthus:workflows-list-v2/search-bar

Conversation

@Silthus

@Silthus Silthus commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Note

Stacked on #106561 (the slim hog_flows/summaries/ endpoint). Only the commits after aafae709320 (the #106561 head) are this PR's own. Review it here: Silthus/posthog@aafae70...workflows-list-v2/search-bar
This PR targets master because GitHub stacks cannot span a fork. It is rebased onto master once #106561 merges.

Problem

  • People with many workflows can't find the failing ones, or the ones a teammate owns. Today's list has four dropdowns, a server search and 30 rows a page.
  • The workflows list v2 direction puts one pill search bar over a compact list of every workflow, with facets and counts computed in the browser.
  • feat(workflows): add a slim hog_flows summaries endpoint #106561 adds the slim summaries endpoint this needs. This PR adds the frontend, behind the workflows-list-v2 flag.

Changes

  • With workflows-list-v2 on, the Workflows tab shows a pill search bar over one compact list of workflows, most recently updated first.
    • Each row shows its description under the name as one muted line, with the full text in a tooltip.
    • Facets: status, type, trigger, owner and health on focus, created-by by typing. Suggestions show counts.
    • Typing sta, Tab, then picking a value adds a Status: Active pill. -status: excludes. Free text stays text.
    • Typed or pasted status:active also makes a pill, once the token is complete.
    • Owner is an explicit Owner: @handle in the description, else the creator's first name or email name.
  • Default columns are Name, Status, Owner (@handle) and Updated. The "…" menu next to "New workflow" adds Type, Trigger, Created by, Last 7 days and Health, or hides Owner. The choice persists per browser.
  • Health and "Last 7 days" come from one call to the existing hog_flows/metrics/global/ endpoint, made after the list loads.
    • The list renders first. Those two columns show a spinner until the counts arrive.
    • A failed call shows "Unavailable" there, with no error toast. The list keeps working.
    • Health is Failing with any failed run, Healthy with only successful runs, else No runs.
  • Free text matches name and description in the browser. Text of 3+ characters also asks the server search (debounced, previous request cancelled), so step names and email content still match.
    • While that server search is pending, the table shows its loading state. "No workflows match these filters" waits for the answer.
    • A failed server search keeps the browser matches and shows a one-line notice above the table. It shows no toast.
  • The list loads every page of hog_flows/summaries/ (500 per page, type=messaging,automation,loop) through the generated client.
  • Loading, a load error with Retry, and "No workflows match these filters" with "Clear filters" are separate screens. Pages of 100 rows stay on the client.
  • URL state: q holds the pills and text the free text. Old status, type, trigger_type, created_by and search params move into them once.
  • Row menus keep Enable or Disable, Duplicate, Archive, Restore and Delete. Duplicate fetches the full workflow first, because the summary row has no step graph.
    • One action runs per workflow at a time. A repeated press does nothing, and the menu item shows a loading state until the request ends.
    • Enable, Disable, Archive and Restore take status and updated_at from the server answer, so Updated and the sort stay current.
  • With the flag off, the list, its URL params and its requests are unchanged.
  • Mechanical, shared with the flag-off list:
    • One workflowRowActions module on the generated client, and one WorkflowRowMenuOverlay and WorkflowStatusTag.
    • The flag-off list's archive, restore and delete now go through that module. They still address the team id, which differs from the project id in a child environment.
  • FacetSearchBar lives next to its only consumer, in products/workflows/frontend/Workflows/WorkflowsListV2/FacetSearchBar/. It is controlled and has no workflow-specific code, so it can move to a shared layer when a second product needs it.
  • LemonInput accepts the combobox ARIA props, LemonButton accepts aria-selected, and LemonSnack takes an optional closeLabel. The bar needs them for its combobox role, its option state and labeled pill remove buttons.
  • Known gap, accepted for phase 1: no step-match excerpts under the name, because the summary rows carry no email bodies. Server search still finds those workflows.

Storybook screenshots, invented data only (Scenes-App/Workflows/List v2):

Default columns, 1440px Default columns, 520px scene
Default columns Default columns narrow
All columns, 1440px All columns, 520px scene
All columns All columns narrow
Health suggestions with counts Metrics still loading
Health suggestions Metrics loading
Server search failed 520px scene, pills and text
Server search failed Narrow scene

The earlier screenshots in Silthus#163 (comment) show the first, larger version (email templates, the Sends column, channel and from facets). They no longer match this PR.

How did you test this code?

  • workflowsListV2Logic.test.ts:
    • Two presses of Duplicate while the first is pending send one copy, and the pending state clears on success and on failure. Catches duplicate copies from a double press.
    • Enable and Restore write the server's updated_at into the row. Catches a stale Updated column and sort.
    • A server search for the current text reports pending until it answers, then done. A failed one reports failed, keeps the browser matches and shows no toast. Catches the "No workflows match" flash and a toast per keystroke.
    • Two pages load, merge newest first, and dedupe a repeated row. Catches load-all stopping at page one.
    • A next link that never ends shows the load error. Catches an endless load.
    • The list shows before the metrics answer, then health:idle narrows once they arrive. Catches the list waiting on metrics, or health never filling in.
    • A failed metrics call keeps the rows and shows no toast. Catches a metrics outage breaking the list.
    • With a team id that differs from the project id, list, metrics, status and duplicate calls go to the team id.
    • Old params move into q and text once, and 007 survives the URL round trip.
    • Server search ORs its ids in and ignores a late answer on A→B and A→B→A.
  • workflowListRows.test.ts: owner parsing (Co-owner: and Previous owner: don't count), health buckets from metrics rows, and the trigger read from an unknown trigger value. Catches wrong owners, wrong health, and a crash on a workflow without a trigger.
  • WorkflowsScene.test.tsx: keyboard filtering writes q=status:active; page 2 works and a new filter goes back to page one; Owner shows with no saved columns; a stale saved column is ignored; the flag off makes no v2 requests.
  • workflowRowActions.test.ts and workflowsLogic.test.ts: the shared dialogs reach the team's endpoint, run the refresh, and skip it on failure. A failed archive or delete also clears the pending state.
  • The duplicate and updated_at tests fail with the guard and the merge removed, checked locally.
  • facetQuery.test.ts, facetSearchBarLogic.test.ts, FacetSearchBar.test.tsx: parsing, counts, suggestion order and the keyboard paths of the generic bar.
  • Local runs: the Jest files above, tsgo --noEmit over frontend (0 errors), oxlint and oxfmt on the changed files, and hogli ci:preflight --strict in the pre-push hook.
  • The diff-aware devex and security semgrep run (--baseline-commit 2bd04659765) reports 0 findings on this layer.
  • Visual check: the Storybook stories rendered in headless Chromium at 1440px and at a 520px scene. The table above shows them.
  • Not checked visually: the loading state on a pending row menu item. The logic test covers the pending state.
  • CI not run: fork runs await maintainer approval.
  • Not run: the new stories in the visual-regression runner.
  • test-new-events-schema: not needed. The diff is frontend only and touches no event ingestion, event reads or SQL over events.

👉 Stay up-to-date with PostHog coding conventions for a smoother review.

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. No doc under docs/ covers the workflows list UI.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, claude-opus-5-5[1m]

  • Scope was cut to match the slimmed feat(workflows): add a slim hog_flows summaries endpoint #106561, which now returns only summary rows. This layer dropped email template rows, the Sends column and its highlighting, dispatch icons, the kind, channel, sends and from facets, and every backend and products/messaging change.
  • Health and "Last 7 days" moved from row fields to one lazy call to the existing global metrics endpoint.
  • The branch was rebuilt on the new feat(workflows): add a slim hog_flows summaries endpoint #106561 head as one commit. The earlier seven commits are replaced.
  • Skills invoked: /writing-ui-components, /placing-product-frontend-code, /adopting-generated-api-types, /writing-tests, /writing-user-facing-copy, /writing-code-comments, /writing-pr-descriptions.
  • Comments were trimmed to the ones that explain a why.
  • After review, a second commit moved FacetSearchBar into the workflows product, added the description line and the Owner default, and fixed three findings from an adversarial review: double-submit on row actions, the "No workflows match" flash during server search, and a stale updated_at after status changes.
  • The ARIA additions to LemonInput, LemonButton and LemonSnack stay. Without them the bar needs DOM attribute writes or a hand-rolled input. The PR touches frontend/src/lib/constants.tsx for the flag in any case.
  • Screenshots are from Storybook with invented fixture data. They live on the fork's proof/wl-v2-search-slim-2 branch, because hogli pr:upload-image needs PostHog write access.
  • CodeRabbit CLI: no local pass on this rework. The CodeRabbit bot reviews the draft.
  • products/workflows/package.json gains @testing-library/user-event as a dev dependency for the scene test.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Sep 25, 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 Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The pull request adds reusable facet search behavior and a feature-flagged workflows list. It adds workflow and email-template summary APIs with filtering, pagination, sender resolution, metrics, and generated contracts. The new frontend combines workflow and template rows, supports facet and text search, URL synchronization, optional columns, row actions, and summary rendering. Tests and Storybook stories cover API responses, accessibility, keyboard behavior, filtering, pagination, loading, errors, and feature-flag states.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 47b23

With the new list enabled, quickly editing search text back to an earlier value can hide workflows that only the server search found. The list stays unchanged when the flag is off. This can merge with a small follow-up.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 47b23

The reviewed list and summary endpoints did not show a new route to hidden workflows or credential-bearing action data. Some server-side checks for actions launched from the new list remain unverified, so the assessment is not a clean bill of health.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The new browser view increases the amount of workflow and template metadata loaded for an authorized project viewer. The inspected workflow route filters object visibility, while the template route filters by team; no cross-team exposure was demonstrated.

Trust Boundaries and Controls

  • observed — The workflow summary action repeats object-access filtering because the normal list hook does not cover this custom action. Its service-authenticated branch is an explicit exception; the exact credential classification for that branch was not inspected.
  • observed — The new workflow menu gates enable, disable, archive, restore, and delete behind Editor access in the UI. That control does not substitute for server-side authorization, which was not fully established for every mutation path.

Resilience and Maintainability Implications

  • inferred — Server-first workflow mutations limit optimistic local misrepresentation on request failure. Server-side ordering and idempotency under repeated or concurrent actions remain unverified.

Hardening Proposals

  • proposed — Before rollout, verify server-enforced project ownership, mutation permissions, and repeat-request behavior for both kinds of row action; reconcile the list after failed destructive operations. This is a verification and hardening proposal, not an established bypass.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and follows the repository template. It explains the problem, user-visible changes, feature-flag release status, testing scope, screenshots, known gaps, agent context, and …
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch workflows-list-v2/search-bar
🧪 Generate unit tests (beta)
  • Create a new PR

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/frontend/Workflows/WorkflowsListV2/workflowsListV2Logic.ts-395-399 (1)

395-399: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clearing or shortening the text leaves the old server search result in place.

The listener only calls searchWorkflows when the text has at least 3 characters. It never clears serverSearch. matchesText compares the server result to the current text, so a stale result cannot match the wrong text. A stale result can still block a new request, though. For example, the user types renews and then renew, which starts a new search. The user then types renews again. The renew result is still in flight, so values.serverSearch?.text still equals renews. The listener skips the request. When the renew response arrives, it replaces serverSearch, and rows found only by the server for renews disappear.

Fix: in the guard, also compare against the text that was last requested, or always dispatch searchWorkflows when the text differs from the pending request.

Proposed fix
-    listeners(({ actions, values }) => ({
+    listeners(({ actions, values, cache }) => ({
         setValue: ({ value }) => {
             const text = value.text.trim()
-            if (text.length >= MIN_SERVER_SEARCH_LENGTH && values.serverSearch?.text !== text) {
+            if (text.length >= MIN_SERVER_SEARCH_LENGTH && cache.lastSearchText !== text) {
+                cache.lastSearchText = text
                 actions.searchWorkflows(text)
             }
         },
🧹 Nitpick comments (2)
products/workflows/backend/api/hog_flow_list.py (1)

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

Generate the type parameter from WORKFLOW_TYPES like its siblings.

type uses a hand-written description, while exclude_type uses _comma_list_parameter(..., WORKFLOW_TYPES, ...). If HogFlowType gains a value, the type docs go out of date and exclude_type does not. Build type with _comma_list_parameter and keep the extra semantics text in the description.

products/workflows/backend/api/test/test_hog_flow_summaries.py (1)

375-375: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Exercise query counts as an ordinary member.

self.user is the organization owner, so this test does not exercise object-level access checks for an ordinary member. The equal query counts can therefore miss a per-row access-control query. Log in a non-admin member and grant that member access to the created workflows before measuring queries. products/workflows/backend/api/test/test_hog_flow_access_control.py Line 253 identifies the owner bypass.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: efe5cbd9-0104-413e-9b25-84ea9a15c7e6

📥 Commits

Reviewing files that changed from the base of the PR and between a58fabc and 47b2328.

⛔ Files ignored due to path filters (7)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • products/messaging/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/messaging/frontend/generated/api.ts is excluded by !**/generated/**
  • products/workflows/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/workflows/frontend/generated/api.ts is excluded by !**/generated/**
  • services/mcp/src/generated/workflows/api.ts is excluded by !**/generated/**
  • services/mcp/src/tools/generated/workflows.ts is excluded by !**/generated/**
📒 Files selected for processing (49)
  • frontend/src/lib/components/FacetSearchBar/FacetSearchBar.stories.tsx
  • frontend/src/lib/components/FacetSearchBar/FacetSearchBar.test.tsx
  • frontend/src/lib/components/FacetSearchBar/FacetSearchBar.tsx
  • frontend/src/lib/components/FacetSearchBar/facetQuery.test.ts
  • frontend/src/lib/components/FacetSearchBar/facetQuery.ts
  • frontend/src/lib/components/FacetSearchBar/facetSearchBarLogic.test.ts
  • frontend/src/lib/components/FacetSearchBar/facetSearchBarLogic.ts
  • frontend/src/lib/components/owners.yaml
  • frontend/src/lib/constants.tsx
  • frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx
  • frontend/src/lib/lemon-ui/LemonInput/LemonInput.tsx
  • frontend/src/lib/lemon-ui/LemonSnack/LemonSnack.tsx
  • posthog/api/app_metrics2.py
  • posthog/cdp/test/test_validation.py
  • posthog/cdp/validation.py
  • posthog/settings/web.py
  • products/messaging/backend/api/message_templates.py
  • products/messaging/backend/api/test/test_message_templates.py
  • products/messaging/backend/email_senders.py
  • products/messaging/mcp/tools.yaml
  • products/workflows/CONTRIBUTING.md
  • products/workflows/backend/api/hog_flow.py
  • products/workflows/backend/api/hog_flow_list.py
  • products/workflows/backend/api/test/test_hog_flow.py
  • products/workflows/backend/api/test/test_hog_flow_access_control.py
  • products/workflows/backend/api/test/test_hog_flow_summaries.py
  • products/workflows/frontend/Workflows/WorkflowDispatchIcons.tsx
  • products/workflows/frontend/Workflows/WorkflowStatusTag.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/WorkflowSendsCell.test.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/WorkflowSendsCell.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/WorkflowsListV2.stories.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/WorkflowsListV2.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/WorkflowsListV2ColumnsMenu.tsx
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowListFacets.ts
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowListRows.test.ts
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowListRows.ts
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowsListV2Fixtures.ts
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowsListV2Logic.test.ts
  • products/workflows/frontend/Workflows/WorkflowsListV2/workflowsListV2Logic.ts
  • products/workflows/frontend/Workflows/WorkflowsTable.tsx
  • products/workflows/frontend/Workflows/hogflows/steps/HogFlowSteps.tsx
  • products/workflows/frontend/WorkflowsScene.test.tsx
  • products/workflows/frontend/WorkflowsScene.tsx
  • products/workflows/mcp/tools.yaml
  • products/workflows/package.json
  • services/mcp/schema/generated-tool-definitions.json
  • services/mcp/schema/tool-definitions-all.json
  • services/mcp/src/api/generated.ts
  • services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-list.json

Included review availability: Your plan provides up to 12 included reviews per hour; 7 remain after this review.

@Silthus
Silthus force-pushed the workflows-list-v2/search-bar branch from 47b2328 to 4ce5faa Compare September 25, 2026 15:37
@Silthus Silthus changed the title feat(workflows): add a pill search bar to the workflows list behind a flag feat(workflows): add a pill search bar to the workflows list Sep 25, 2026
@Silthus
Silthus force-pushed the workflows-list-v2/search-bar branch from 4ce5faa to 7cbee12 Compare September 25, 2026 15:52
@Silthus
Silthus force-pushed the workflows-list-v2/search-bar branch from 7cbee12 to ff0173f Compare September 25, 2026 17:01
`GET hog_flows/summaries/` returns the MCP summary fields plus the workflow
type, without the step graph, so the workflows list can load every row. It
takes the list's filters and search, sorts on created_at so a save during a
paged load does not move rows, applies the access-level filter itself, and
is gzipped. Both list actions now select the creator in the same query.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silthus and others added 3 commits September 28, 2026 06:30
The list v2 applies its facets in the browser, so the name-first search
tier hid step content matches that the facets would have kept. The
summaries action now matches name, description and step content in one
pass. The existing list keeps its tiered search.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Behind the `workflows-list-v2` flag, the Workflows tab shows a compact
list with a pill search bar over it.

- Loads every row from `hog_flows/summaries/` (500 per page) through
  the generated client.
- Filters on status, type, trigger, owner, health and created by, and
  matches free text on name and description. Text of 3+ characters
  also asks the server, so step names and email content still match.
- Health and the "Last 7 days" column come from one lazy call to
  `hog_flows/metrics/global/` after the list renders. A failed call
  shows "Unavailable" and leaves the list working.
- Optional columns are picked from the "..." menu and persist.
- Filters live in `q` and `text` URL params. Old list params redirect
  once.
- Shares the row menu, status tag and archive, restore and delete
  dialogs with the flag-off list, which now sends those calls to the
  team id through the generated client.
- `FacetSearchBar` in `lib/components` holds the generic bar.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Each row shows its description under the name as one muted line,
  with the full text in a tooltip. Owner is a default column.
- FacetSearchBar moves from lib/components into the list v2 folder of
  the workflows product, with its tests and stories.
- Row actions run one at a time per workflow. A repeated press is
  ignored and the menu item shows a loading state until it finishes.
- Enable, disable, archive and restore take status and updated_at
  from the server answer, so Updated and the sort stay current.
- While the server search for the current text is pending, the table
  shows its loading state instead of "No workflows match". A failed
  server search keeps the client matches and shows a notice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Silthus
Silthus force-pushed the workflows-list-v2/search-bar branch from 1fbb79f to 97972c5 Compare September 28, 2026 06:31

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