Skip to content

fix: enable workflow validation before deployment (was hardcoded to false)#3579

Open
guoyangzhen wants to merge 4 commits intosimstudioai:mainfrom
guoyangzhen:fix/workflow-validation
Open

fix: enable workflow validation before deployment (was hardcoded to false)#3579
guoyangzhen wants to merge 4 commits intosimstudioai:mainfrom
guoyangzhen:fix/workflow-validation

Conversation

@guoyangzhen
Copy link

Problem

The hasValidationErrors flag in panel.tsx was hardcoded to false with a TODO comment, allowing completely broken workflows to be deployed. The backend deploy route also only validated schedules, not workflow state.

Users could deploy workflows with:

  • Unconfigured blocks
  • Disconnected blocks
  • Unknown block types
  • Invalid tool references

Fix

Frontend (panel.tsx):

  • Replace const hasValidationErrors = false with actual validateWorkflowState() call
  • Deploy/Run buttons now properly disabled when validation errors exist

Backend (deploy/route.ts):

  • Add validateWorkflowState() check before allowing deployment
  • Reject with 400 and descriptive error message on validation failure

Defense in Depth

Fix applied at both layers:

  1. UI layer: Prevents clicking Deploy when workflow is invalid
  2. API layer: Rejects invalid deployments even if UI check is bypassed

Fixes #3444

@cursor
Copy link

cursor bot commented Mar 14, 2026

PR Summary

Medium Risk
Adds stricter validation gates in both UI and the deploy API, which may newly block deployments/runs for previously accepted (but invalid) workflow states. Risk is moderate because it changes deployment behavior and returns new 400 errors but does not alter auth or data models.

Overview
Prevents deploying or running invalid workflows by wiring up real workflow-state validation in both the UI and API.

The workflow panel now computes hasValidationErrors via validateWorkflowState(...), disabling the Run action when the current graph is invalid. The POST /api/workflows/[id]/deploy route adds the same validation step (before schedule checks) and rejects invalid states with a 400 including the validation errors.

Written by Cursor Bugbot for commit d646ee1. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 14, 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 14, 2026 1:01pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR enables workflow validation before deployment by replacing a hardcoded false flag in panel.tsx with an actual validateWorkflowState() call, and adding the same check to the backend deploy/route.ts. The intent — defence-in-depth validation at both the UI and API layers — is sound, but both call sites contain the same critical omission: they pass only { blocks, edges } to validateWorkflowState, leaving out loops and parallels. The validation function resolves those to empty sets when absent, so every edge connected to a loop or parallel container is reported as referencing a non-existent block. This means any workflow that uses loops or parallels will have its Run button permanently disabled and all deploy attempts rejected with a 400, even when the workflow is perfectly valid.

Key findings:

  • [Critical] Both panel.tsx (line 362) and route.ts (line 139) omit loops and parallels from the validateWorkflowState call, causing false-positive validation failures for all workflows using loop/parallel blocks.
  • [Performance] The validation selector in panel.tsx runs synchronously on every Zustand store update (including every keystroke), executing a full block/edge/tool scan each time. Extracting it into a useMemo or using a shallow selector would avoid unnecessary re-computation.
  • The Deploy button in deploy.tsx does not consume hasValidationErrors and remains clickable even when the Run button is blocked — the PR description states both buttons should be disabled, but only the Run button is wired up. This is mitigated by the new backend check, but the UX mismatch is worth noting.

Confidence Score: 1/5

  • Not safe to merge — both changed files share a critical bug that will break any workflow using loops or parallels.
  • Both the frontend selector and backend route call validateWorkflowState without supplying loops and parallels. The validation function treats missing loop/parallel maps as empty, so every edge that touches a loop or parallel container is flagged as a dangling reference. This will (a) permanently disable the Run button for affected workflows and (b) reject all deploy requests for those workflows with a 400 error. Loops and parallels are core workflow primitives, so the blast radius is large.
  • Both apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx and apps/sim/app/api/workflows/[id]/deploy/route.ts need the loops and parallels fields added to their validateWorkflowState calls before this can be merged.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx Replaces hardcoded false with a live validateWorkflowState call inside a Zustand selector, but omits loops and parallels from the call — causing false-positive errors for all loop/parallel workflows — and runs an expensive validation on every store update.
