Skip to content

improvement(workflows): replace Zustand workflow sync with React Query as single source of truth#3859

Closed
waleedlatif1 wants to merge 42 commits intostagingfrom
waleedlatif1/fix-chat-workflow-stale-name
Closed

improvement(workflows): replace Zustand workflow sync with React Query as single source of truth#3859
waleedlatif1 wants to merge 42 commits intostagingfrom
waleedlatif1/fix-chat-workflow-stale-name

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Remove workflows record and sync effects from Zustand registry store — React Query is now the single source of truth for workflow metadata
  • Add useWorkflowMap hook with select for structural sharing (replaces consumer-side useMemo + Object.fromEntries)
  • Add useUpdateWorkflowMutation and useDeleteWorkflowMutation with optimistic updates
  • Extract workflowKeys to shared util to avoid circular imports between store and query hooks
  • Simplify hydration phases from 6 to 4 (metadata phases replaced by RQ isPending/isSuccess)
  • Delete dead optimistic-update.ts util (zero consumers)
  • ~50 consumer files migrated from Zustand selectors to RQ hooks/cache reads

Type of Change

  • Improvement (refactor)

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

waleedlatif1 and others added 30 commits February 16, 2026 00:36
…stash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)

* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

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

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

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

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
… fixes, removed retired models, hex integration
…ogle tasks and bigquery integrations, workflow lock
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 31, 2026 2:53am

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 31, 2026

PR Summary

Medium Risk
Broad refactor of workflow metadata reads/writes across the app, replacing Zustand-backed lookups with React Query cache and new optimistic mutations; risk is mainly regressions in hydration/loading states and cache consistency during create/update/delete/duplicate/reorder flows.

Overview
Workflow metadata is migrated from the Zustand useWorkflowRegistry.workflows record to React Query as the single source of truth. Consumers across home, logs, sidebar, workflow canvas, copilot mentions, and selectors now read via useWorkflows/useWorkflowMap or synchronous cache helpers (getWorkflows) instead of registry state.

The PR introduces shared workflowKeys/WorkflowQueryScope utilities, adds useWorkflowMap (RQ select-based record), and implements new React Query mutations for workflow lifecycle (useUpdateWorkflow, useDeleteWorkflowMutation) plus rewritten optimistic handling for create/duplicate/reorder. Registry hydration is simplified (metadata phases removed), and sandbox hydration now seeds/cleans up the React Query workflow list cache.

It also deletes the unused optimistic-update.ts helper and updates the workflow executor handler to rely on loaded child workflow names rather than registry metadata.

Written by Cursor Bugbot for commit 0eafb33. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

setExecutor,
setPendingBlocks,
setActiveBlocks,
workflows,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing routeWorkspaceId in useCallback dependency array

Medium Severity

routeWorkspaceId is used inside handleRunWorkflow (line 386 via getWorkflows(routeWorkspaceId)) but is not listed in the useCallback dependency array. The callback captures a stale closure value. If the workspace changes, getWorkflows will query the old workspace's cache, potentially failing to find the active workflow or finding stale metadata, which could prevent workflow execution or miss the isSandbox flag.

Additional Locations (1)
Fix in Cursor Fix in Web

workspaceId,
workflowId,
metadata: { description: description.trim() || 'New workflow' },
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fire-and-forget mutate loses error handling in save

Medium Severity

The handleSave callback uses updateWorkflowMutation.mutate() (fire-and-forget) instead of await updateWorkflowMutation.mutateAsync(). Since the modal closes immediately after on line 195, any error from the description update is silently swallowed and never reported to the user via setSaveError. The previous code used updateWorkflow which was awaited within the same try/catch. This is a behavioral regression.

Fix in Cursor Fix in Web

