Skip to content

Commit dde098e

Browse files
waleedlatif1claude
andauthored
fix: prevent raw workflowInput from overwriting coerced start block values (#3347)
buildUnifiedStartOutput and buildIntegrationTriggerOutput first populate output with schema-coerced structuredInput values (via coerceValue), then iterate workflowInput and unconditionally overwrite those keys with raw strings. This causes typed values (arrays, objects, numbers, booleans) passed to child workflows to arrive as stringified versions. Add a structuredKeys guard so the workflowInput loop skips keys already set by the coerced structuredInput, letting coerceValue's type-aware parsing (JSON.parse for objects/arrays, Number() for numbers, etc.) take effect. Fixes #3105 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5ae0115 commit dde098e

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

apps/sim/executor/utils/start-block.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,115 @@ describe('start-block utilities', () => {
215215

216216
expect(output.customField).toBe('defaultValue')
217217
})
218+
219+
it.concurrent('preserves coerced types for unified start payload', () => {
220+
const block = createBlock('start_trigger', 'start', {
221+
subBlocks: {
222+
inputFormat: {
223+
value: [
224+
{ name: 'conversation_id', type: 'number' },
225+
{ name: 'sender', type: 'object' },
226+
{ name: 'is_active', type: 'boolean' },
227+
],
228+
},
229+
},
230+
})
231+
232+
const resolution = {
233+
blockId: 'start',
234+
block,
235+
path: StartBlockPath.UNIFIED,
236+
} as const
237+
238+
const output = buildStartBlockOutput({
239+
resolution,
240+
workflowInput: {
241+
conversation_id: '149',
242+
sender: '{"id":10,"email":"user@example.com"}',
243+
is_active: 'true',
244+
},
245+
})
246+
247+
expect(output.conversation_id).toBe(149)
248+
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
249+
expect(output.is_active).toBe(true)
250+
})
251+
252+
it.concurrent(
253+
'prefers coerced inputFormat values over duplicated top-level workflowInput keys',
254+
() => {
255+
const block = createBlock('start_trigger', 'start', {
256+
subBlocks: {
257+
inputFormat: {
258+
value: [
259+
{ name: 'conversation_id', type: 'number' },
260+
{ name: 'sender', type: 'object' },
261+
{ name: 'is_active', type: 'boolean' },
262+
],
263+
},
264+
},
265+
})
266+
267+
const resolution = {
268+
blockId: 'start',
269+
block,
270+
path: StartBlockPath.UNIFIED,
271+
} as const
272+
273+
const output = buildStartBlockOutput({
274+
resolution,
275+
workflowInput: {
276+
input: {
277+
conversation_id: '149',
278+
sender: '{"id":10,"email":"user@example.com"}',
279+
is_active: 'false',
280+
},
281+
conversation_id: '150',
282+
sender: '{"id":99,"email":"wrong@example.com"}',
283+
is_active: 'true',
284+
extra: 'keep-me',
285+
},
286+
})
287+
288+
expect(output.conversation_id).toBe(149)
289+
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
290+
expect(output.is_active).toBe(false)
291+
expect(output.extra).toBe('keep-me')
292+
}
293+
)
294+
})
295+
296+
describe('EXTERNAL_TRIGGER path', () => {
297+
it.concurrent('preserves coerced types for integration trigger payload', () => {
298+
const block = createBlock('webhook', 'start', {
299+
subBlocks: {
300+
inputFormat: {
301+
value: [
302+
{ name: 'count', type: 'number' },
303+
{ name: 'payload', type: 'object' },
304+
],
305+
},
306+
},
307+
})
308+
309+
const resolution = {
310+
blockId: 'start',
311+
block,
312+
path: StartBlockPath.EXTERNAL_TRIGGER,
313+
} as const
314+
315+
const output = buildStartBlockOutput({
316+
resolution,
317+
workflowInput: {
318+
count: '5',
319+
payload: '{"event":"push"}',
320+
extra: 'untouched',
321+
},
322+
})
323+
324+
expect(output.count).toBe(5)
325+
expect(output.payload).toEqual({ event: 'push' })
326+
expect(output.extra).toBe('untouched')
327+
})
218328
})
219329
})

apps/sim/executor/utils/start-block.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ function buildUnifiedStartOutput(
262262
hasStructured: boolean
263263
): NormalizedBlockOutput {
264264
const output: NormalizedBlockOutput = {}
265+
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
265266

266267
if (hasStructured) {
267268
for (const [key, value] of Object.entries(structuredInput)) {
@@ -272,6 +273,9 @@ function buildUnifiedStartOutput(
272273
if (isPlainObject(workflowInput)) {
273274
for (const [key, value] of Object.entries(workflowInput)) {
274275
if (key === 'onUploadError') continue
276+
// Skip keys already set by schema-coerced structuredInput to
277+
// prevent raw workflowInput strings from overwriting typed values.
278+
if (structuredKeys?.has(key)) continue
275279
// Runtime values override defaults (except undefined/null which mean "not provided")
276280
if (value !== undefined && value !== null) {
277281
output[key] = value
@@ -384,6 +388,7 @@ function buildIntegrationTriggerOutput(
384388
hasStructured: boolean
385389
): NormalizedBlockOutput {
386390
const output: NormalizedBlockOutput = {}
391+
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
387392

388393
if (hasStructured) {
389394
for (const [key, value] of Object.entries(structuredInput)) {
@@ -393,6 +398,7 @@ function buildIntegrationTriggerOutput(
393398

394399
if (isPlainObject(workflowInput)) {
395400
for (const [key, value] of Object.entries(workflowInput)) {
401+
if (structuredKeys?.has(key)) continue
396402
if (value !== undefined && value !== null) {
397403
output[key] = value
398404
} else if (!Object.hasOwn(output, key)) {

0 commit comments

Comments
 (0)