apps/sim/app/api/workflows/[id]/deploy/route.ts Adds backend deployment validation via validateWorkflowState, but omits loops and parallels from the call despite them being present in normalizedData, causing valid workflows with loops or parallels to be incorrectly rejected with a 400.

Sequence Diagram

sequenceDiagram
    participant User
    participant Panel (panel.tsx)
    participant WorkflowStore
    participant validateWorkflowState
    participant DeployAPI (route.ts)

    User->>Panel (panel.tsx): Edits workflow / opens panel
    Panel (panel.tsx)->>WorkflowStore: useWorkflowStore selector
    WorkflowStore-->>Panel (panel.tsx): { blocks, edges } (loops/parallels missing ⚠️)
    Panel (panel.tsx)->>validateWorkflowState: validateWorkflowState({ blocks, edges })
    validateWorkflowState-->>Panel (panel.tsx): { valid, errors }
    Note over Panel (panel.tsx): hasValidationErrors controls Run button disabled state

    User->>Panel (panel.tsx): Clicks Deploy
    Panel (panel.tsx)->>DeployAPI (route.ts): POST /api/workflows/[id]/deploy
    DeployAPI (route.ts)->>DeployAPI (route.ts): loadWorkflowFromNormalizedTables(id)
    DeployAPI (route.ts)->>validateWorkflowState: validateWorkflowState({ blocks, edges }) (loops/parallels missing ⚠️)
    validateWorkflowState-->>DeployAPI (route.ts): { valid, errors }
    alt Validation fails (incl. false positives for loops/parallels)
        DeployAPI (route.ts)-->>Panel (panel.tsx): 400 Workflow has validation errors
    else Validation passes
        DeployAPI (route.ts)->>DeployAPI (route.ts): validateWorkflowSchedules(...)
        DeployAPI (route.ts)->>DeployAPI (route.ts): deployWorkflow(...)
        DeployAPI (route.ts)-->>Panel (panel.tsx): 200 Deployed
    end
Loading

Last reviewed commit: 70dd04d

