diff --git a/apps/sim/executor/utils/start-block.test.ts b/apps/sim/executor/utils/start-block.test.ts index 4c6fd708e0..9a3942cbee 100644 --- a/apps/sim/executor/utils/start-block.test.ts +++ b/apps/sim/executor/utils/start-block.test.ts @@ -215,5 +215,115 @@ describe('start-block utilities', () => { expect(output.customField).toBe('defaultValue') }) + + it.concurrent('preserves coerced types for unified start payload', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'conversation_id', type: 'number' }, + { name: 'sender', type: 'object' }, + { name: 'is_active', type: 'boolean' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + conversation_id: '149', + sender: '{"id":10,"email":"user@example.com"}', + is_active: 'true', + }, + }) + + expect(output.conversation_id).toBe(149) + expect(output.sender).toEqual({ id: 10, email: 'user@example.com' }) + expect(output.is_active).toBe(true) + }) + + it.concurrent( + 'prefers coerced inputFormat values over duplicated top-level workflowInput keys', + () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'conversation_id', type: 'number' }, + { name: 'sender', type: 'object' }, + { name: 'is_active', type: 'boolean' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + input: { + conversation_id: '149', + sender: '{"id":10,"email":"user@example.com"}', + is_active: 'false', + }, + conversation_id: '150', + sender: '{"id":99,"email":"wrong@example.com"}', + is_active: 'true', + extra: 'keep-me', + }, + }) + + expect(output.conversation_id).toBe(149) + expect(output.sender).toEqual({ id: 10, email: 'user@example.com' }) + expect(output.is_active).toBe(false) + expect(output.extra).toBe('keep-me') + } + ) + }) + + describe('EXTERNAL_TRIGGER path', () => { + it.concurrent('preserves coerced types for integration trigger payload', () => { + const block = createBlock('webhook', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'count', type: 'number' }, + { name: 'payload', type: 'object' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.EXTERNAL_TRIGGER, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + count: '5', + payload: '{"event":"push"}', + extra: 'untouched', + }, + }) + + expect(output.count).toBe(5) + expect(output.payload).toEqual({ event: 'push' }) + expect(output.extra).toBe('untouched') + }) }) }) diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index 3b7982f934..5ac0936e1b 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -262,6 +262,7 @@ function buildUnifiedStartOutput( hasStructured: boolean ): NormalizedBlockOutput { const output: NormalizedBlockOutput = {} + const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null if (hasStructured) { for (const [key, value] of Object.entries(structuredInput)) { @@ -272,6 +273,9 @@ function buildUnifiedStartOutput( if (isPlainObject(workflowInput)) { for (const [key, value] of Object.entries(workflowInput)) { if (key === 'onUploadError') continue + // Skip keys already set by schema-coerced structuredInput to + // prevent raw workflowInput strings from overwriting typed values. + if (structuredKeys?.has(key)) continue // Runtime values override defaults (except undefined/null which mean "not provided") if (value !== undefined && value !== null) { output[key] = value @@ -384,6 +388,7 @@ function buildIntegrationTriggerOutput( hasStructured: boolean ): NormalizedBlockOutput { const output: NormalizedBlockOutput = {} + const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null if (hasStructured) { for (const [key, value] of Object.entries(structuredInput)) { @@ -393,6 +398,7 @@ function buildIntegrationTriggerOutput( if (isPlainObject(workflowInput)) { for (const [key, value] of Object.entries(workflowInput)) { + if (structuredKeys?.has(key)) continue if (value !== undefined && value !== null) { output[key] = value } else if (!Object.hasOwn(output, key)) {