Compare commits

..

16 Commits

Author SHA1 Message Date
Vikhyath Mondreti
78f818f7cd remove duplicate code 2026-01-20 18:32:17 -08:00
Vikhyath Mondreti
cd1c5315d6 fix tag dropdown merge conflict change 2026-01-20 18:19:55 -08:00
Vikhyath Mondreti
601f58cec9 use helper for internal route check 2026-01-20 18:11:38 -08:00
Vikhyath Mondreti
9fc6378f17 Merge remote-tracking branch 'origin/staging' into feat/tools 2026-01-20 18:00:41 -08:00
waleed
63d109de3a added description for inputs to workflow 2026-01-20 17:52:22 -08:00
waleed
c9239b55ef cleanup 2026-01-20 16:34:21 -08:00
waleed
233a3ee0b4 updated extension finder 2026-01-20 16:02:54 -08:00
waleed
c3634c2e38 updated tag dropdown to parse non-operation fields as well 2026-01-20 15:59:40 -08:00
waleed
51ed4f506d updated the rest of the old file patterns, updated mistral outputs for v2 2026-01-20 15:41:06 -08:00
waleed
59578dd140 added mistral v2, files v2, and finalized textract 2026-01-20 15:02:27 -08:00
waleed
dcaae1df7c fix additional fields dropdown in editor, update parser to leave validation to be done on the server 2026-01-20 11:51:22 -08:00
waleed
c5d3405c7a removed upload for textract async version 2026-01-20 11:44:24 -08:00
waleed
0ac6fec0a5 reorder 2026-01-20 11:31:57 -08:00
waleed
75450afb11 ack pr comments 2026-01-20 11:24:03 -08:00
waleed
dbee20e9e5 cleanup 2026-01-20 11:13:29 -08:00
waleed
ecf39c5a54 feat(tools): added textract 2026-01-20 11:06:38 -08:00
42 changed files with 153 additions and 515 deletions

View File

