- rows.length > 1 && !isPreview && (
-
+ rows.length > 1 &&
+ !isPreview && (
+
(blockId, subBlockId)
-
+
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const [isOpen, setIsOpen] = React.useState(false)
@@ -55,8 +54,8 @@ export function TimeInput({
// Update the time when any component changes
const updateTime = (newHour?: string, newMinute?: string, newAmpm?: 'AM' | 'PM') => {
if (isPreview) return
- const h = parseInt(newHour ?? hour) || 12
- const m = parseInt(newMinute ?? minute) || 0
+ const h = Number.parseInt(newHour ?? hour) || 12
+ const m = Number.parseInt(newMinute ?? minute) || 0
const p = newAmpm ?? ampm
setStoreValue(formatStorageTime(h, m, p))
}
@@ -91,7 +90,7 @@ export function TimeInput({
>
-
-
-
-
- No tools found.
- {toolBlocks.map((block) => (
-
- {
- if (!isPreview) {
- handleSelectTool(block)
- setOpen(false)
- }
- }}
- disabled={isPreview || isToolAlreadySelected(block.type)}
- >
-
-
-
- ))}
-
-
-
+
+
+
+ No tools found
+
+ {
if (!isPreview) {
setCustomToolModalOpen(true)
setOpen(false)
}
}}
+ className='mb-1 flex cursor-pointer items-center gap-2'
disabled={isPreview}
>
-
-
-
Create Custom Tool
+
+
-
-
-
-
+
Create Tool
+
+
+ {/* Display saved custom tools at the top */}
+ {customTools.length > 0 && (
+ <>
+
+
+ Custom Tools
+
+
+ {customTools.map((customTool) => (
+ {
+ const newTool: StoredTool = {
+ type: 'custom-tool',
+ title: customTool.title,
+ params: {},
+ isExpanded: true,
+ schema: customTool.schema,
+ code: customTool.code,
+ usageControl: 'auto',
+ }
+
+ if (isWide) {
+ setStoreValue([
+ ...selectedTools.map((tool, index) => ({
+ ...tool,
+ isExpanded:
+ Math.floor(selectedTools.length / 2) ===
+ Math.floor(index / 2),
+ })),
+ newTool,
+ ])
+ } else {
+ setStoreValue([
+ ...selectedTools.map((tool) => ({
+ ...tool,
+ isExpanded: false,
+ })),
+ newTool,
+ ])
+ }
+ setOpen(false)
+ }}
+ className='flex cursor-pointer items-center gap-2'
+ >
+
+
+
+ {customTool.title}
+
+ ))}
+
+
+ >
+ )}
+
+ {/* Display built-in tools */}
+ {toolBlocks.some((block) => customFilter(block.name, searchQuery || '') > 0) && (
+ <>
+
+ Built-in Tools
+
+
+ {toolBlocks.map((block) => (
+ handleSelectTool(block)}
+ className='flex cursor-pointer items-center gap-2'
+ >
+
+
+
+ {block.name}
+
+ ))}
+
+ >
+ )}
+
+
+
) : (
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
index 4a9e9e3ad6..65d1a89df7 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
+++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx
@@ -295,12 +295,12 @@ interface WebhookConfigProps {
}
}
-export function WebhookConfig({
- blockId,
- subBlockId,
- isConnecting,
+export function WebhookConfig({
+ blockId,
+ subBlockId,
+ isConnecting,
isPreview = false,
- value: propValue
+ value: propValue,
}: WebhookConfigProps) {
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSaving, setIsSaving] = useState(false)
@@ -323,7 +323,7 @@ export function WebhookConfig({
// Store provider-specific configuration
const [storeProviderConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
-
+
// Use prop values when available (preview mode), otherwise use store values
const webhookProvider = propValue?.webhookProvider ?? storeWebhookProvider
const webhookPath = propValue?.webhookPath ?? storeWebhookPath
@@ -570,7 +570,7 @@ export function WebhookConfig({
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels',
]}
- label="Select Gmail account"
+ label='Select Gmail account'
disabled={isConnecting || isSaving || isDeleting || isPreview}
/>
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
index 9f5bf2fa4a..e14122c2c5 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
+++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts
@@ -153,8 +153,8 @@ function storeApiKeyValue(
export function useSubBlockValue(
blockId: string,
subBlockId: string,
- triggerWorkflowUpdate: boolean = false,
- isPreview: boolean = false,
+ triggerWorkflowUpdate = false,
+ isPreview = false,
directValue?: T
): readonly [T | null, (value: T) => void] {
const blockType = useWorkflowStore(
@@ -300,4 +300,4 @@ export function useSubBlockValue(
}, [storeValue, initialValue])
return [valueRef.current as T | null, setValue] as const
-}
\ No newline at end of file
+}
diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx
index 0adcf93173..ffc4558f5f 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx
+++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx
@@ -1,10 +1,10 @@
import { Info } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
-import { useSubBlockStore } from '@/stores/workflows/subblock/store'
-import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { getBlock } from '@/blocks/index'
import type { SubBlockConfig } from '@/blocks/types'
+import { useSubBlockStore } from '@/stores/workflows/subblock/store'
+import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { CheckboxList } from './components/checkbox-list'
import { Code } from './components/code'
import { ConditionInput } from './components/condition-input'
@@ -35,12 +35,12 @@ interface SubBlockProps {
subBlockValues?: Record
}
-export function SubBlock({
- blockId,
- config,
+export function SubBlock({
+ blockId,
+ config,
isConnecting,
isPreview = false,
- subBlockValues
+ subBlockValues,
}: SubBlockProps) {
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
@@ -66,7 +66,7 @@ export function SubBlock({
const renderInput = () => {
const previewValue = getPreviewValue()
-
+
switch (config.type) {
case 'short-input':
return (
@@ -121,13 +121,15 @@ export function SubBlock({
/>
)
case 'table':
- return
+ return (
+
+ )
case 'code':
return (
)
case 'switch':
- return
+ return (
+
+ )
case 'tool-input':
- return
+ return (
+
+ )
case 'checkbox-list':
return (
)
case 'eval-input':
- return
+ return (
+
+ )
case 'date-input':
return (
-
)
- case 'webhook-config':
+ case 'webhook-config': {
// For webhook config, we need to construct the value from multiple subblock values
- const webhookValue = isPreview && subBlockValues ? {
- webhookProvider: subBlockValues['webhookProvider']?.value,
- webhookPath: subBlockValues['webhookPath']?.value,
- providerConfig: subBlockValues['providerConfig']?.value,
- } : previewValue
-
+ const webhookValue =
+ isPreview && subBlockValues
+ ? {
+ webhookProvider: subBlockValues.webhookProvider?.value,
+ webhookPath: subBlockValues.webhookPath?.value,
+ providerConfig: subBlockValues.providerConfig?.value,
+ }
+ : previewValue
+
return (
-
)
+ }
case 'schedule-config':
return (
- {
// Only allow changes in non-preview mode
if (!isPreview) {
@@ -269,36 +283,44 @@ export function SubBlock({
/>
)
case 'file-selector':
- return
+ return (
+
+ )
case 'project-selector':
- return
+ return (
+
+ )
case 'folder-selector':
- return
+ return (
+
+ )
case 'input-format':
- return
+ return (
+
+ )
default:
return Unknown input type: {config.type}
}
@@ -343,4 +365,4 @@ export function SubBlock({
{renderInput()}
)
-}
\ No newline at end of file
+}
diff --git a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
index 2ec26f026e..b50f375d75 100644
--- a/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
@@ -32,14 +32,20 @@ export function WorkflowBlock({ id, data }: NodeProps) {
// State management
const [isConnecting, setIsConnecting] = useState(false)
+
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
+ const [isLoadingScheduleInfo, setIsLoadingScheduleInfo] = useState(false)
const [scheduleInfo, setScheduleInfo] = useState<{
scheduleTiming: string
nextRunAt: string | null
lastRanAt: string | null
timezone: string
+ status?: string
+ isDisabled?: boolean
+ id?: string
} | null>(null)
+ const [isLoadingWebhookInfo, setIsLoadingWebhookInfo] = useState(false)
const [webhookInfo, setWebhookInfo] = useState<{
webhookPath: string
provider: string
@@ -59,8 +65,9 @@ export function WorkflowBlock({ id, data }: NodeProps) {
)
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
- const hasActiveSchedule = useWorkflowStore((state) => state.hasActiveSchedule ?? false)
const hasActiveWebhook = useWorkflowStore((state) => state.hasActiveWebhook ?? false)
+ const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
+ const toggleBlockAdvancedMode = useWorkflowStore((state) => state.toggleBlockAdvancedMode)
// Workflow store actions
const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
@@ -71,47 +78,97 @@ export function WorkflowBlock({ id, data }: NodeProps) {
const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id))
const isActive = dataIsActive || isActiveBlock
- // Get schedule information for the tooltip
- useEffect(() => {
- if (type === 'starter' && hasActiveSchedule) {
- const fetchScheduleInfo = async () => {
- try {
- const workflowId = useWorkflowRegistry.getState().activeWorkflowId
- if (!workflowId) return
+ const reactivateSchedule = async (scheduleId: string) => {
+ try {
+ const response = await fetch(`/api/schedules/${scheduleId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ action: 'reactivate' }),
+ })
- const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
- cache: 'no-store',
- headers: {
- 'Cache-Control': 'no-cache',
- },
- })
+ if (response.ok) {
+ fetchScheduleInfo()
+ } else {
+ console.error('Failed to reactivate schedule')
+ }
+ } catch (error) {
+ console.error('Error reactivating schedule:', error)
+ }
+ }
- if (response.ok) {
- const data = await response.json()
- if (data.schedule) {
- let scheduleTiming = 'Unknown schedule'
- if (data.schedule.cronExpression) {
- scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression)
- }
+ const fetchScheduleInfo = async () => {
+ try {
+ setIsLoadingScheduleInfo(true)
+ const workflowId = useWorkflowRegistry.getState().activeWorkflowId
+ if (!workflowId) return
- setScheduleInfo({
- scheduleTiming,
- nextRunAt: data.schedule.nextRunAt,
- lastRanAt: data.schedule.lastRanAt,
- timezone: data.schedule.timezone || 'UTC',
- })
- }
- }
- } catch (error) {
- console.error('Error fetching schedule info:', error)
- }
+ const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache',
+ },
+ })
+
+ if (!response.ok) {
+ setScheduleInfo(null)
+ return
}
- fetchScheduleInfo()
- } else if (!hasActiveSchedule) {
+ const data = await response.json()
+
+ if (!data.schedule) {
+ setScheduleInfo(null)
+ return
+ }
+
+ let scheduleTiming = 'Unknown schedule'
+ if (data.schedule.cronExpression) {
+ scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression)
+ }
+
+ const baseInfo = {
+ scheduleTiming,
+ nextRunAt: data.schedule.nextRunAt as string | null,
+ lastRanAt: data.schedule.lastRanAt as string | null,
+ timezone: data.schedule.timezone || 'UTC',
+ status: data.schedule.status as string,
+ isDisabled: data.schedule.status === 'disabled',
+ id: data.schedule.id as string,
+ }
+
+ try {
+ const statusRes = await fetch(`/api/schedules/${baseInfo.id}/status`, {
+ cache: 'no-store',
+ headers: { 'Cache-Control': 'no-cache' },
+ })
+
+ if (statusRes.ok) {
+ const statusData = await statusRes.json()
+ setScheduleInfo({
+ scheduleTiming: baseInfo.scheduleTiming,
+ nextRunAt: statusData.nextRunAt ?? baseInfo.nextRunAt,
+ lastRanAt: statusData.lastRanAt ?? baseInfo.lastRanAt,
+ timezone: baseInfo.timezone,
+ status: statusData.status ?? baseInfo.status,
+ isDisabled: statusData.isDisabled ?? baseInfo.isDisabled,
+ id: baseInfo.id,
+ })
+ return
+ }
+ } catch (err) {
+ console.error('Error fetching schedule status:', err)
+ }
+
+ setScheduleInfo(baseInfo)
+ } catch (error) {
+ console.error('Error fetching schedule info:', error)
setScheduleInfo(null)
+ } finally {
+ setIsLoadingScheduleInfo(false)
}
- }, [type, hasActiveSchedule])
+ }
// Get webhook information for the tooltip
useEffect(() => {
@@ -202,7 +259,7 @@ export function WorkflowBlock({ id, data }: NodeProps) {
// Get the appropriate state for conditional evaluation
let stateToUse: Record = {}
-
+
if (data.isPreview && data.subBlockValues) {
// In preview mode, use the preview values
stateToUse = data.subBlockValues
@@ -315,7 +372,6 @@ export function WorkflowBlock({ id, data }: NodeProps) {
// Check if this is a starter block and has active schedule or webhook
const isStarterBlock = type === 'starter'
- const showScheduleIndicator = isStarterBlock && hasActiveSchedule
const showWebhookIndicator = isStarterBlock && hasActiveWebhook
const getProviderName = (providerId: string): string => {
@@ -332,6 +388,8 @@ export function WorkflowBlock({ id, data }: NodeProps) {
return providers[providerId] || 'Webhook'
}
+ const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null
+
return (
) {
)}
{/* Schedule indicator badge - displayed for starter blocks with active schedules */}
- {showScheduleIndicator && (
+ {shouldShowScheduleBadge && (
) {
key={`${id}-${rowIndex}-${blockIndex}`}
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
>
- ) {
)
-}
\ No newline at end of file
+}
diff --git a/apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts b/apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts
index c38297be9f..8adb8dcf3c 100644
--- a/apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts
+++ b/apps/sim/app/w/[id]/hooks/use-deployment-change-detection.ts
@@ -54,7 +54,6 @@ export function useDeploymentChangeDetection(activeWorkflowId: string | null, is
// Always update the needsRedeployment flag based on API response to handle both true and false
// This ensures it's updated when changes are detected and when changes are no longer detected
if (data.needsRedeployment) {
-
// Update local state
setNeedsRedeployment(true)
@@ -66,7 +65,6 @@ export function useDeploymentChangeDetection(activeWorkflowId: string | null, is
.getState()
.getWorkflowDeploymentStatus(requestedWorkflowId)
if (currentStatus?.needsRedeployment) {
-
// Update local state
setNeedsRedeployment(false)
diff --git a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx
index 89685d113f..d319b076ed 100644
--- a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx
+++ b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx
@@ -7,27 +7,19 @@ import ReactFlow, {
ConnectionLineType,
type Edge,
type EdgeTypes,
- Handle,
type Node,
- type NodeProps,
type NodeTypes,
- Position,
ReactFlowProvider,
} from 'reactflow'
import 'reactflow/dist/style.css'
-import { Card } from '@/components/ui/card'
-import { Label } from '@/components/ui/label'
+import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { LoopTool } from '@/app/w/[id]/components/loop-node/loop-config'
-import { createLogger } from '@/lib/logs/console-logger'
import { WorkflowBlock } from '@/app/w/[id]/components/workflow-block/workflow-block'
import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edge'
-// import { LoopInput } from '@/app/w/[id]/components/workflow-loop/components/loop-input/loop-input'
-// import { LoopLabel } from '@/app/w/[id]/components/workflow-loop/components/loop-label/loop-label'
-// import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-loop'
import { getBlock } from '@/blocks'
-import { WorkflowState } from '@/stores/workflows/workflow/types'
+import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowPreview')
@@ -68,20 +60,37 @@ export function WorkflowPreview({
defaultZoom,
}: WorkflowPreviewProps) {
// Track structure changes efficiently
- const blocksStructure = useMemo(() => ({
- count: Object.keys(workflowState.blocks || {}).length,
- ids: Object.keys(workflowState.blocks || {}).join(',')
- }), [workflowState.blocks]);
-
- const loopsStructure = useMemo(() => ({
- count: Object.keys(workflowState.loops || {}).length,
- ids: Object.keys(workflowState.loops || {}).join(',')
- }), [workflowState.loops]);
-
- const edgesStructure = useMemo(() => ({
- count: workflowState.edges.length,
- ids: workflowState.edges.map(e => e.id).join(',')
- }), [workflowState.edges]);
+ const blocksStructure = useMemo(
+ () => ({
+ count: Object.keys(workflowState.blocks || {}).length,
+ ids: Object.keys(workflowState.blocks || {}).join(','),
+ }),
+ [workflowState.blocks]
+ )
+
+ const loopsStructure = useMemo(
+ () => ({
+ count: Object.keys(workflowState.loops || {}).length,
+ ids: Object.keys(workflowState.loops || {}).join(','),
+ }),
+ [workflowState.loops]
+ )
+
+ const parallelsStructure = useMemo(
+ () => ({
+ count: Object.keys(workflowState.parallels || {}).length,
+ ids: Object.keys(workflowState.parallels || {}).join(','),
+ }),
+ [workflowState.parallels]
+ )
+
+ const edgesStructure = useMemo(
+ () => ({
+ count: workflowState.edges.length,
+ ids: workflowState.edges.map((e) => e.id).join(','),
+ }),
+ [workflowState.edges]
+ )
// Transform blocks and loops into ReactFlow nodes
const nodes: Node[] = useMemo(() => {
@@ -105,18 +114,18 @@ export function WorkflowPreview({
// Add block nodes using the same approach as workflow.tsx
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
if (!block || !block.type) {
- logger.warn(`Skipping invalid block: ${blockId}`);
- return;
+ logger.warn(`Skipping invalid block: ${blockId}`)
+ return
}
-
+
const blockConfig = getBlock(block.type)
if (!blockConfig) {
logger.error(`No configuration found for block type: ${block.type}`, { blockId })
- return;
+ return
}
// Create a deep clone of subBlocks to avoid any references to the original state
- const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {};
+ const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {}
nodeArray.push({
id: blockId,
@@ -166,15 +175,22 @@ export function WorkflowPreview({
})
})
}
- logger.info(`Preview node created: ${blockId}`, {
+ logger.info(`Preview node created: ${blockId}`, {
blockType: block.type,
- hasSubBlocks: block.subBlocks && Object.keys(block.subBlocks).length > 0
- });
+ hasSubBlocks: block.subBlocks && Object.keys(block.subBlocks).length > 0,
+ })
})
-
-
+
return nodeArray
- }, [blocksStructure, loopsStructure, showSubBlocks, workflowState.blocks, workflowState.loops])
+ }, [
+ blocksStructure,
+ loopsStructure,
+ parallelsStructure,
+ showSubBlocks,
+ workflowState.blocks,
+ workflowState.loops,
+ workflowState.parallels,
+ ])
// Transform edges
const edges: Edge[] = useMemo(() => {
@@ -186,14 +202,11 @@ export function WorkflowPreview({
targetHandle: edge.targetHandle,
type: 'workflowEdge',
}))
- }, [edgesStructure, workflowState.edges])
+ }, [edgesStructure, workflowState.edges, workflowState.parallels])
return (
-
+
) : workflow.thumbnail ? (
diff --git a/apps/sim/app/w/marketplace/marketplace.tsx b/apps/sim/app/w/marketplace/marketplace.tsx
index b8dde8e252..a4a0253094 100644
--- a/apps/sim/app/w/marketplace/marketplace.tsx
+++ b/apps/sim/app/w/marketplace/marketplace.tsx
@@ -30,6 +30,7 @@ export interface Workflow {
targetHandle?: string
}>
loops: Record
+ parallels?: Record
}
}
@@ -54,6 +55,7 @@ export interface MarketplaceWorkflow {
targetHandle?: string
}>
loops: Record
+ parallels?: Record
}
}
diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts
index eee80d857e..555d40ae06 100644
--- a/apps/sim/stores/workflows/workflow/types.ts
+++ b/apps/sim/stores/workflows/workflow/types.ts
@@ -1,6 +1,6 @@
-import { Edge } from 'reactflow'
-import { BlockOutput, SubBlockType } from '@/blocks/types'
-import { DeploymentStatus } from '../registry/types'
+import type { Edge } from 'reactflow'
+import type { BlockOutput, SubBlockType } from '@/blocks/types'
+import type { DeploymentStatus } from '../registry/types'
export interface Position {
x: number
@@ -64,6 +64,12 @@ export interface Loop {
forEachItems?: any[] | Record | string // Items or expression
}
+export interface Parallel {
+ id: string
+ nodes: string[]
+ distribution?: any[] | Record | string // Items or expression
+}
+
export interface WorkflowState {
blocks: Record
edges: Edge[]