Skip to content

Commit e231feb

Browse files
committed
fix(workflows): accept legacy input field types instead of rejecting them
Stored Start inputs carry a long tail of types outside the editor's list (json, legacy widget names). Until the shared run schema began throwing on them, they ran with the executor passing values through unchanged. Keep that contract end to end: an unknown type accepts any value in run validation and advertises no type over MCP, the save path stores input-format rows unchanged again, and the executor no longer reinterprets json values, so deployed runs behave exactly as they did. files stays an alias of file[], which both sides already apply.
1 parent 60f7996 commit e231feb

8 files changed

Lines changed: 51 additions & 111 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ describe('start-block utilities', () => {
454454

455455
expect(output.count).toBe(5)
456456
expect(output.payload).toEqual({ event: 'push' })
457-
expect(output.meta).toEqual({ source: 'api' })
457+
expect(output.meta).toBe('{"source":"api"}')
458458
expect(output.extra).toBe('untouched')
459459
})
460460
})

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
isInternalFileUrl,
88
parseInternalFileUrl,
99
} from '@/lib/uploads/utils/file'
10-
import { canonicalInputFormatFieldType } from '@/lib/workflows/input-format-schema'
1110
import {
1211
classifyStartBlockType,
1312
resolveStartCandidates,
@@ -246,7 +245,7 @@ export function coerceValue(type: string | null | undefined, value: unknown): un
246245
return value
247246
}
248247