: useWorkflowRegistry.getState().activeWorkflowId
if (targetWorkflowId) {
const meta = useWorkflowRegistry.getState().workflows[targetWorkflowId]
const meta = getWorkflows().find((w) => w.id === targetWorkflowId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getWorkflows() called without workspaceId in callback

Low Severity

getWorkflows() is called without a workspaceId argument, relying on URL parsing as a fallback. This occurs inside an SSE streaming callback where the URL may not reliably reflect the expected workspace (e.g. during navigation). This contrasts with ensureWorkflowInRegistry at line 307 which correctly passes workspaceId. An explicit workspaceId argument would be more robust.

Fix in Cursor Fix in Web

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR is a significant architectural refactor that makes React Query the single source of truth for workflow metadata, replacing the workflows record previously held in the Zustand useWorkflowRegistry store. Across ~50 consumer files, direct Zustand selectors (state.workflows[id]) are replaced with useWorkflows, useWorkflowMap, and the synchronous getWorkflows() cache-read helper. Three new mutation hooks — useUpdateWorkflow, useDeleteWorkflowMutation, and useDuplicateWorkflowMutation — implement full optimistic-update / rollback / invalidation cycles. The hydration phase enum is trimmed from 6 to 4 values (idle | state-loading | ready | error) and the dead optimistic-update.ts utility is deleted.

Key observations:

  • The workflowKeys factory extracted to a shared util (hooks/queries/utils/workflow-keys.ts) cleanly avoids the circular-import risk.
  • useWorkflowMap leverages React Query's select option for structural sharing, which is the correct pattern to avoid spurious re-renders.
  • getWorkflows() provides a safe synchronous escape hatch for non-React contexts (event handlers, store actions, drag-drop callbacks), with URL-based workspace fallback via getWorkspaceIdFromUrl().
  • Several useCallback dependency arrays are missing the new mutation objects (duplicateWorkflowMutation, updateWorkflowMutation, updatePublicApiMutation) and routeWorkspaceId. React Query guarantees stable mutate/mutateAsync references so these are not runtime bugs, but they violate react-hooks/exhaustive-deps and should be fixed for consistency.
  • isLoadingWorkflows in use-mention-data.ts is still driven by hydrationPhase even though the workflow list now comes from React Query — consider switching to useWorkflows(...).isPending.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/lint issues with no runtime impact given React Query's stable mutation references.

The architectural direction is correct and the implementation is comprehensive. All 7 comments are P2: missing entries in useCallback dep arrays (safe because mutate/mutateAsync are stable in React Query v5), a minor loading-state derivation inconsistency in the copilot mention hook, and a naming inconsistency in duplicate suffix capitalisation. No data loss, broken contracts, or runtime errors were found.

use-mention-data.ts (isLoadingWorkflows driven by hydration phase instead of React Query state) and the useCallback dep arrays in panel.tsx, api-info-modal.tsx, workflow-item.tsx, use-workflow-execution.ts, and use-drag-drop.ts.

Important Files Changed

Filename Overview
apps/sim/hooks/queries/workflows.ts Core refactor: new useWorkflows, useWorkflowMap, getWorkflows, useUpdateWorkflow, useDeleteWorkflowMutation, useDuplicateWorkflowMutation hooks; all mutations implement optimistic updates with correct rollback and cache invalidation.
apps/sim/stores/workflows/registry/store.ts Removed workflows record and associated CRUD actions; store now only manages active workflow ID, deployment statuses, hydration state, and clipboard — React Query cache is the single source of truth for metadata.
apps/sim/stores/workflows/registry/types.ts Simplified HydrationPhase to 4 values (`idle
apps/sim/hooks/queries/utils/workflow-keys.ts New shared query key factory extracted to break circular imports between the store and query hooks; well-structured with scope support.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts Replaced workflows Zustand selector with getWorkflows(routeWorkspaceId) synchronous cache read; routeWorkspaceId is missing from the handleRunWorkflow callback dependency array.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts Workflows now fetched via useWorkflows (React Query) but isLoadingWorkflows still derived from hydration phase — can incorrectly show a loading state when the React Query cache already has data.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx Duplicate workflow migrated from Zustand action to useDuplicateWorkflowMutation; duplicateWorkflowMutation missing from dep array, and copy suffix (copy) differs from sidebar convention (Copy).
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts Replaced useWorkflowRegistry.getState().workflows with getWorkflows(workspaceId); workspaceId missing from dep arrays of getSiblingItems and collectMovingItems.
apps/sim/app/academy/components/sandbox-canvas-provider.tsx Sandbox workflow metadata now written directly into the React Query cache instead of Zustand state; cleanup path correctly removes the entry on unmount.
apps/sim/lib/core/utils/optimistic-update.ts Dead file with zero consumers deleted — optimistic update logic is now inlined directly in each mutation's onMutate/onError/onSettled handlers.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component needs workflow metadata] --> B{React context?}
    B -->|Hook in component| C[useWorkflows / useWorkflowMap]
    B -->|Non-React: event handler / store action| D[getWorkflows]
    C --> E[(React Query Cache\nworkflowKeys.list)]
    D --> E
    E -->|cache miss / stale| F[fetch /api/workflows?workspaceId=...]
    F --> E

    G[User action: create/update/delete/duplicate] --> H[Mutation hook\nuseCreateWorkflow etc.]
    H -->|onMutate| I[Optimistic update to cache]
    H -->|mutationFn| J[API call]
    J -->|onSuccess| K[Replace temp entry in cache]
    J -->|onError| L[Rollback to snapshot]
    J -->|onSettled| M[invalidateQueries]
    M --> E

    N[Zustand useWorkflowRegistry] -.->|activeWorkflowId\nhydration phase\ndeploymentStatuses\nclipboard| O[Active workflow state only]
    E -.->|metadata only| N
Loading

Comments Outside Diff (3)

  1. apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts, line 742-755 (link)

    P2 routeWorkspaceId missing from useCallback dependency array

    routeWorkspaceId is captured from useParams() and used inside handleRunWorkflow to call getWorkflows(routeWorkspaceId), but it is absent from the dependency array. React Query's getWorkflows will be called with the stale value if the closure is ever reused after a workspace change.

    In practice this is safe today because navigating to a different workspace causes the component to remount (new URL segment), but the missing dep will also suppress the standard exhaustive-deps lint rule.

  2. apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx, line 1035 (link)

    P2 updateWorkflowMutation missing from handleColorChange dependency array

    updateWorkflowMutation.mutate is called inside the callback but updateWorkflowMutation is not listed as a dependency. React Query's mutate is stable so no stale-closure issue in practice, but the dep array is inconsistent.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  3. apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts, line 258 (link)

    P2 workspaceId missing from getSiblingItems and collectMovingItems dependency arrays

    Both callbacks call getWorkflows(workspaceId) where workspaceId is captured from the outer scope via useParams, but neither lists it as a dependency:

    const getSiblingItems = useCallback(, [])                // line 258 — workspaceId missing
    const collectMovingItems = useCallback(, [])             // line 352 — workspaceId missing

    If workspaceId ever changes without an unmount (unlikely given URL-based routing, but possible during fast navigation), drag-and-drop would silently read the wrong workspace's workflow list from the React Query cache. Adding workspaceId to both arrays and clearing siblingsCacheRef on change is the safe fix.

Reviews (1): Last reviewed commit: "improvement(workflows): replace Zustand ..." | Re-trigger Greptile

router,
workspaceId,
])
}, [activeWorkflowId, userPermissions.canEdit, isDuplicating, workflows, router, workspaceId])
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.

P2 duplicateWorkflowMutation missing from useCallback dependency array

duplicateWorkflowMutation.mutateAsync is called inside handleDuplicateWorkflow, but duplicateWorkflowMutation is absent from the dep array. Because React Query guarantees a stable mutateAsync reference this is safe today, but the react-hooks/exhaustive-deps lint rule will flag it and a future change to the mutation object could re-introduce the problem.

Suggested change
}, [activeWorkflowId, userPermissions.canEdit, isDuplicating, workflows, router, workspaceId])
}, [activeWorkflowId, userPermissions.canEdit, isDuplicating, workflows, router, workspaceId, duplicateWorkflowMutation])

Comment on lines 202 to 213
}, [
workflowId,
workspaceId,
description,
workflowMetadata,
updateWorkflow,
starterBlockId,
inputFormat,
paramDescriptions,
setValue,
onOpenChange,
accessMode,
updatePublicApiMutation,
])
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.

P2 updatePublicApiMutation and updateWorkflowMutation missing from handleSave dependency array

Both updatePublicApiMutation.mutateAsync (line 173) and updateWorkflowMutation.mutate (line 180) are called inside handleSave, yet neither mutation object appears in the dependency array. React Query v5 stabilises mutate/mutateAsync across renders so there is no real stale-closure risk today, but this violates react-hooks/exhaustive-deps and should be kept consistent with the rest of the codebase.

Suggested change
}, [
workflowId,
workspaceId,
description,
workflowMetadata,
updateWorkflow,
starterBlockId,
inputFormat,
paramDescriptions,
setValue,
onOpenChange,
accessMode,
updatePublicApiMutation,
])
}, [
workflowId,
workspaceId,
description,
workflowMetadata,
starterBlockId,
inputFormat,
paramDescriptions,
setValue,
onOpenChange,
accessMode,
updatePublicApiMutation,
updateWorkflowMutation,
])

Comment on lines +155 to +157
const { data: registryWorkflowList = [] } = useWorkflows(workspaceId)
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
const isLoadingWorkflows =
hydrationPhase === 'idle' ||
hydrationPhase === 'metadata-loading' ||
hydrationPhase === 'state-loading'
const isLoadingWorkflows = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
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.

P2 isLoadingWorkflows still derives from hydration phase after React Query migration

The workflow list now comes from useWorkflows (React Query), but isLoadingWorkflows is still driven by hydrationPhase. hydrationPhase tracks whether the workflow state (blocks/edges) is loaded, not whether the metadata list is ready. When hydrationPhase === 'idle' (no workflow selected yet), isLoadingWorkflows is true even though React Query may already have the workflow list in cache, causing the mention picker to show a spinner with data already available.

Consider deriving the flag from the React Query query state instead:

Suggested change
const { data: registryWorkflowList = [] } = useWorkflows(workspaceId)
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
const isLoadingWorkflows =
hydrationPhase === 'idle' ||
hydrationPhase === 'metadata-loading' ||
hydrationPhase === 'state-loading'
const isLoadingWorkflows = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
const { data: registryWorkflowList = [], isPending: isWorkflowsPending } = useWorkflows(workspaceId)
const isLoadingWorkflows = isWorkflowsPending

This keeps the loading flag accurate after the move to React Query as single source of truth.

const result = await duplicateWorkflowMutation.mutateAsync({
workspaceId,
sourceId: activeWorkflowId,
name: `${sourceWorkflow.name} (copy)`,
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.

P2 Inconsistent duplicate naming convention: (copy) vs (Copy)

The panel's handleDuplicateWorkflow names the copy "${name} (copy)" (lowercase), while use-duplicate-workflow.ts (used from the sidebar context menu) names it "${name} (Copy)" (uppercase). Both code paths create duplicates but will produce differently capitalised names depending on entry point.

Suggested change
name: `${sourceWorkflow.name} (copy)`,
name: `${sourceWorkflow.name} (Copy)`,

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.

3 participants