Comment on lines +360 to +367
const hasValidationErrors = useWorkflowStore((state) => {
if (Object.keys(state.blocks).length === 0) return false
const result = validateWorkflowState({
blocks: state.blocks,
edges: state.edges,
})
return !result.valid
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing loops and parallels causes false-positive validation errors

validateWorkflowState checks edges against loop and parallel container IDs (lines 270–292 of validation.ts). Because state.loops and state.parallels are not passed here, any workflow that uses a loop or parallel block will have all edges connected to those containers reported as referencing "non-existent" source/target blocks — causing hasValidationErrors to be true for valid workflows and permanently disabling the Run button.

Suggested change
const hasValidationErrors = useWorkflowStore((state) => {
if (Object.keys(state.blocks).length === 0) return false
const result = validateWorkflowState({
blocks: state.blocks,
edges: state.edges,
})
return !result.valid
})
const hasValidationErrors = useWorkflowStore((state) => {
if (Object.keys(state.blocks).length === 0) return false
const result = validateWorkflowState({
blocks: state.blocks,
edges: state.edges,
loops: state.loops,
parallels: state.parallels,
})
return !result.valid
})

Comment on lines +138 to +151
const { validateWorkflowState } = await import('@/lib/workflows/sanitization/validation')
const stateValidation = validateWorkflowState({
blocks: normalizedData.blocks,
edges: normalizedData.edges,
})
if (!stateValidation.valid) {
logger.warn(
`[${requestId}] Workflow validation failed for ${id}: ${stateValidation.errors.join('; ')}`
)
return createErrorResponse(
`Workflow has validation errors: ${stateValidation.errors.join('; ')}`,
400
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing loops and parallels blocks valid workflow deployments

normalizedData already contains loops and parallels (used in the GET handler on lines 83–86 of this same file), but they are omitted from the validateWorkflowState call here. The validation function uses workflowState.loops || {} and workflowState.parallels || {}, so passing undefined means every edge whose source or target is a loop/parallel container will be flagged as referencing a non-existent block — causing the deployment to be rejected with a 400 even for a perfectly valid workflow.

Suggested change
const { validateWorkflowState } = await import('@/lib/workflows/sanitization/validation')
const stateValidation = validateWorkflowState({
blocks: normalizedData.blocks,
edges: normalizedData.edges,
})
if (!stateValidation.valid) {
logger.warn(
`[${requestId}] Workflow validation failed for ${id}: ${stateValidation.errors.join('; ')}`
)
return createErrorResponse(
`Workflow has validation errors: ${stateValidation.errors.join('; ')}`,
400
)
}
const stateValidation = validateWorkflowState({
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
})

Comment on lines +360 to +367
const hasValidationErrors = useWorkflowStore((state) => {
if (Object.keys(state.blocks).length === 0) return false
const result = validateWorkflowState({
blocks: state.blocks,
edges: state.edges,
})
return !result.valid
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Expensive validation runs on every Zustand store update

validateWorkflowState iterates all blocks, calls getBlock() and getTool() per block, and checks all edges — all synchronously inside a Zustand selector. Selectors run on every state change, including every single keystroke in any block's input field. For larger workflows this will cause perceptible UI lag.

Consider memoising outside the selector or debouncing:

// Coarse selector — only re-validates when blocks/edges identity changes
const { blocks, edges, loops, parallels } = useWorkflowStore(
  useShallow((state) => ({
    blocks: state.blocks,
    edges: state.edges,
    loops: state.loops,
    parallels: state.parallels,
  }))
)
const hasValidationErrors = useMemo(() => {
  if (Object.keys(blocks).length === 0) return false
  return !validateWorkflowState({ blocks, edges, loops, parallels }).valid
}, [blocks, edges, loops, parallels])

This keeps the computation lazy but avoids re-running it on unrelated store updates (e.g. execution state, cursor position).

Copy link

@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 2 potential issues.

Fix All in Cursor

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

edges: state.edges,
})
return !result.valid
})
Copy link

Choose a reason for hiding this comment

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

Validation in selector mutates Zustand store state

Medium Severity

Calling validateWorkflowState inside a Zustand selector causes direct mutation of the store state. The validation function internally calls sanitizeAgentToolsInBlocks, which mutates block.subBlocks.tools.value on the original store objects (line 160 in validation.ts assigns toolsSubBlock.value = cleaned). Since the selector receives references to live store state, this silently rewrites agent tool arrays on every store update — outside of Zustand's set() — violating its immutability contract and potentially causing stale references or subtle inconsistencies in other components.

Fix in Cursor Fix in Web

edges: state.edges,
})
return !result.valid
})
Copy link

Choose a reason for hiding this comment

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

Expensive validation runs on every store update

Medium Severity

validateWorkflowState is called inside a Zustand selector, meaning it re-executes on every store state change — not just when blocks or edges change. This includes high-frequency updates like block dragging (position changes at ~60fps) and text input in sub-blocks. The function iterates all blocks, performs registry lookups, runs sanitizeAgentToolsInBlocks (which creates filtered/mapped arrays), builds Set objects from edge data, and allocates multiple temporary objects per call, creating unnecessary GC pressure and main-thread work.

Fix in Cursor Fix in Web

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.

Incomplete/broken workflows can be deployed due to hardcoded validation bypass

1 participant