Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions apps/sim/executor/utils/block-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
import { normalizeName } from '@/executor/constants'
import type { ExecutionContext } from '@/executor/types'
import type { OutputSchema } from '@/executor/utils/block-reference'
Expand All @@ -11,6 +12,38 @@ export interface BlockDataCollection {
blockOutputSchemas: Record<string, OutputSchema>
}

/**
* Triggers where inputFormat fields should be merged into outputs schema.
* These are blocks where users define custom fields via inputFormat that become
* valid output paths (e.g., <start.myField>, <webhook1.customField>).
*/
const TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS = [
'start_trigger',
'starter',
'api_trigger',
'input_trigger',
'generic_webhook',
'human_in_the_loop',
'approval',
] as const

function getInputFormatFields(block: SerializedBlock): OutputSchema {
const inputFormat = normalizeInputFormatValue(block.config?.params?.inputFormat)
if (inputFormat.length === 0) {
return {}
}

const schema: OutputSchema = {}
for (const field of inputFormat) {
if (!field.name) continue
schema[field.name] = {
type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any',
}
}

return schema
}

export function getBlockSchema(
block: SerializedBlock,
toolConfig?: ToolConfig
Expand All @@ -19,17 +52,31 @@ export function getBlockSchema(
block.metadata?.category === 'triggers' ||
(block.config?.params as Record<string, unknown> | undefined)?.triggerMode === true

// Triggers use saved outputs (defines the trigger payload schema)
const blockType = block.metadata?.id

if (
isTrigger &&
blockType &&
TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS.includes(
blockType as (typeof TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS)[number]
)
) {
const baseOutputs = (block.outputs as OutputSchema) || {}
const inputFormatFields = getInputFormatFields(block)
const merged = { ...baseOutputs, ...inputFormatFields }
if (Object.keys(merged).length > 0) {
return merged
}
}
Comment thread
icecrasher321 marked this conversation as resolved.

if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}

// When a tool is selected, tool outputs are the source of truth
if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) {
return toolConfig.outputs as OutputSchema
}

// Fallback to saved outputs for blocks without tools
if (block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}
Expand Down
73 changes: 65 additions & 8 deletions apps/sim/lib/workflows/comparison/compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ describe('hasWorkflowChanged', () => {
})

describe('InputFormat SubBlock Special Handling', () => {
it.concurrent('should ignore value and collapsed fields in inputFormat', () => {
it.concurrent('should ignore collapsed field but detect value changes in inputFormat', () => {
// Only collapsed changes - should NOT detect as change
const state1 = createWorkflowState({
blocks: {
block1: createBlock('block1', {
Expand All @@ -578,8 +579,8 @@ describe('hasWorkflowChanged', () => {
subBlocks: {
inputFormat: {
value: [
{ id: 'input1', name: 'Name', value: 'Jane', collapsed: false },
{ id: 'input2', name: 'Age', value: 30, collapsed: true },
{ id: 'input1', name: 'Name', value: 'John', collapsed: false },
{ id: 'input2', name: 'Age', value: 25, collapsed: true },
],
},
},
Expand All @@ -589,6 +590,32 @@ describe('hasWorkflowChanged', () => {
expect(hasWorkflowChanged(state1, state2)).toBe(false)
})

it.concurrent('should detect value changes in inputFormat', () => {
const state1 = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [{ id: 'input1', name: 'Name', value: 'John' }],
},
},
}),
},
})
const state2 = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [{ id: 'input1', name: 'Name', value: 'Jane' }],
},
},
}),
},
})
expect(hasWorkflowChanged(state1, state2)).toBe(true)
})