249-
switch (type ? (canonicalInputFormatFieldType(type) ?? type) : type) {
248+
switch (type) {
250249
case 'string':
251250
return typeof value === 'string' ? value : String(value)
252251
case 'number': {

‎apps/sim/lib/workflows/editing/validation.test.ts‎

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -345,34 +345,27 @@ describe('validateInputsForBlock', () => {
345345
).toBe(false)
346346
})
347347

348-
describe('input-format', () => {
349-
const config = { id: 'inputFormat', type: 'input-format' as const }
350-
const validate = (value: unknown) =>
351-
validateValueForSubBlockType(config, value, 'inputFormat', 'start_trigger', 'start-1')
352-
353-
it('stores type aliases as their canonical type and keeps untyped rows', () => {
354-
expect(
355-
validate([
356-
{ name: 'payload', type: 'json' },
357-
{ name: 'documents', type: 'files' },
358-
{ name: 'message' },
359-
])
360-
).toEqual({
361-
valid: true,
362-
value: [
363-
{ name: 'payload', type: 'object' },
364-
{ name: 'documents', type: 'file[]' },
365-
{ name: 'message' },
366-
],
367-
})
368-
})
369-
370-
it('rejects a type no workflow input can declare, listing the allowed types', () => {
371-
const result = validate([{ name: 'count', type: 'integer' }])
372-
expect(result.valid).toBe(false)
373-
expect(result.error?.error).toBe(
374-
'Invalid type "integer" for inputFormat[0] - use one of: string, number, boolean, object, array, file[]'
348+
it('stores input-format rows with legacy or aliased types unchanged', () => {
349+
const rows = [
350+
{ name: 'payload', type: 'json' },
351+
{ name: 'title', type: 'short-input' },
352+
{ name: 'message' },
353+
]
354+
expect(
355+
validateValueForSubBlockType(
356+
{ id: 'inputFormat', type: 'input-format' },
357+
rows,
358+
'inputFormat',
359+
'start_trigger',
360+
'start-1'
375361
)
362+
).toEqual({
363+
valid: true,
364+
value: [
365+
{ name: 'payload', type: 'json' },
366+
{ name: 'title', type: 'short-input' },
367+
{ name: 'message' },
368+
],
376369
})
377370
})
378371

‎apps/sim/lib/workflows/editing/validation.ts‎

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ import {
2424
type SelectorReference,
2525
validateSelectorIds,
2626
} from '@/lib/workflows/editing/selector-validator'
27-
import {
28-
canonicalInputFormatFieldType,
29-
INPUT_FORMAT_FIELD_TYPES,
30-
} from '@/lib/workflows/input-format-schema'
3127
import { containsReference } from '@/lib/workflows/sanitization/references'
3228
import { getSkillById } from '@/lib/workflows/skills/operations'
3329
import {
@@ -643,44 +639,7 @@ export function validateValueForSubBlockType(
643639
return { valid: true, value }
644640
}
645641

646-
case 'input-format': {
647-
if (!Array.isArray(value)) {
648-
return {
649-
valid: false,
650-
error: {
651-
blockId,
652-
blockType,
653-
field: fieldName,
654-
value,
655-
error: `Invalid ${type} value for field "${fieldName}" - expected an array`,
656-
},
657-
}
658-
}
659-
const rows: unknown[] = []
660-
for (const [index, row] of value.entries()) {
661-
if (!isPlainRecord(row) || row.type === undefined) {
662-
rows.push(row)
663-
continue
664-
}
665-
const fieldType =
666-
typeof row.type === 'string' ? canonicalInputFormatFieldType(row.type) : null
667-
if (!fieldType) {
668-
return {
669-
valid: false,
670-
error: {
671-
blockId,
672-
blockType,
673-
field: fieldName,
674-
value,
675-
error: `Invalid type ${JSON.stringify(row.type)} for ${fieldName}[${index}] - use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}`,
676-
},
677-
}
678-
}
679-
rows.push(fieldType === row.type ? row : { ...row, type: fieldType })
680-
}
681-
return { valid: true, value: rows }
682-
}
683-
642+
case 'input-format':
684643
case 'table': {
685644
// Should be an array
686645
if (!Array.isArray(value)) {

‎apps/sim/lib/workflows/input-format-schema.ts‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from 'zod'
22

3-
/** The input field types the editor offers and workflow runs accept. */
4-
export const INPUT_FORMAT_FIELD_TYPES = [
3+
/** The input field types the editor offers. */
4+
const INPUT_FORMAT_FIELD_TYPES = [
55
'string',
66
'number',
77
'boolean',
@@ -12,14 +12,9 @@ export const INPUT_FORMAT_FIELD_TYPES = [
1212

1313
export type InputFormatFieldType = (typeof INPUT_FORMAT_FIELD_TYPES)[number]
1414

15-
/**
16-
* Every spelling a stored field's `type` may use, mapped to its canonical type:
17-
* `json` is the name blocks use for object params and outputs, `files` the
18-
* legacy MCP alias.
19-
*/
15+
/** Every canonical type by name, plus `files`, the legacy MCP alias of `file[]`. */
2016
const INPUT_FORMAT_FIELD_TYPES_BY_NAME = new Map<string, InputFormatFieldType>([
2117
...INPUT_FORMAT_FIELD_TYPES.map((type) => [type, type] as const),
22-
['json', 'object'],
2318
['files', 'file[]'],
2419
])
2520

‎apps/sim/lib/workflows/input-schema.test.ts‎

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,22 @@ describe('workflow input schemas', () => {
108108
expect(z.object(generateWorkflowInputShape(fields)).parse(input)).toEqual(input)
109109
})
110110

111-
it('accepts the json and files aliases as their canonical types', () => {
112-
const shape = z.object(
113-
generateWorkflowInputShape([
114-
{ name: 'payload', type: 'json' },
115-
{ name: 'documents', type: 'files' },
116-
])
117-
)
118-
expect(shape.safeParse({ payload: { a: 1 }, documents: [file] }).success).toBe(true)
119-
expect(shape.safeParse({ payload: 'text', documents: [file] }).success).toBe(false)
111+
it('accepts the files alias as file[]', () => {
112+
const shape = z.object(generateWorkflowInputShape([{ name: 'documents', type: 'files' }]))
113+
expect(shape.safeParse({ documents: [file] }).success).toBe(true)
114+
expect(shape.safeParse({ documents: 'text' }).success).toBe(false)
120115
})
121116

122-
it('rejects unsupported type names as a validation failure naming the field', () => {
123-
expect(() => generateToolInputSchema([{ name: 'count', type: 'unsupported' }])).toThrow(
124-
expect.objectContaining({
125-
name: 'OrchestrationError',
126-
code: 'validation',
127-
message:
128-
'Workflow input "count" has unsupported type "unsupported". Use one of: string, number, boolean, object, array, file[].',
117+
it.each(['short-input', 'json'])(
118+
'accepts any value for the legacy type %s and advertises no type for it',
119+
(type) => {
120+
const fields = [{ name: 'title', type }]
121+
expect(generateToolInputSchema(fields).properties).toEqual({
122+
title: { description: 'title' },
129123
})
130-
)
131-
})
124+
const shape = z.object(generateWorkflowInputShape(fields))
125+
expect(shape.safeParse({ title: 'Quarterly report' }).success).toBe(true)
126+
expect(shape.safeParse({ title: { text: 'Quarterly report' } }).success).toBe(true)
127+
}
128+
)
132129
})

‎apps/sim/lib/workflows/input-schema.ts‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
33
import { workflowFileInputSchema } from '@/lib/workflows/input-file-schema'
44
import {
55
canonicalInputFormatFieldType,
6-
INPUT_FORMAT_FIELD_TYPES,
76
type InputFormatFieldType,
87
} from '@/lib/workflows/input-format-schema'
98
import type { InputFormatField } from '@/lib/workflows/types'
109
import { isSafeKey } from '@/tools/safe-assign'
1110

12-
function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType {
11+
/**
12+
* A stored type outside the editor's list (legacy widget names such as `short-input`) accepts any
13+
* value, as the executor passes it through unchanged.
14+
*/
15+
function fieldTypeToSchema(type: InputFormatFieldType | null): z.ZodType {
1316
switch (type) {
1417
case 'string':
1518
return z.string()
@@ -23,15 +26,16 @@ function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType {
2326
return z.array(z.unknown())
2427
case 'file[]':
2528
return z.array(workflowFileInputSchema)
29+
case null:
30+
return z.unknown()
2631
}
2732
}
2833

2934
/**
3035
* Shared by workflow run validation and MCP discovery so the advertised input
3136
* types match execution. The executor supplies configured defaults at run time.
3237
*
33-
* @throws OrchestrationError `validation` when a field name is reserved or its
34-
* type is not one a workflow input can declare.
38+
* @throws OrchestrationError `validation` when a field name is reserved.
3539
*/
3640
export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.ZodRawShape {
3741
const shape: Record<string, z.ZodType> = {}
@@ -46,12 +50,6 @@ export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.Z
4650
)
4751
}
4852
const type = canonicalInputFormatFieldType(field.type)
49-
if (!type) {
50-
throw new OrchestrationError(
51-
'validation',
52-
`Workflow input "${name}" has unsupported type "${field.type}". Use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}.`
53-
)
54-
}
5553

5654
const schema = fieldTypeToSchema(type).describe(
5755
field.description?.trim() ||

‎apps/sim/lib/workflows/triggers/run-options.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,15 @@ describe('file inputs through workflow run options', () => {
115115
})
116116
})
117117

118-
describe('mock payloads for aliased field types', () => {
119-
it('samples a json field as an object the run validation accepts', () => {
118+
describe('mock payloads for legacy field types', () => {
119+
it('samples a json field with a value the run validation accepts', () => {
120120
const [option] = resolveTriggerRunOptions({
121121
start: {
122122
type: 'start_trigger',
123123
name: 'Start',
124124
subBlocks: { inputFormat: { value: [{ name: 'payload', type: 'json' }] } },
125125
},
126126
})
127-
expect(option.mockPayload).toEqual({ payload: {} })
128127
expect(validateTriggerInput(option, option.mockPayload)).toEqual({ ok: true })
129128
})
130129
})

0 commit comments

Comments
 (0)