@@ -11,7 +11,7 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { setFormAuthCookie, validateFormAuth } from '@/app/api/form/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -36,7 +36,7 @@ async function getWorkflowInputSchema(workflowId: string): Promise<any[]> {
.from(workflowBlocks)
.where(eq(workflowBlocks.workflowId, workflowId))
const startBlock = blocks.find((block) => isInputDefinitionTrigger(block.type))
const startBlock = blocks.find((block) => isValidStartBlockType(block.type))
if (!startBlock) {
return []

View File

@@ -234,7 +234,7 @@ function ProgressBar({
{segments.map((segment, index) => (
<div
key={index}
className='absolute h-full opacity-70'
className='absolute h-full'
style={{
left: `${segment.startPercent}%`,
width: `${segment.widthPercent}%`,

View File

@@ -257,7 +257,7 @@ export const LogDetails = memo(function LogDetails({
Version
</span>
<div className='flex w-0 flex-1 justify-end'>
<span className='max-w-full truncate rounded-[6px] bg-[#bbf7d0] px-[9px] py-[2px] font-medium text-[#15803d] text-[12px] dark:bg-[#14291B] dark:text-[#86EFAC]'>
<span className='max-w-full truncate rounded-[6px] bg-[#14291B] px-[9px] py-[2px] font-medium text-[#86EFAC] text-[12px]'>
{log.deploymentVersionName || `v${log.deploymentVersion}`}
</span>
</div>

View File

@@ -19,7 +19,6 @@ import { DatePicker } from '@/components/emcn/components/date-picker/date-picker
import { cn } from '@/lib/core/utils/cn'
import { hasActiveFilters } from '@/lib/logs/filters'
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils'
import { getBlock } from '@/blocks/registry'
import { useFolderStore } from '@/stores/folders/store'
import { useFilterStore } from '@/stores/logs/filters/store'
@@ -212,12 +211,12 @@ export function LogsToolbar({
}, [level])
const statusOptions: ComboboxOption[] = useMemo(
() =>
(Object.keys(STATUS_CONFIG) as LogStatus[]).map((status) => ({
value: status,
label: STATUS_CONFIG[status].label,
icon: getColorIcon(STATUS_CONFIG[status].color),
})),
() => [
{ value: 'error', label: 'Error', icon: getColorIcon('var(--text-error)') },
{ value: 'info', label: 'Info', icon: getColorIcon('var(--terminal-status-info-color)') },
{ value: 'running', label: 'Running', icon: getColorIcon('#22c55e') },
{ value: 'pending', label: 'Pending', icon: getColorIcon('#f59e0b') },
],
[]
)
@@ -243,8 +242,12 @@ export function LogsToolbar({
const selectedStatusColor = useMemo(() => {
if (selectedStatuses.length !== 1) return null
const status = selectedStatuses[0] as LogStatus
return STATUS_CONFIG[status]?.color ?? null
const status = selectedStatuses[0]
if (status === 'error') return 'var(--text-error)'
if (status === 'info') return 'var(--terminal-status-info-color)'
if (status === 'running') return '#22c55e'
if (status === 'pending') return '#f59e0b'
return null
}, [selectedStatuses])
const workflowOptions: ComboboxOption[] = useMemo(

View File

@@ -5,6 +5,7 @@ import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { getBlock } from '@/blocks/registry'
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
/** Column configuration for logs table - shared between header and rows */
export const LOG_COLUMNS = {
date: { width: 'w-[8%]', minWidth: 'min-w-[70px]', label: 'Date' },
time: { width: 'w-[12%]', minWidth: 'min-w-[90px]', label: 'Time' },
@@ -15,8 +16,10 @@ export const LOG_COLUMNS = {
duration: { width: 'w-[20%]', minWidth: 'min-w-[100px]', label: 'Duration' },
} as const
/** Type-safe column key derived from LOG_COLUMNS */
export type LogColumnKey = keyof typeof LOG_COLUMNS
/** Ordered list of column keys for rendering table headers */
export const LOG_COLUMN_ORDER: readonly LogColumnKey[] = [
'date',
'time',
@@ -27,6 +30,7 @@ export const LOG_COLUMN_ORDER: readonly LogColumnKey[] = [
'duration',
] as const
/** Possible execution status values for workflow logs */
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
/**
@@ -49,28 +53,30 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus {
}
}
export const STATUS_CONFIG: Record<
/** Configuration mapping log status to Badge variant and display label */
const STATUS_VARIANT_MAP: Record<
LogStatus,
{ variant: React.ComponentProps<typeof Badge>['variant']; label: string; color: string }
{ variant: React.ComponentProps<typeof Badge>['variant']; label: string }
> = {
error: { variant: 'red', label: 'Error', color: 'var(--text-error)' },
pending: { variant: 'amber', label: 'Pending', color: '#f59e0b' },
running: { variant: 'green', label: 'Running', color: '#22c55e' },
cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316' },
info: { variant: 'gray', label: 'Info', color: 'var(--terminal-status-info-color)' },
error: { variant: 'red', label: 'Error' },
pending: { variant: 'amber', label: 'Pending' },
running: { variant: 'green', label: 'Running' },
cancelled: { variant: 'gray', label: 'Cancelled' },
info: { variant: 'gray', label: 'Info' },
}
/** Configuration mapping core trigger types to Badge color variants */
const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['variant']> = {
manual: 'gray-secondary',
api: 'blue',
schedule: 'green',
chat: 'purple',
webhook: 'orange',
mcp: 'cyan',
a2a: 'teal',
}
interface StatusBadgeProps {
/** The execution status to display */
status: LogStatus
}
@@ -80,13 +86,14 @@ interface StatusBadgeProps {
* @returns A Badge with dot indicator and status label
*/
export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => {
const config = STATUS_CONFIG[status]
const config = STATUS_VARIANT_MAP[status]
return React.createElement(Badge, { variant: config.variant, dot: true }, config.label)
})
StatusBadge.displayName = 'StatusBadge'
interface TriggerBadgeProps {
/** The trigger type identifier (e.g., 'manual', 'api', or integration block type) */
trigger: string
}

View File

@@ -2,7 +2,7 @@ import { memo, useCallback } from 'react'
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react'
import { Button, Copy, Tooltip, Trash2 } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -90,7 +90,7 @@ export const ActionBar = memo(
const userPermissions = useUserPermissionsContext()
const isStartBlock = isInputDefinitionTrigger(blockType)
const isStartBlock = isValidStartBlockType(blockType)
const isResponseBlock = blockType === 'response'
const isNoteBlock = blockType === 'note'
const isSubflowBlock = blockType === 'loop' || blockType === 'parallel'
@@ -142,7 +142,7 @@ export const ActionBar = memo(
</Tooltip.Root>
)}
{!isStartBlock && !isResponseBlock && (
{!isStartBlock && !isResponseBlock && !isSubflowBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
@@ -213,29 +213,6 @@ export const ActionBar = memo(
</Tooltip.Root>
)}
{isSubflowBlock && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
onClick={(e) => {
e.stopPropagation()
if (!disabled) {
collaborativeBatchToggleBlockEnabled([blockId])
}
}}
className={ACTION_BUTTON_STYLES}
disabled={disabled}
>
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
</Tooltip.Content>
</Tooltip.Root>
)}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button

View File

@@ -8,7 +8,7 @@ import {
PopoverDivider,
PopoverItem,
} from '@/components/emcn'
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
/**
* Block information for context menu actions
@@ -74,16 +74,12 @@ export function BlockMenu({
const allEnabled = selectedBlocks.every((b) => b.enabled)
const allDisabled = selectedBlocks.every((b) => !b.enabled)
const hasSingletonBlock = selectedBlocks.some(
(b) =>
TriggerUtils.requiresSingleInstance(b.type) || TriggerUtils.isSingleInstanceBlockType(b.type)
)
const hasTriggerBlock = selectedBlocks.some((b) => TriggerUtils.isTriggerBlock(b))
const hasStarterBlock = selectedBlocks.some((b) => isValidStartBlockType(b.type))
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
const isSubflow =
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')
const canRemoveFromSubflow = showRemoveFromSubflow && !hasTriggerBlock
const canRemoveFromSubflow = showRemoveFromSubflow && !hasStarterBlock
const getToggleEnabledLabel = () => {
if (allEnabled) return 'Disable'
@@ -131,7 +127,7 @@ export function BlockMenu({
<span>Paste</span>
<span className='ml-auto opacity-70 group-hover:opacity-100'>V</span>
</PopoverItem>
{!hasSingletonBlock && (
{!hasStarterBlock && (
<PopoverItem
disabled={disableEdit}
onClick={() => {

View File

@@ -26,6 +26,7 @@ export interface CanvasMenuProps {
onOpenLogs: () => void
onToggleVariables: () => void
onToggleChat: () => void
onInvite: () => void
isVariablesOpen?: boolean
isChatOpen?: boolean
hasClipboard?: boolean
@@ -54,12 +55,15 @@ export function CanvasMenu({
onOpenLogs,
onToggleVariables,
onToggleChat,
onInvite,
isVariablesOpen = false,
isChatOpen = false,
hasClipboard = false,
disableEdit = false,
disableAdmin = false,
canUndo = false,
canRedo = false,
isInvitationsDisabled = false,
}: CanvasMenuProps) {
return (
<Popover
@@ -175,6 +179,22 @@ export function CanvasMenu({
>
{isChatOpen ? 'Close Chat' : 'Open Chat'}
</PopoverItem>
{/* Admin action - hidden when invitations are disabled */}
{!isInvitationsDisabled && (
<>
<PopoverDivider />
<PopoverItem
disabled={disableAdmin}
onClick={() => {
onInvite()
onClose()
}}
>
Invite to Workspace
</PopoverItem>
</>
)}
</PopoverContent>
</Popover>
)

View File

@@ -886,16 +886,17 @@ export function Chat() {
onMouseDown={(e) => e.stopPropagation()}
>
{shouldShowConfigureStartInputsButton && (
<div
className='flex flex-none cursor-pointer items-center whitespace-nowrap rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[9px] py-[2px] font-medium font-sans text-[12px] text-[var(--text-primary)] hover:bg-[var(--surface-7)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
<Badge
variant='outline'
className='flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
title='Add chat inputs to Start block'
onMouseDown={(e) => {
e.stopPropagation()
handleConfigureStartInputs()
}}
>
<span className='whitespace-nowrap'>Add inputs</span>
</div>
<span className='whitespace-nowrap text-[12px]'>Add inputs</span>
</Badge>
)}
<OutputSelect

View File

@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
import { Check, Clipboard } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
Button,
ButtonGroup,
ButtonGroupItem,
@@ -882,13 +883,14 @@ console.log(data);`
<code className='text-[10px]'>&lt;start.files&gt;</code>.
</p>
{missingFields.any && (
<div
className='flex flex-none cursor-pointer items-center whitespace-nowrap rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[9px] py-[2px] font-medium font-sans text-[12px] text-[var(--text-primary)] hover:bg-[var(--surface-7)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
<Badge
variant='outline'
className='flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
title='Add required A2A input fields to Start block'
onClick={handleAddA2AInputs}
>
<span className='whitespace-nowrap'>Add inputs</span>
</div>
<span className='whitespace-nowrap text-[12px]'>Add inputs</span>
</Badge>
)}
</div>
</div>

View File

@@ -17,7 +17,7 @@ import { Skeleton } from '@/components/ui'
import { isDev } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import {
type FieldConfig,
useCreateForm,
@@ -147,7 +147,7 @@ export function FormDeploy({
useEffect(() => {
const blocks = Object.values(useWorkflowStore.getState().blocks)
const startBlock = blocks.find((b) => isInputDefinitionTrigger(b.type))
const startBlock = blocks.find((b) => isValidStartBlockType(b.type))
if (startBlock) {
const inputFormat = useSubBlockStore.getState().getValue(startBlock.id, 'inputFormat')

View File

@@ -14,7 +14,7 @@ import {
Textarea,
} from '@/components/emcn'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -52,7 +52,7 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
for (const [blockId, block] of Object.entries(blocks)) {
if (!block || typeof block !== 'object') continue
const blockType = (block as { type?: string }).type
if (blockType && isInputDefinitionTrigger(blockType)) {
if (blockType && isValidStartBlockType(blockType)) {
return blockId
}
}

View File

@@ -15,7 +15,7 @@ import {
import { Skeleton } from '@/components/ui'
import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
import {
useAddWorkflowMcpTool,
@@ -107,7 +107,7 @@ export function McpDeploy({
for (const [blockId, block] of Object.entries(blocks)) {
if (!block || typeof block !== 'object') continue
const blockType = (block as { type?: string }).type
if (blockType && isInputDefinitionTrigger(blockType)) {
if (blockType && isValidStartBlockType(blockType)) {
return blockId
}
}

View File

@@ -1,7 +1,6 @@
import { memo, useMemo, useRef } from 'react'
import { RepeatIcon, SplitIcon } from 'lucide-react'
import { Handle, type NodeProps, Position, useReactFlow } from 'reactflow'
import { Badge } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
import { type DiffStatus, hasDiffStatus } from '@/lib/workflows/diff/types'
@@ -79,7 +78,6 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
? currentBlock.is_diff
: undefined
const isEnabled = currentBlock?.enabled ?? true
const isPreview = data?.isPreview || false
// Focus state
@@ -186,21 +184,14 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: isEnabled ? blockIconBg : 'gray' }}
style={{ backgroundColor: blockIconBg }}
>
<BlockIcon className='h-[16px] w-[16px] text-white' />
</div>
<span
className={cn(
'truncate font-medium text-[16px]',
!isEnabled && 'text-[var(--text-muted)]'
)}
title={blockName}
>
<span className='font-medium text-[16px]' title={blockName}>
{blockName}
</span>
</div>
{!isEnabled && <Badge variant='gray-secondary'>disabled</Badge>}
</div>
{!isPreview && (

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { normalizeName } from '@/executor/constants'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
@@ -27,7 +27,7 @@ export function useAccessibleReferencePrefixes(blockId?: string | null): Set<str
const accessibleIds = new Set<string>(ancestorIds)
accessibleIds.add(blockId)
const starterBlock = Object.values(blocks).find((block) => isInputDefinitionTrigger(block.type))
const starterBlock = Object.values(blocks).find((block) => isValidStartBlockType(block.type))
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
accessibleIds.add(starterBlock.id)
}

View File

@@ -180,21 +180,6 @@ function mapEdgesByNode(edges: Edge[], nodeIds: Set<string>): Map<string, Edge[]
return result
}
/**
* Syncs the panel editor with the current selection state.
* Shows block details when exactly one block is selected, clears otherwise.
*/
function syncPanelWithSelection(selectedIds: string[]) {
const { currentBlockId, clearCurrentBlock, setCurrentBlockId } = usePanelEditorStore.getState()
if (selectedIds.length === 1 && selectedIds[0] !== currentBlockId) {
setCurrentBlockId(selectedIds[0])
} else if (selectedIds.length === 0 && currentBlockId) {
clearCurrentBlock()
} else if (selectedIds.length > 1 && currentBlockId) {
clearCurrentBlock()
}
}
/** Custom node types for ReactFlow. */
const nodeTypes: NodeTypes = {
workflowBlock: WorkflowBlock,
@@ -2090,10 +2075,7 @@ const WorkflowContent = React.memo(() => {
...node,
selected: pendingSet.has(node.id),
}))
const resolved = resolveParentChildSelectionConflicts(withSelection, blocks)
setDisplayNodes(resolved)
const selectedIds = resolved.filter((node) => node.selected).map((node) => node.id)
syncPanelWithSelection(selectedIds)
setDisplayNodes(resolveParentChildSelectionConflicts(withSelection, blocks))
return
}
@@ -2193,7 +2175,13 @@ const WorkflowContent = React.memo(() => {
})
const selectedIds = selectedIdsRef.current as string[] | null
if (selectedIds !== null) {
syncPanelWithSelection(selectedIds)
const { currentBlockId, clearCurrentBlock, setCurrentBlockId } =
usePanelEditorStore.getState()
if (selectedIds.length === 1 && selectedIds[0] !== currentBlockId) {
setCurrentBlockId(selectedIds[0])
} else if (selectedIds.length === 0 && currentBlockId) {
clearCurrentBlock()
}
}
},
[blocks]
@@ -3335,12 +3323,15 @@ const WorkflowContent = React.memo(() => {
onOpenLogs={handleContextOpenLogs}
onToggleVariables={handleContextToggleVariables}
onToggleChat={handleContextToggleChat}
onInvite={handleContextInvite}
isVariablesOpen={isVariablesOpen}
isChatOpen={isChatOpen}
hasClipboard={hasClipboard()}
disableEdit={!effectivePermissions.canEdit}
disableAdmin={!effectivePermissions.canAdmin}
canUndo={canUndo}
canRedo={canRedo}
isInvitationsDisabled={isInvitationsDisabled}
/>
</>
)}

View File

@@ -214,15 +214,6 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
],
config: {
tool: (params) => params.operation as string,
params: (params) => {
const { fileUpload, fileReference, ...rest } = params
const hasFileUpload = Array.isArray(fileUpload) ? fileUpload.length > 0 : !!fileUpload
const files = hasFileUpload ? fileUpload : fileReference
return {
...rest,
...(files ? { files } : {}),
}
},
},
},
inputs: {

View File

@@ -25,7 +25,7 @@ const badgeVariants = cva(
orange: `${STATUS_BASE} bg-[#fed7aa] text-[#c2410c] dark:bg-[rgba(249,115,22,0.2)] dark:text-[#fdba74]`,
amber: `${STATUS_BASE} bg-[#fde68a] text-[#a16207] dark:bg-[rgba(245,158,11,0.2)] dark:text-[#fcd34d]`,
teal: `${STATUS_BASE} bg-[#99f6e4] text-[#0f766e] dark:bg-[rgba(20,184,166,0.2)] dark:text-[#5eead4]`,
cyan: `${STATUS_BASE} bg-[var(--surface-4)] text-[#0891b2] dark:bg-[rgba(14,165,233,0.2)] dark:text-[#7dd3fc]`,
cyan: `${STATUS_BASE} bg-[#a5f3fc] text-[#0e7490] dark:bg-[rgba(14,165,233,0.2)] dark:text-[#7dd3fc]`,
'gray-secondary': `${STATUS_BASE} bg-[var(--surface-4)] text-[var(--text-secondary)]`,
},
size: {

View File

@@ -1,287 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>What's New at Sim</title>
</head>
<body style="margin:0;padding:0;background-color:#ffffff;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" style="background-color:#ffffff;">
<tr>
<td align="center" style="padding:0 16px;">
<table width="600" cellspacing="0" cellpadding="0" border="0" role="presentation" style="max-width:600px;width:100%;">
<!-- Logo -->
<tr>
<td align="center" style="padding-top:32px;padding-bottom:16px;">
<a href="https://sim.ai" style="color:#000;text-decoration:none;" target="_blank">
<img src="https://sim.ai/email/broadcast/v0.5/logo.png" width="79" alt="Sim Logo" style="display:block;width:79px;" border="0">
</a>
</td>
</tr>
<!-- Intro Paragraph -->
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:20px;padding-top:8px;text-align:left;">
<p style="margin:0;">We're excited to introduce <strong>Sim v0.5</strong>, the next evolution of our agent workflow platform—built for teams who need <strong>context-aware AI assistance</strong>, <strong>seamless tool deployment</strong>, and <strong>full execution observability</strong> in production.</p>
</td>
</tr>
<!-- CTA Button -->
<tr>
<td align="center" style="padding-bottom:25px;padding-top:5px;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td align="center" style="background-color:#32bd7e;border-radius:5px;">
<a href="https://sim.ai" style="display:inline-block;font-weight:500;font-size:14px;padding:7px 16px;text-decoration:none;color:#ffffff;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;" target="_blank">Try Sim</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Header Text -->
<tr>
<td align="left" valign="top" style="color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;word-break:break-word;padding-top:10px;padding-bottom:8px;text-align:left;">
<h2 style="margin:0;color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:28px;font-weight:600;word-break:break-word;">Get things done <em>faster</em></h2>
</td>
</tr>
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:30px;text-align:left;">
<p style="margin:0;">You're focused on the most critical tasks, let agents handle the rest.</p>
</td>
</tr>
<!-- FEATURE 1: Copilot -->
<tr>
<td align="left" style="color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:18px;word-break:break-word;padding-top:10px;padding-bottom:12px;text-align:left;">
<strong>Copilot</strong>
</td>
</tr>
<tr>
<td align="center" style="border-radius:8px;font-size:0;line-height:0;">
<a href="https://sim.ai" style="color:#000;text-decoration:none;" target="_blank">
<img src="https://sim.ai/email/broadcast/v0.5/copilot.jpg" width="570" alt="Sim Copilot" style="display:block;width:100%;border-radius:8px;" border="0">
</a>
</td>
</tr>
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:25px;padding-top:12px;text-align:left;">
<p style="margin:0;">Copilot is a context-aware assistant embedded directly into the Sim editor. It has first-class access to your workflows, blocks, execution logs, and documentation—helping you build, debug, and optimize without leaving the canvas. Ask it to explain a workflow, propose changes, or execute actions. All writes are gated behind explicit approval, so you stay in control.</p>
</td>
</tr>
<!-- FEATURE 2: MCP Deployment -->
<tr>
<td align="left" style="color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:18px;word-break:break-word;padding-top:20px;padding-bottom:12px;text-align:left;">
<strong>MCP Deployment</strong>
</td>
</tr>
<tr>
<td align="center">
<a href="https://sim.ai" style="text-decoration:none;display:block;" target="_blank">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" style="background-color:#181C1E;border-radius:8px;">
<tr>
<td align="center" style="padding:40px 30px;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td align="center" style="padding-bottom:16px;">
<img src="https://sim.ai/email/broadcast/v0.5/mcp.png" width="48" height="48" alt="MCP" style="display:block;" border="0">
</td>
</tr>
<tr>
<td align="center">
<p style="margin:0;color:#ffffff;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:20px;font-weight:500;">Expose workflows as tools for any agent</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</a>
</td>
</tr>
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:25px;padding-top:12px;text-align:left;">
<p style="margin:0;">Deploy any workflow as an MCP tool and expose it to any MCP-compatible agent—Claude Desktop, Cursor, or your own applications. Define custom tool names, descriptions, and parameter schemas. Your workflows become reusable building blocks that other agents can invoke, turning Sim into the backend for your agentic systems.</p>
</td>
</tr>
<!-- FEATURE 3: Logs & Dashboard -->
<tr>
<td align="left" style="color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:18px;word-break:break-word;padding-top:20px;padding-bottom:12px;text-align:left;">
<strong>Logs & Dashboard</strong>
</td>
</tr>
<tr>
<td align="center" style="border-radius:8px;font-size:0;line-height:0;">
<a href="https://sim.ai" style="color:#000;text-decoration:none;" target="_blank">
<img src="https://sim.ai/email/broadcast/v0.5/dashboard.jpg" width="570" alt="Logs & Dashboard" style="display:block;width:100%;border-radius:8px;" border="0">
</a>
</td>
</tr>
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:25px;padding-top:12px;text-align:left;">
<p style="margin:0;">Full observability for every execution. Trace spans show you exactly what happened at each step—durations, token usage, cost breakdowns by model, and error details when things go wrong. Filter by time range, trigger type, or workflow. Live mode auto-refreshes every 5 seconds. Restore any previous workflow state with execution snapshots.</p>
</td>
</tr>
<!-- FEATURE 4: Collaboration -->
<tr>
<td align="left" style="color:#2d2d2d;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:18px;word-break:break-word;padding-top:20px;padding-bottom:12px;text-align:left;">
<strong>Realtime Collaboration</strong>
</td>
</tr>
<tr>
<td align="center" style="border-radius:8px;font-size:0;line-height:0;">
<a href="https://sim.ai" style="color:#000;text-decoration:none;" target="_blank">
<img src="https://sim.ai/email/broadcast/v0.5/collaboration.jpg" width="570" alt="Realtime Collaboration" style="display:block;width:100%;border-radius:8px;" border="0">
</a>
</td>
</tr>
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:25px;padding-top:12px;text-align:left;">
<p style="margin:0;">Invite teammates to your workspace and build workflows together in realtime—like Figma for AI agents. See cursors move, blocks added, and configurations updated as they happen. The operation queue ensures reliability even under poor network conditions. Undo/redo works per-user, so your changes stay separate from your collaborators'.</p>
</td>
</tr>
<!-- Divider -->
<tr>
<td align="center" style="padding-bottom:10px;padding-top:10px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" height="1" style="border-top:1px solid #ededed;">
<tr>
<td height="0" style="font-size:0;line-height:0;">&#173;</td>
</tr>
</table>
</td>
</tr>
<!-- Final CTA -->
<tr>
<td align="left" style="color:#404040;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;font-size:16px;line-height:24px;word-break:break-word;padding-bottom:15px;padding-top:15px;text-align:left;">
<p style="margin:0;">Ready to build? These features are available now in Sim.</p>
</td>
</tr>
<tr>
<td align="center" style="padding-bottom:30px;padding-top:15px;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation">
<tr>
<td align="center" style="background-color:#32bd7e;border-radius:5px;">
<a href="https://sim.ai" style="display:inline-block;font-weight:500;font-size:14px;padding:7px 16px;text-decoration:none;color:#ffffff;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;" target="_blank">Get Started</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer Divider -->
<tr>
<td align="center" style="padding-top:20px;padding-bottom:20px;">
<table width="100%" cellspacing="0" cellpadding="0" border="0" role="presentation" height="1" style="border-top:1px solid #ededed;">
<tr>
<td height="0" style="font-size:0;line-height:0;">&#173;</td>
</tr>
</table>
</td>
</tr>
<!-- Social links row -->
<tr>
<td align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation">
<tbody>
<tr>
<td align="center" style="padding:0 8px 0 0;">
<a href="https://sim.ai/x" rel="noopener noreferrer" target="_blank">
<img src="https://sim.ai/static/x-icon.png" width="20" height="20" alt="X" style="display:block;" border="0">
</a>
</td>
<td align="center" style="padding:0 8px;">
<a href="https://sim.ai/discord" rel="noopener noreferrer" target="_blank">
<img src="https://sim.ai/static/discord-icon.png" width="20" height="20" alt="Discord" style="display:block;" border="0">
</a>
</td>
<td align="center" style="padding:0 8px;">
<a href="https://sim.ai/github" rel="noopener noreferrer" target="_blank">
<img src="https://sim.ai/static/github-icon.png" width="20" height="20" alt="GitHub" style="display:block;" border="0">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="16" style="font-size:1px;line-height:1px;">&nbsp;</td>
</tr>
<!-- Address row -->
<tr>
<td align="center" style="font-size:12px;line-height:20px;color:#737373;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;margin:0;">
Sim, 80 Langton St, San Francisco, CA 94103, USA
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="8" style="font-size:1px;line-height:1px;">&nbsp;</td>
</tr>
<!-- Contact row -->
<tr>
<td align="center" style="font-size:12px;line-height:20px;color:#737373;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;margin:0;">
Questions? <a href="mailto:support@sim.ai" style="color:#737373;text-decoration:underline;font-weight:normal;">support@sim.ai</a>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="8" style="font-size:1px;line-height:1px;">&nbsp;</td>
</tr>
<!-- Links row -->
<tr>
<td align="center" style="font-size:12px;line-height:20px;color:#737373;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;margin:0;">
<a href="https://sim.ai/privacy" style="color:#737373;text-decoration:underline;font-weight:normal;" rel="noopener noreferrer" target="_blank">Privacy Policy</a>
&nbsp;&nbsp;
<a href="https://sim.ai/terms" style="color:#737373;text-decoration:underline;font-weight:normal;" rel="noopener noreferrer" target="_blank">Terms of Service</a>
&nbsp;&nbsp;
<a href="{{{RESEND_UNSUBSCRIBE_URL}}}" style="color:#737373;text-decoration:underline;font-weight:normal;" rel="noopener noreferrer" target="_blank">Unsubscribe</a>
</td>
</tr>
<!-- Spacer -->
<tr>
<td height="16" style="font-size:1px;line-height:1px;">&nbsp;</td>
</tr>
<!-- Copyright row -->
<tr>
<td align="center" style="font-size:12px;line-height:20px;color:#737373;font-family:-apple-system,'SF Pro Display','SF Pro Text','Helvetica',sans-serif;margin:0;">
© 2026 Sim, All Rights Reserved
</td>
</tr>
<!-- Bottom spacer -->
<tr>
<td height="32" style="font-size:1px;line-height:1px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -10,7 +10,7 @@ import {
GetBlockConfigInput,
GetBlockConfigResult,
} from '@/lib/copilot/tools/shared/schemas'
import { getLatestBlock } from '@/blocks/registry'
import { getBlock } from '@/blocks/registry'
interface GetBlockConfigArgs {
blockType: string
@@ -40,7 +40,8 @@ export class GetBlockConfigClientTool extends BaseClientTool {
},
getDynamicText: (params, state) => {
if (params?.blockType && typeof params.blockType === 'string') {
const blockConfig = getLatestBlock(params.blockType)
// Look up the block config to get the human-readable name
const blockConfig = getBlock(params.blockType)
const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase()
const opSuffix = params.operation ? ` (${params.operation})` : ''

View File

@@ -10,7 +10,7 @@ import {
GetBlockOptionsInput,
GetBlockOptionsResult,
} from '@/lib/copilot/tools/shared/schemas'
import { getLatestBlock } from '@/blocks/registry'
import { getBlock } from '@/blocks/registry'
interface GetBlockOptionsArgs {
blockId: string
@@ -43,7 +43,8 @@ export class GetBlockOptionsClientTool extends BaseClientTool {
(params as any)?.block_id ||
(params as any)?.block_type
if (typeof blockId === 'string') {
const blockConfig = getLatestBlock(blockId)
// Look up the block config to get the human-readable name
const blockConfig = getBlock(blockId)
const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase()
switch (state) {

View File

@@ -17,7 +17,7 @@ import {
type GetBlockUpstreamReferencesResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
@@ -141,7 +141,7 @@ export class GetBlockUpstreamReferencesClientTool extends BaseClientTool {
const accessibleIds = new Set<string>(ancestorIds)
accessibleIds.add(blockId)
const starterBlock = Object.values(blocks).find((b) => isInputDefinitionTrigger(b.type))
const starterBlock = Object.values(blocks).find((b) => isValidStartBlockType(b.type))
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
accessibleIds.add(starterBlock.id)
}

View File

@@ -5,7 +5,7 @@ import {
GetBlockConfigResult,
type GetBlockConfigResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
import { registry as blockRegistry } from '@/blocks/registry'
import type { SubBlockConfig } from '@/blocks/types'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
@@ -452,12 +452,9 @@ export const getBlockConfigServerTool: BaseServerTool<
const inputs = extractInputsFromSubBlocks(subBlocks, operation, trigger)
const outputs = extractOutputs(blockConfig, operation, trigger)
const latestBlock = getLatestBlock(blockType)
const displayName = latestBlock?.name ?? blockConfig.name
const result = {
blockType,
blockName: displayName,
blockName: blockConfig.name,
operation,
trigger,
inputs,

View File

@@ -5,7 +5,7 @@ import {
GetBlockOptionsResult,
type GetBlockOptionsResultType,
} from '@/lib/copilot/tools/shared/schemas'
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
import { registry as blockRegistry } from '@/blocks/registry'
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
import { tools as toolsRegistry } from '@/tools/registry'
@@ -113,12 +113,9 @@ export const getBlockOptionsServerTool: BaseServerTool<
}
}
const latestBlock = getLatestBlock(blockId)
const displayName = latestBlock?.name ?? blockConfig.name
const result = {
blockId,
blockName: displayName,
blockName: blockConfig.name,
operations,
}

View File

@@ -1,7 +1,6 @@
import dns from 'dns/promises'
import http from 'http'
import https from 'https'
import type { LookupFunction } from 'net'
import { createLogger } from '@sim/logger'
import * as ipaddr from 'ipaddr.js'
@@ -908,28 +907,26 @@ export async function secureFetchWithPinnedIP(
const isIPv6 = resolvedIP.includes(':')
const family = isIPv6 ? 6 : 4
const lookup: LookupFunction = (_hostname, options, callback) => {
if (options.all) {
callback(null, [{ address: resolvedIP, family }])
} else {
const agentOptions = {
lookup: (
_hostname: string,
_options: unknown,
callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void
) => {
callback(null, resolvedIP, family)
}
},
}
const agentOptions: http.AgentOptions = { lookup }
const agent = isHttps ? new https.Agent(agentOptions) : new http.Agent(agentOptions)
// Remove accept-encoding since Node.js http/https doesn't auto-decompress
// Headers are lowercase due to Web Headers API normalization in executeToolRequest
const { 'accept-encoding': _, ...sanitizedHeaders } = options.headers ?? {}
const agent = isHttps
? new https.Agent(agentOptions as https.AgentOptions)
: new http.Agent(agentOptions as http.AgentOptions)
const requestOptions: http.RequestOptions = {
hostname: parsed.hostname,
port,
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: sanitizedHeaders,
headers: options.headers || {},
agent,
timeout: options.timeout || 30000,
}

View File

@@ -1,6 +1,6 @@
import { z } from 'zod'
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
import type { McpToolSchema } from './types'
@@ -217,7 +217,7 @@ export function extractInputFormatFromBlocks(
const blockObj = block as Record<string, unknown>
const blockType = blockObj.type as string
if (isInputDefinitionTrigger(blockType)) {
if (isValidStartBlockType(blockType)) {
// Try to get inputFormat from subBlocks.inputFormat.value
const subBlocks = blockObj.subBlocks as Record<string, { value?: unknown }> | undefined
const subBlockValue = subBlocks?.inputFormat?.value

View File

@@ -1,4 +1,4 @@
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { SYSTEM_SUBBLOCK_IDS, TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
import {
normalizedStringify,
@@ -13,20 +13,6 @@ import {
sortEdges,
} from './normalize'
/** Block with optional diff markers added by copilot */
type BlockWithDiffMarkers = BlockState & {
is_diff?: string
field_diffs?: Record<string, unknown>
}
/** SubBlock with optional diff marker */
type SubBlockWithDiffMarker = {
id: string
type: string
value: unknown
is_diff?: string
}
/**
* Compare the current workflow state with the deployed state to detect meaningful changes
* @param currentState - The current workflow state
@@ -77,32 +63,21 @@ export function hasWorkflowChanged(
// - subBlocks: handled separately below
// - layout: contains measuredWidth/measuredHeight from autolayout
// - height: block height measurement from autolayout
// - outputs: derived from subBlocks (e.g., inputFormat), already compared via subBlocks
// - is_diff, field_diffs: diff markers from copilot edits
const currentBlockWithDiff = currentBlock as BlockWithDiffMarkers
const deployedBlockWithDiff = deployedBlock as BlockWithDiffMarkers
const {
position: _currentPos,
subBlocks: currentSubBlocks = {},
layout: _currentLayout,
height: _currentHeight,
outputs: _currentOutputs,
is_diff: _currentIsDiff,
field_diffs: _currentFieldDiffs,
...currentRest
} = currentBlockWithDiff
} = currentBlock
const {
position: _deployedPos,
subBlocks: deployedSubBlocks = {},
layout: _deployedLayout,
height: _deployedHeight,
outputs: _deployedOutputs,
is_diff: _deployedIsDiff,
field_diffs: _deployedFieldDiffs,
...deployedRest
} = deployedBlockWithDiff
} = deployedBlock
// Also exclude width/height from data object (container dimensions from autolayout)
const {
@@ -181,13 +156,14 @@ export function hasWorkflowChanged(
}
}
// Compare type and other properties (excluding diff markers and value)
const currentSubBlockWithDiff = currentSubBlocks[subBlockId] as SubBlockWithDiffMarker
const deployedSubBlockWithDiff = deployedSubBlocks[subBlockId] as SubBlockWithDiffMarker
const { value: _cv, is_diff: _cd, ...currentSubBlockRest } = currentSubBlockWithDiff
const { value: _dv, is_diff: _dd, ...deployedSubBlockRest } = deployedSubBlockWithDiff
// Compare type and other properties
const currentSubBlockWithoutValue = { ...currentSubBlocks[subBlockId], value: undefined }
const deployedSubBlockWithoutValue = { ...deployedSubBlocks[subBlockId], value: undefined }
if (normalizedStringify(currentSubBlockRest) !== normalizedStringify(deployedSubBlockRest)) {
if (
normalizedStringify(currentSubBlockWithoutValue) !==
normalizedStringify(deployedSubBlockWithoutValue)
) {
return true
}
}

View File

@@ -1,4 +1,4 @@
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import type { InputFormatField } from '@/lib/workflows/types'
/**
@@ -25,7 +25,7 @@ export function extractInputFieldsFromBlocks(
// Find trigger block
const triggerEntry = Object.entries(blocks).find(([, block]) => {
const b = block as Record<string, unknown>
return typeof b.type === 'string' && isInputDefinitionTrigger(b.type)
return typeof b.type === 'string' && isValidStartBlockType(b.type)
})
if (!triggerEntry) return []

View File

@@ -376,7 +376,6 @@ describe('Database Helpers', () => {
forEachItems: '',
doWhileCondition: '',
whileCondition: '',
enabled: true,
})
expect(result?.parallels['parallel-1']).toEqual({
@@ -385,7 +384,6 @@ describe('Database Helpers', () => {
count: 5,
distribution: ['item1', 'item2'],
parallelType: 'count',
enabled: true,
})
})

View File

@@ -273,7 +273,6 @@ export async function loadWorkflowFromNormalizedTables(
forEachItems: (config as Loop).forEachItems ?? '',
whileCondition: (config as Loop).whileCondition ?? '',
doWhileCondition: (config as Loop).doWhileCondition ?? '',
enabled: migratedBlocks[subflow.id]?.enabled ?? true,
}
loops[subflow.id] = loop
@@ -302,7 +301,6 @@ export async function loadWorkflowFromNormalizedTables(
(config as Parallel).parallelType === 'collection'
? (config as Parallel).parallelType
: 'count',
enabled: migratedBlocks[subflow.id]?.enabled ?? true,
}
parallels[subflow.id] = parallel
} else {

View File

@@ -1,28 +0,0 @@
/**
* Trigger types that define workflow input parameters (inputFormat).
* These are triggers where users can configure input schema for the workflow.
*
* This module is kept lightweight with no dependencies to avoid circular imports.
*
* Note: External triggers like webhook/schedule are NOT included here because
* they receive input from external event payloads, not user-defined inputFormat.
*/
export const INPUT_DEFINITION_TRIGGER_TYPES = [
'starter',
'start',
'start_trigger',
'api_trigger',
'input_trigger',
] as const
export type InputDefinitionTriggerType = (typeof INPUT_DEFINITION_TRIGGER_TYPES)[number]
/**
* Check if a block type is a trigger that defines workflow input parameters.
* Used to find blocks that have inputFormat subblock for workflow input schema.
*/
export function isInputDefinitionTrigger(
blockType: string
): blockType is InputDefinitionTriggerType {
return INPUT_DEFINITION_TRIGGER_TYPES.includes(blockType as InputDefinitionTriggerType)
}

View File

@@ -0,0 +1,21 @@
/**
* Valid start block types that can trigger a workflow
* This module is kept lightweight with no dependencies to avoid circular imports
*/
export const VALID_START_BLOCK_TYPES = [
'starter',
'start',
'start_trigger',
'api',
'api_trigger',
'input_trigger',
] as const
export type ValidStartBlockType = (typeof VALID_START_BLOCK_TYPES)[number]
/**
* Check if a block type is a valid start block type
*/
export function isValidStartBlockType(blockType: string): blockType is ValidStartBlockType {
return VALID_START_BLOCK_TYPES.includes(blockType as ValidStartBlockType)
}

View File

@@ -1,5 +1,5 @@
import { createLogger } from '@sim/logger'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { isValidStartBlockType } from '@/lib/workflows/triggers/start-block-types'
import {
type StartBlockCandidate,
StartBlockPath,
@@ -22,7 +22,7 @@ export function hasValidStartBlockInState(state: WorkflowState | null | undefine
const startBlock = Object.values(state.blocks).find((block: BlockState) => {
const blockType = block?.type
return isInputDefinitionTrigger(blockType)
return isValidStartBlockType(blockType)
})
return !!startBlock

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -172,14 +172,7 @@ export type TimeRange =
| 'All time'
| 'Custom range'
export type LogLevel =
| 'error'
| 'info'
| 'running'
| 'pending'
| 'cancelled'
| 'all'
| (string & {})
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
/** Core trigger types for workflow execution */
export const CORE_TRIGGER_TYPES = [
'manual',

View File

@@ -130,7 +130,6 @@ export interface Loop {
forEachItems?: any[] | Record<string, any> | string // Items or expression
whileCondition?: string // JS expression that evaluates to boolean (for while loops)
doWhileCondition?: string // JS expression that evaluates to boolean (for do-while loops)
enabled: boolean
}
export interface Parallel {
@@ -139,7 +138,6 @@ export interface Parallel {
distribution?: any[] | Record<string, any> | string // Items or expression
count?: number // Number of parallel executions for count-based parallel
parallelType?: 'count' | 'collection' // Explicit parallel type to avoid inference bugs
enabled: boolean
}
export interface Variable {

View File

@@ -72,7 +72,6 @@ export function convertLoopBlockToLoop(
nodes: findChildNodes(loopBlockId, blocks),
iterations: loopBlock.data?.count || DEFAULT_LOOP_ITERATIONS,
loopType,
enabled: loopBlock.enabled,
}
loop.forEachItems = loopBlock.data?.collection || ''
@@ -114,7 +113,6 @@ export function convertParallelBlockToParallel(
distribution,
count,
parallelType: validatedParallelType,
enabled: parallelBlock.enabled,
}
}

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",