From e231febe752f64978c47c84eb98c59e405df9d30 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 23 Oct 2026 15:04:09 -0700 Subject: [PATCH] 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. --- apps/sim/executor/utils/start-block.test.ts | 2 +- apps/sim/executor/utils/start-block.ts | 3 +- .../lib/workflows/editing/validation.test.ts | 47 ++++++++----------- apps/sim/lib/workflows/editing/validation.ts | 43 +---------------- apps/sim/lib/workflows/input-format-schema.ts | 11 ++--- apps/sim/lib/workflows/input-schema.test.ts | 33 ++++++------- apps/sim/lib/workflows/input-schema.ts | 18 ++++--- .../workflows/triggers/run-options.test.ts | 5 +- 8 files changed, 51 insertions(+), 111 deletions(-) diff --git a/apps/sim/executor/utils/start-block.test.ts b/apps/sim/executor/utils/start-block.test.ts index 758cbe2116c..32b850e4ee1 100644 --- a/apps/sim/executor/utils/start-block.test.ts +++ b/apps/sim/executor/utils/start-block.test.ts @@ -454,7 +454,7 @@ describe('start-block utilities', () => { expect(output.count).toBe(5) expect(output.payload).toEqual({ event: 'push' }) - expect(output.meta).toEqual({ source: 'api' }) + expect(output.meta).toBe('{"source":"api"}') expect(output.extra).toBe('untouched') }) }) diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index c3dea3bd3ab..e2efde6d84a 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -7,7 +7,6 @@ import { isInternalFileUrl, parseInternalFileUrl, } from '@/lib/uploads/utils/file' -import { canonicalInputFormatFieldType } from '@/lib/workflows/input-format-schema' import { classifyStartBlockType, resolveStartCandidates, @@ -246,7 +245,7 @@ export function coerceValue(type: string | null | undefined, value: unknown): un return value } - switch (type ? (canonicalInputFormatFieldType(type) ?? type) : type) { + switch (type) { case 'string': return typeof value === 'string' ? value : String(value) case 'number': { diff --git a/apps/sim/lib/workflows/editing/validation.test.ts b/apps/sim/lib/workflows/editing/validation.test.ts index 88e2e942077..916a85f3190 100644 --- a/apps/sim/lib/workflows/editing/validation.test.ts +++ b/apps/sim/lib/workflows/editing/validation.test.ts @@ -345,34 +345,27 @@ describe('validateInputsForBlock', () => { ).toBe(false) }) - describe('input-format', () => { - const config = { id: 'inputFormat', type: 'input-format' as const } - const validate = (value: unknown) => - validateValueForSubBlockType(config, value, 'inputFormat', 'start_trigger', 'start-1') - - it('stores type aliases as their canonical type and keeps untyped rows', () => { - expect( - validate([ - { name: 'payload', type: 'json' }, - { name: 'documents', type: 'files' }, - { name: 'message' }, - ]) - ).toEqual({ - valid: true, - value: [ - { name: 'payload', type: 'object' }, - { name: 'documents', type: 'file[]' }, - { name: 'message' }, - ], - }) - }) - - it('rejects a type no workflow input can declare, listing the allowed types', () => { - const result = validate([{ name: 'count', type: 'integer' }]) - expect(result.valid).toBe(false) - expect(result.error?.error).toBe( - 'Invalid type "integer" for inputFormat[0] - use one of: string, number, boolean, object, array, file[]' + it('stores input-format rows with legacy or aliased types unchanged', () => { + const rows = [ + { name: 'payload', type: 'json' }, + { name: 'title', type: 'short-input' }, + { name: 'message' }, + ] + expect( + validateValueForSubBlockType( + { id: 'inputFormat', type: 'input-format' }, + rows, + 'inputFormat', + 'start_trigger', + 'start-1' ) + ).toEqual({ + valid: true, + value: [ + { name: 'payload', type: 'json' }, + { name: 'title', type: 'short-input' }, + { name: 'message' }, + ], }) }) diff --git a/apps/sim/lib/workflows/editing/validation.ts b/apps/sim/lib/workflows/editing/validation.ts index 5fcab91ce65..a816ec9f8d0 100644 --- a/apps/sim/lib/workflows/editing/validation.ts +++ b/apps/sim/lib/workflows/editing/validation.ts @@ -24,10 +24,6 @@ import { type SelectorReference, validateSelectorIds, } from '@/lib/workflows/editing/selector-validator' -import { - canonicalInputFormatFieldType, - INPUT_FORMAT_FIELD_TYPES, -} from '@/lib/workflows/input-format-schema' import { containsReference } from '@/lib/workflows/sanitization/references' import { getSkillById } from '@/lib/workflows/skills/operations' import { @@ -643,44 +639,7 @@ export function validateValueForSubBlockType( return { valid: true, value } } - case 'input-format': { - if (!Array.isArray(value)) { - return { - valid: false, - error: { - blockId, - blockType, - field: fieldName, - value, - error: `Invalid ${type} value for field "${fieldName}" - expected an array`, - }, - } - } - const rows: unknown[] = [] - for (const [index, row] of value.entries()) { - if (!isPlainRecord(row) || row.type === undefined) { - rows.push(row) - continue - } - const fieldType = - typeof row.type === 'string' ? canonicalInputFormatFieldType(row.type) : null - if (!fieldType) { - return { - valid: false, - error: { - blockId, - blockType, - field: fieldName, - value, - error: `Invalid type ${JSON.stringify(row.type)} for ${fieldName}[${index}] - use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}`, - }, - } - } - rows.push(fieldType === row.type ? row : { ...row, type: fieldType }) - } - return { valid: true, value: rows } - } - + case 'input-format': case 'table': { // Should be an array if (!Array.isArray(value)) { diff --git a/apps/sim/lib/workflows/input-format-schema.ts b/apps/sim/lib/workflows/input-format-schema.ts index 1f22315e482..3b69b674112 100644 --- a/apps/sim/lib/workflows/input-format-schema.ts +++ b/apps/sim/lib/workflows/input-format-schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod' -/** The input field types the editor offers and workflow runs accept. */ -export const INPUT_FORMAT_FIELD_TYPES = [ +/** The input field types the editor offers. */ +const INPUT_FORMAT_FIELD_TYPES = [ 'string', 'number', 'boolean', @@ -12,14 +12,9 @@ export const INPUT_FORMAT_FIELD_TYPES = [ export type InputFormatFieldType = (typeof INPUT_FORMAT_FIELD_TYPES)[number] -/** - * Every spelling a stored field's `type` may use, mapped to its canonical type: - * `json` is the name blocks use for object params and outputs, `files` the - * legacy MCP alias. - */ +/** Every canonical type by name, plus `files`, the legacy MCP alias of `file[]`. */ const INPUT_FORMAT_FIELD_TYPES_BY_NAME = new Map([ ...INPUT_FORMAT_FIELD_TYPES.map((type) => [type, type] as const), - ['json', 'object'], ['files', 'file[]'], ]) diff --git a/apps/sim/lib/workflows/input-schema.test.ts b/apps/sim/lib/workflows/input-schema.test.ts index 77a408be3b7..7e268d6c591 100644 --- a/apps/sim/lib/workflows/input-schema.test.ts +++ b/apps/sim/lib/workflows/input-schema.test.ts @@ -108,25 +108,22 @@ describe('workflow input schemas', () => { expect(z.object(generateWorkflowInputShape(fields)).parse(input)).toEqual(input) }) - it('accepts the json and files aliases as their canonical types', () => { - const shape = z.object( - generateWorkflowInputShape([ - { name: 'payload', type: 'json' }, - { name: 'documents', type: 'files' }, - ]) - ) - expect(shape.safeParse({ payload: { a: 1 }, documents: [file] }).success).toBe(true) - expect(shape.safeParse({ payload: 'text', documents: [file] }).success).toBe(false) + it('accepts the files alias as file[]', () => { + const shape = z.object(generateWorkflowInputShape([{ name: 'documents', type: 'files' }])) + expect(shape.safeParse({ documents: [file] }).success).toBe(true) + expect(shape.safeParse({ documents: 'text' }).success).toBe(false) }) - it('rejects unsupported type names as a validation failure naming the field', () => { - expect(() => generateToolInputSchema([{ name: 'count', type: 'unsupported' }])).toThrow( - expect.objectContaining({ - name: 'OrchestrationError', - code: 'validation', - message: - 'Workflow input "count" has unsupported type "unsupported". Use one of: string, number, boolean, object, array, file[].', + it.each(['short-input', 'json'])( + 'accepts any value for the legacy type %s and advertises no type for it', + (type) => { + const fields = [{ name: 'title', type }] + expect(generateToolInputSchema(fields).properties).toEqual({ + title: { description: 'title' }, }) - ) - }) + const shape = z.object(generateWorkflowInputShape(fields)) + expect(shape.safeParse({ title: 'Quarterly report' }).success).toBe(true) + expect(shape.safeParse({ title: { text: 'Quarterly report' } }).success).toBe(true) + } + ) }) diff --git a/apps/sim/lib/workflows/input-schema.ts b/apps/sim/lib/workflows/input-schema.ts index f4fed3364c0..ac7f7563e7f 100644 --- a/apps/sim/lib/workflows/input-schema.ts +++ b/apps/sim/lib/workflows/input-schema.ts @@ -3,13 +3,16 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { workflowFileInputSchema } from '@/lib/workflows/input-file-schema' import { canonicalInputFormatFieldType, - INPUT_FORMAT_FIELD_TYPES, type InputFormatFieldType, } from '@/lib/workflows/input-format-schema' import type { InputFormatField } from '@/lib/workflows/types' import { isSafeKey } from '@/tools/safe-assign' -function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType { +/** + * A stored type outside the editor's list (legacy widget names such as `short-input`) accepts any + * value, as the executor passes it through unchanged. + */ +function fieldTypeToSchema(type: InputFormatFieldType | null): z.ZodType { switch (type) { case 'string': return z.string() @@ -23,6 +26,8 @@ function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType { return z.array(z.unknown()) case 'file[]': return z.array(workflowFileInputSchema) + case null: + return z.unknown() } } @@ -30,8 +35,7 @@ function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType { * Shared by workflow run validation and MCP discovery so the advertised input * types match execution. The executor supplies configured defaults at run time. * - * @throws OrchestrationError `validation` when a field name is reserved or its - * type is not one a workflow input can declare. + * @throws OrchestrationError `validation` when a field name is reserved. */ export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.ZodRawShape { const shape: Record = {} @@ -46,12 +50,6 @@ export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.Z ) } const type = canonicalInputFormatFieldType(field.type) - if (!type) { - throw new OrchestrationError( - 'validation', - `Workflow input "${name}" has unsupported type "${field.type}". Use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}.` - ) - } const schema = fieldTypeToSchema(type).describe( field.description?.trim() || diff --git a/apps/sim/lib/workflows/triggers/run-options.test.ts b/apps/sim/lib/workflows/triggers/run-options.test.ts index 327a9d49552..3f710c44aa1 100644 --- a/apps/sim/lib/workflows/triggers/run-options.test.ts +++ b/apps/sim/lib/workflows/triggers/run-options.test.ts @@ -115,8 +115,8 @@ describe('file inputs through workflow run options', () => { }) }) -describe('mock payloads for aliased field types', () => { - it('samples a json field as an object the run validation accepts', () => { +describe('mock payloads for legacy field types', () => { + it('samples a json field with a value the run validation accepts', () => { const [option] = resolveTriggerRunOptions({ start: { type: 'start_trigger', @@ -124,7 +124,6 @@ describe('mock payloads for aliased field types', () => { subBlocks: { inputFormat: { value: [{ name: 'payload', type: 'json' }] } }, }, }) - expect(option.mockPayload).toEqual({ payload: {} }) expect(validateTriggerInput(option, option.mockPayload)).toEqual({ ok: true }) }) })