Skip to content

Commit 5f21542

Browse files
committed
fix: enforce workflow validation before deploy and run
Previously, hasValidationErrors was hardcoded to false in panel.tsx, and the deploy API only validated schedule data. This allowed users to deploy completely broken/unconfigured workflows. Frontend (panel.tsx): - Replace hardcoded `false` with actual validation using validateWorkflowBlocks() that checks required sub-block fields and block connectivity - Deploy/Run buttons are now disabled when validation errors exist Backend (deploy/route.ts): - Call validateWorkflowBlocks() before deploying - Return 400 with detailed error messages when validation fails New file (lib/workflows/validation.ts): - validateWorkflowBlocks(): checks that all required sub-block fields are filled and non-trigger blocks have incoming connections - Supports conditional required fields (field-dependent validation) - Skips disabled blocks and container nodes (loops, parallels) Fixes #3444
1 parent 40ffd2f commit 5f21542

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

apps/sim/app/api/workflows/[id]/deploy/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
validateWorkflowSchedules,
2222
} from '@/lib/workflows/schedules'
2323
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
24+
import { validateWorkflowBlocks } from '@/lib/workflows/validation'
2425
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
2526
import type { WorkflowState } from '@/stores/workflows/workflow/types'
2627

@@ -134,6 +135,21 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
134135
return createErrorResponse('Failed to load workflow state', 500)
135136
}
136137

138+
// Validate workflow blocks have required fields and are connected
139+
const blockValidationErrors = validateWorkflowBlocks(
140+
normalizedData.blocks,
141+
normalizedData.edges
142+
)
143+
if (blockValidationErrors.length > 0) {
144+
logger.warn(
145+
`[${requestId}] Workflow validation failed for ${id}: ${blockValidationErrors.length} error(s)`
146+
)
147+
return createErrorResponse(
148+
`Workflow has validation errors: ${blockValidationErrors.map((e) => `${e.blockName}: ${e.message}`).join('; ')}`,
149+
400
150+
)
151+
}
152+
137153
const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks)
138154
if (!scheduleValidation.isValid) {
139155
logger.warn(

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { memo, useCallback, useEffect, useRef, useState } from 'react'
3+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { ArrowUp, Lock, Square, Unlock } from 'lucide-react'
66
import { useParams, useRouter } from 'next/navigation'
@@ -55,6 +55,7 @@ import { useVariablesStore } from '@/stores/variables/store'
5555
import { getWorkflowWithValues } from '@/stores/workflows'
5656
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
5757
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
58+
import { validateWorkflowBlocks } from '@/lib/workflows/validation'
5859

5960
const logger = createLogger('Panel')
6061
/**
@@ -356,7 +357,12 @@ export const Panel = memo(function Panel() {
356357
// Compute run button state
357358
const canRun = userPermissions.canRead // Running only requires read permissions
358359
const isLoadingPermissions = userPermissions.isLoading
359-
const hasValidationErrors = false // TODO: Add validation logic if needed
360+
361+
const blocks = useWorkflowStore((state) => state.blocks)
362+
const edges = useWorkflowStore((state) => state.edges)
363+
const validationErrors = useMemo(() => validateWorkflowBlocks(blocks, edges), [blocks, edges])
364+
const hasValidationErrors = validationErrors.length > 0
365+
360366
const isWorkflowBlocked = isExecuting || hasValidationErrors
361367
const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions))
362368

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { Edge } from 'reactflow'
2+
import { getBlock } from '@/blocks/registry'
3+
import type { SubBlockConfig } from '@/blocks/types'
4+
import type { BlockState } from '@/stores/workflows/workflow/types'
5+
6+
export interface WorkflowValidationError {
7+
blockId: string
8+
blockName: string
9+
message: string
10+
}
11+
12+
/**
13+
* Check whether a sub-block's `required` condition is satisfied given current block state.
14+
*/
15+
function isSubBlockRequired(
16+
subBlockConfig: SubBlockConfig,
17+
blockState: BlockState
18+
): boolean {
19+
const req = subBlockConfig.required
20+
if (req === undefined || req === false) return false
21+
if (req === true) return true
22+
23+
// Conditional requirement: check field value
24+
const fieldValue = blockState.subBlocks[req.field]?.value
25+
const matches = Array.isArray(req.value)
26+
? req.value.includes(fieldValue as string | number | boolean)
27+
: fieldValue === req.value
28+
const fieldSatisfied = req.not ? !matches : matches
29+
30+
if (req.and) {
31+
const andValue = blockState.subBlocks[req.and.field]?.value
32+
const andMatches = Array.isArray(req.and.value)
33+
? req.and.value.includes(andValue as string | number | boolean)
34+
: andValue === req.and.value
35+
const andSatisfied = req.and.not ? !andMatches : andMatches
36+
return fieldSatisfied && andSatisfied
37+
}
38+
39+
return fieldSatisfied
40+
}
41+
42+
/**
43+
* Validate that all required sub-block fields in a workflow are filled
44+
* and that non-trigger blocks have at least one incoming connection.
45+
*/
46+
export function validateWorkflowBlocks(
47+
blocks: Record<string, BlockState>,
48+
edges: Edge[]
49+
): WorkflowValidationError[] {
50+
const errors: WorkflowValidationError[] = []
51+
52+
// Build set of block IDs that have incoming edges
53+
const blocksWithIncoming = new Set<string>()
54+
for (const edge of edges) {
55+
blocksWithIncoming.add(edge.target)
56+
}
57+
58+
for (const [blockId, blockState] of Object.entries(blocks)) {
59+
if (!blockState.enabled) continue
60+
61+
const blockConfig = getBlock(blockState.type)
62+
if (!blockConfig) continue
63+
64+
// Skip container-type blocks (loops, parallels)
65+
if (blockState.data?.type === 'loop' || blockState.data?.type === 'parallel') continue
66+
67+
// Check required sub-block fields
68+
for (const subBlockConfig of blockConfig.subBlocks) {
69+
if (!isSubBlockRequired(subBlockConfig, blockState)) continue
70+
71+
const subBlockState = blockState.subBlocks[subBlockConfig.id]
72+
const value = subBlockState?.value
73+
const isEmpty =
74+
value === null ||
75+
value === undefined ||
76+
value === '' ||
77+
(Array.isArray(value) && value.length === 0)
78+
79+
if (isEmpty) {
80+
errors.push({
81+
blockId,
82+
blockName: blockState.name,
83+
message: `Missing required field: ${subBlockConfig.title || subBlockConfig.id}`,
84+
})
85+
}
86+
}
87+
88+
// Non-trigger blocks should have at least one incoming connection
89+
if (blockConfig.category !== 'triggers' && !blocksWithIncoming.has(blockId)) {
90+
errors.push({
91+
blockId,
92+
blockName: blockState.name,
93+
message: 'Block is not connected to any input',
94+
})
95+
}
96+
}
97+
98+
return errors
99+
}

0 commit comments

Comments
 (0)