Skip to content

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

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

fix: enable workflow validation before deployment (was hardcoded to false)#3579
guoyangzhen wants to merge 5 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 new validation gates to both the UI and deploy API, which can newly block previously-allowed workflow runs/deployments and return 400s on invalid state. Risk is moderate due to behavior change in a core deployment path, though the change is localized.

Overview
Prevents invalid workflows from being run/deployed by wiring validateWorkflowState() into both the frontend panel and the backend deploy API.

The panel now computes hasValidationErrors from the current blocks/edges/loops/parallels (memoized) and disables Run/blocked states accordingly, instead of the previous hardcoded false.

The POST /api/workflows/[id]/deploy route now validates the normalized workflow state before schedule validation and rejects invalid deployments with a 400 and aggregated error message.

Written by Cursor Bugbot for commit 945b207. 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 15, 2026 2:24pm

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).

…recomputation

- Use individual selectors for blocks/edges/loops/parallels with useShallow
- Memoize validation result with useMemo, only recomputing when deps change
- Pass shallow copies of state to validateWorkflowState to prevent any
  internal mutation from affecting Zustand store state

Addresses Bugbot review feedback for simstudioai#3579
@guoyangzhen
Copy link
Author

@cursor[bot] Thanks for the review! Fixed both issues:

  1. Mutation: Added shallow copies ({ ...blocks }, [...edges]) before passing to validateWorkflowState to prevent any internal mutation from affecting Zustand store state.

  2. Performance: Replaced the inline selector with useMemo + individual selectors. Validation now only recomputes when blocks, edges, loops, or parallels actually change — not on every store update (e.g. block dragging at 60fps).

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.

parallels: { ...(parallels || {}) },
})
return !result.valid
}, [blocks, edges, loops, parallels])
Copy link

Choose a reason for hiding this comment

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

Deploy button not disabled when validation errors exist

Medium Severity

The new hasValidationErrors value only feeds into isWorkflowBlockedisButtonDisabled, which is wired to the Run button. The Deploy component doesn't receive hasValidationErrors as a prop and its own isDisabled (isDeploying || !canDeploy || isEmpty) doesn't account for validation errors. Users can still click Deploy on an invalid workflow. The backend will reject it, but the UI-layer prevention described in the PR is missing for the Deploy button.

Fix in Cursor Fix in Web

parallels: { ...(parallels || {}) },
})
return !result.valid
}, [blocks, edges, loops, parallels])
Copy link

Choose a reason for hiding this comment

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

Keyboard shortcut bypasses validation-disabled Run button

Medium Severity

The run-workflow keyboard shortcut handler (Mod+Enter) only checks isExecuting before calling runWorkflow(). It does not check hasValidationErrors or isButtonDisabled. Previously this didn't matter because hasValidationErrors was hardcoded to false. Now that it reflects real validation state, users can bypass the disabled Run button entirely by pressing the keyboard shortcut, running invalid workflows the UI is supposed to prevent.

Additional Locations (1)
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