it.concurrent('should detect actual inputFormat changes', () => {
const state1 = createWorkflowState({
blocks: {
Expand Down Expand Up @@ -1712,15 +1739,15 @@ describe('hasWorkflowChanged', () => {
})

describe('Input Format Field Scenarios', () => {
it.concurrent('should not detect change when inputFormat value is typed and cleared', () => {
// The "value" field in inputFormat is UI-only and should be ignored
it.concurrent('should not detect change when only inputFormat collapsed changes', () => {
// The "collapsed" field in inputFormat is UI-only and should be ignored
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [
{ id: 'field1', name: 'Name', type: 'string', value: '', collapsed: false },
{ id: 'field1', name: 'Name', type: 'string', value: 'test', collapsed: false },
],
},
},
Expand All @@ -1738,7 +1765,7 @@ describe('hasWorkflowChanged', () => {
id: 'field1',
name: 'Name',
type: 'string',
value: 'typed then cleared',
value: 'test',
collapsed: true,
},
],
Expand All @@ -1748,10 +1775,40 @@ describe('hasWorkflowChanged', () => {
},
})

// value and collapsed are UI-only fields - should NOT detect as change
// collapsed is UI-only field - should NOT detect as change
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
})

it.concurrent('should detect change when inputFormat value changes', () => {
// The "value" field in inputFormat is meaningful and should trigger change detection
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [{ id: 'field1', name: 'Name', type: 'string', value: '' }],
},
},
}),
},
})

const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: {
inputFormat: {
value: [{ id: 'field1', name: 'Name', type: 'string', value: 'new value' }],
},
},
}),
},
})

// value changes should be detected
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
})

it.concurrent('should detect change when inputFormat field name changes', () => {
const deployedState = createWorkflowState({
blocks: {
Expand Down
9 changes: 5 additions & 4 deletions apps/sim/lib/workflows/comparison/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ describe('Workflow Normalization Utilities', () => {
expect(sanitizeInputFormat({} as any)).toEqual([])
})

it.concurrent('should remove value and collapsed fields', () => {
it.concurrent('should remove collapsed field but keep value', () => {
const inputFormat = [
{ id: 'input1', name: 'Name', value: 'John', collapsed: true },
{ id: 'input2', name: 'Age', value: 25, collapsed: false },
Expand All @@ -379,13 +379,13 @@ describe('Workflow Normalization Utilities', () => {
const result = sanitizeInputFormat(inputFormat)

expect(result).toEqual([
{ id: 'input1', name: 'Name' },
{ id: 'input2', name: 'Age' },
{ id: 'input1', name: 'Name', value: 'John' },
{ id: 'input2', name: 'Age', value: 25 },
{ id: 'input3', name: 'Email' },
])
})

it.concurrent('should preserve all other fields', () => {
it.concurrent('should preserve all other fields including value', () => {
const inputFormat = [
{
id: 'input1',
Expand All @@ -402,6 +402,7 @@ describe('Workflow Normalization Utilities', () => {
expect(result[0]).toEqual({
id: 'input1',
name: 'Complex Input',
value: 'test-value',
type: 'string',
required: true,
validation: { min: 0, max: 100 },
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/lib/workflows/comparison/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,18 @@ export function normalizeVariables(variables: unknown): Record<string, Variable>
}

/** Input format item with optional UI-only fields */
type InputFormatItem = Record<string, unknown> & { value?: unknown; collapsed?: boolean }
type InputFormatItem = Record<string, unknown> & { collapsed?: boolean }

/**
* Sanitizes inputFormat array by removing UI-only fields like value and collapsed
* Sanitizes inputFormat array by removing UI-only fields like collapsed
* @param inputFormat - Array of input format configurations
* @returns Sanitized input format array
*/
export function sanitizeInputFormat(inputFormat: unknown[] | undefined): Record<string, unknown>[] {
if (!Array.isArray(inputFormat)) return []
return inputFormat.map((item) => {
if (item && typeof item === 'object' && !Array.isArray(item)) {
const { value, collapsed, ...rest } = item as InputFormatItem
const { collapsed, ...rest } = item as InputFormatItem
Comment thread
icecrasher321 marked this conversation as resolved.
return rest
}
return item as Record<string, unknown>
Expand Down