From aca7005e8a1b77cf96fce6737a8f20425380b11d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 10:45:29 -0800 Subject: [PATCH 1/7] fix(resolver): consolidate code to resolve references --- .../app/api/function/execute/route.test.ts | 4 +- apps/sim/app/api/function/execute/route.ts | 73 +++-- .../executor/handlers/agent/agent-handler.ts | 3 +- .../handlers/condition/condition-handler.ts | 3 +- .../function/function-handler.test.ts | 29 +- .../handlers/function/function-handler.ts | 3 +- apps/sim/executor/utils/block-data.ts | 32 ++- .../executor/utils/block-reference.test.ts | 255 +++++++++++++++++ apps/sim/executor/utils/block-reference.ts | 176 ++++++++++++ .../sim/executor/variables/resolvers/block.ts | 256 +++++------------- apps/sim/lib/execution/isolated-vm-worker.cjs | 6 +- apps/sim/tools/function/execute.test.ts | 3 + apps/sim/tools/function/execute.ts | 8 + apps/sim/tools/function/types.ts | 1 + 14 files changed, 621 insertions(+), 231 deletions(-) create mode 100644 apps/sim/executor/utils/block-reference.test.ts create mode 100644 apps/sim/executor/utils/block-reference.ts diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index ea020abaf5..e0e7723bd4 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -313,7 +313,7 @@ describe('Function Execute API Route', () => { 'block-2': 'world', }, blockNameMapping: { - validVar: 'block-1', + validvar: 'block-1', another_valid: 'block-2', }, }) @@ -539,7 +539,7 @@ describe('Function Execute API Route', () => { 'block-complex': complexData, }, blockNameMapping: { - complexData: 'block-complex', + complexdata: 'block-complex', }, }) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 8868c2d404..193663f43a 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -6,11 +6,11 @@ import { executeInE2B } from '@/lib/execution/e2b' import { executeInIsolatedVM } from '@/lib/execution/isolated-vm' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants' +import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference' import { createEnvVarPattern, createWorkflowVariablePattern, } from '@/executor/utils/reference-validation' -import { navigatePath } from '@/executor/variables/resolvers/reference' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' @@ -470,11 +470,14 @@ function resolveEnvironmentVariables( function resolveTagVariables( code: string, - blockData: Record, + blockData: Record, blockNameMapping: Record, - contextVariables: Record + blockOutputSchemas: Record, + contextVariables: Record, + language = 'javascript' ): string { let resolvedCode = code + const undefinedLiteral = language === 'python' ? 'None' : 'undefined' const tagPattern = new RegExp( `${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`, @@ -486,25 +489,22 @@ function resolveTagVariables( const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim() const pathParts = tagName.split(REFERENCE.PATH_DELIMITER) const blockName = pathParts[0] + const fieldPath = pathParts.slice(1) - const blockId = blockNameMapping[blockName] - if (!blockId) { - continue - } + const result = resolveBlockReference(blockName, fieldPath, { + blockNameMapping, + blockData, + blockOutputSchemas, + }) - const blockOutput = blockData[blockId] - if (blockOutput === undefined) { + if (!result) { continue } - let tagValue: any - if (pathParts.length === 1) { - tagValue = blockOutput - } else { - tagValue = navigatePath(blockOutput, pathParts.slice(1)) - } + let tagValue = result.value if (tagValue === undefined) { + resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral) continue } @@ -537,18 +537,27 @@ function resolveTagVariables( */ function resolveCodeVariables( code: string, - params: Record, + params: Record, envVars: Record = {}, - blockData: Record = {}, + blockData: Record = {}, blockNameMapping: Record = {}, - workflowVariables: Record = {} -): { resolvedCode: string; contextVariables: Record } { + blockOutputSchemas: Record = {}, + workflowVariables: Record = {}, + language = 'javascript' +): { resolvedCode: string; contextVariables: Record } { let resolvedCode = code - const contextVariables: Record = {} + const contextVariables: Record = {} resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables) resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables) - resolvedCode = resolveTagVariables(resolvedCode, blockData, blockNameMapping, contextVariables) + resolvedCode = resolveTagVariables( + resolvedCode, + blockData, + blockNameMapping, + blockOutputSchemas, + contextVariables, + language + ) return { resolvedCode, contextVariables } } @@ -585,6 +594,7 @@ export async function POST(req: NextRequest) { envVars = {}, blockData = {}, blockNameMapping = {}, + blockOutputSchemas = {}, workflowVariables = {}, workflowId, isCustomTool = false, @@ -601,20 +611,21 @@ export async function POST(req: NextRequest) { isCustomTool, }) - // Resolve variables in the code with workflow environment variables + const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE + const codeResolution = resolveCodeVariables( code, executionParams, envVars, blockData, blockNameMapping, - workflowVariables + blockOutputSchemas, + workflowVariables, + lang ) resolvedCode = codeResolution.resolvedCode const contextVariables = codeResolution.contextVariables - const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE - let jsImports = '' let jsRemainingCode = resolvedCode let hasImports = false @@ -670,7 +681,11 @@ export async function POST(req: NextRequest) { prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n` + if (v === undefined) { + prologue += `const ${k} = undefined;\n` + } else { + prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n` + } prologueLineCount++ } @@ -741,7 +756,11 @@ export async function POST(req: NextRequest) { prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n` + if (v === undefined) { + prologue += `${k} = None\n` + } else { + prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n` + } prologueLineCount++ } const wrapped = [ diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 3240da8974..9cbd6692ac 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -305,7 +305,7 @@ export class AgentBlockHandler implements BlockHandler { base.executeFunction = async (callParams: Record) => { const mergedParams = mergeToolParameters(userProvidedParams, callParams) - const { blockData, blockNameMapping } = collectBlockData(ctx) + const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) const result = await executeTool( 'function_execute', @@ -317,6 +317,7 @@ export class AgentBlockHandler implements BlockHandler { workflowVariables: ctx.workflowVariables || {}, blockData, blockNameMapping, + blockOutputSchemas, isCustomTool: true, _context: { workflowId: ctx.workflowId, diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index f450460589..2954b06d89 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -26,7 +26,7 @@ export async function evaluateConditionExpression( const contextSetup = `const context = ${JSON.stringify(evalContext)};` const code = `${contextSetup}\nreturn Boolean(${conditionExpression})` - const { blockData, blockNameMapping } = collectBlockData(ctx) + const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) const result = await executeTool( 'function_execute', @@ -37,6 +37,7 @@ export async function evaluateConditionExpression( workflowVariables: ctx.workflowVariables || {}, blockData, blockNameMapping, + blockOutputSchemas, _context: { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index f04de4662b..5426610c70 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -75,7 +75,12 @@ describe('FunctionBlockHandler', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, - _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId }, + blockOutputSchemas: {}, + _context: { + workflowId: mockContext.workflowId, + workspaceId: mockContext.workspaceId, + isDeployedContext: mockContext.isDeployedContext, + }, } const expectedOutput: any = { result: 'Success' } @@ -84,8 +89,8 @@ describe('FunctionBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalledWith( 'function_execute', expectedToolParams, - false, // skipPostProcess - mockContext // execution context + false, + mockContext ) expect(result).toEqual(expectedOutput) }) @@ -107,7 +112,12 @@ describe('FunctionBlockHandler', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, - _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId }, + blockOutputSchemas: {}, + _context: { + workflowId: mockContext.workflowId, + workspaceId: mockContext.workspaceId, + isDeployedContext: mockContext.isDeployedContext, + }, } const expectedOutput: any = { result: 'Success' } @@ -116,8 +126,8 @@ describe('FunctionBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalledWith( 'function_execute', expectedToolParams, - false, // skipPostProcess - mockContext // execution context + false, + mockContext ) expect(result).toEqual(expectedOutput) }) @@ -132,7 +142,12 @@ describe('FunctionBlockHandler', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, - _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId }, + blockOutputSchemas: {}, + _context: { + workflowId: mockContext.workflowId, + workspaceId: mockContext.workspaceId, + isDeployedContext: mockContext.isDeployedContext, + }, } await handler.execute(mockContext, mockBlock, inputs) diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index c7b9b00978..624a262d3a 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -23,7 +23,7 @@ export class FunctionBlockHandler implements BlockHandler { ? inputs.code.map((c: { content: string }) => c.content).join('\n') : inputs.code - const { blockData, blockNameMapping } = collectBlockData(ctx) + const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) const result = await executeTool( 'function_execute', @@ -35,6 +35,7 @@ export class FunctionBlockHandler implements BlockHandler { workflowVariables: ctx.workflowVariables || {}, blockData, blockNameMapping, + blockOutputSchemas, _context: { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index fc7b26ae32..ab783c6976 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,24 +1,44 @@ +import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' +import type { OutputSchema } from '@/executor/utils/block-reference' export interface BlockDataCollection { - blockData: Record + blockData: Record blockNameMapping: Record + blockOutputSchemas: Record } export function collectBlockData(ctx: ExecutionContext): BlockDataCollection { - const blockData: Record = {} + const blockData: Record = {} const blockNameMapping: Record = {} + const blockOutputSchemas: Record = {} for (const [id, state] of ctx.blockStates.entries()) { if (state.output !== undefined) { blockData[id] = state.output - const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id) - if (workflowBlock?.metadata?.name) { - blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id + } + + const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id) + if (!workflowBlock) continue + + if (workflowBlock.metadata?.name) { + blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id + } + + // Build output schema from block type and params + const blockType = workflowBlock.metadata?.id + if (blockType) { + const params = workflowBlock.config?.params as Record | undefined + const subBlocks = params + ? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }])) + : undefined + const schema = getBlockOutputs(blockType, subBlocks) + if (schema && Object.keys(schema).length > 0) { + blockOutputSchemas[id] = schema } } } - return { blockData, blockNameMapping } + return { blockData, blockNameMapping, blockOutputSchemas } } diff --git a/apps/sim/executor/utils/block-reference.test.ts b/apps/sim/executor/utils/block-reference.test.ts new file mode 100644 index 0000000000..6f110c2bc6 --- /dev/null +++ b/apps/sim/executor/utils/block-reference.test.ts @@ -0,0 +1,255 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + type BlockReferenceContext, + InvalidFieldError, + resolveBlockReference, +} from './block-reference' + +describe('resolveBlockReference', () => { + const createContext = ( + overrides: Partial = {} + ): BlockReferenceContext => ({ + blockNameMapping: { start: 'block-1', agent: 'block-2' }, + blockData: {}, + blockOutputSchemas: {}, + ...overrides, + }) + + describe('block name resolution', () => { + it('should return undefined when block name does not exist', () => { + const ctx = createContext() + const result = resolveBlockReference('unknown', ['field'], ctx) + expect(result).toBeUndefined() + }) + + it('should normalize block name before lookup', () => { + const ctx = createContext({ + blockNameMapping: { myblock: 'block-1' }, + blockData: { 'block-1': { value: 'test' } }, + }) + + const result = resolveBlockReference('MyBlock', ['value'], ctx) + expect(result).toEqual({ value: 'test', blockId: 'block-1' }) + }) + + it('should handle block names with spaces', () => { + const ctx = createContext({ + blockNameMapping: { myblock: 'block-1' }, + blockData: { 'block-1': { value: 'test' } }, + }) + + const result = resolveBlockReference('My Block', ['value'], ctx) + expect(result).toEqual({ value: 'test', blockId: 'block-1' }) + }) + }) + + describe('field resolution', () => { + it('should return entire block output when no path specified', () => { + const ctx = createContext({ + blockData: { 'block-1': { input: 'hello', other: 'data' } }, + }) + + const result = resolveBlockReference('start', [], ctx) + expect(result).toEqual({ + value: { input: 'hello', other: 'data' }, + blockId: 'block-1', + }) + }) + + it('should resolve simple field path', () => { + const ctx = createContext({ + blockData: { 'block-1': { input: 'hello' } }, + }) + + const result = resolveBlockReference('start', ['input'], ctx) + expect(result).toEqual({ value: 'hello', blockId: 'block-1' }) + }) + + it('should resolve nested field path', () => { + const ctx = createContext({ + blockData: { 'block-1': { response: { data: { name: 'test' } } } }, + }) + + const result = resolveBlockReference('start', ['response', 'data', 'name'], ctx) + expect(result).toEqual({ value: 'test', blockId: 'block-1' }) + }) + + it('should resolve array index path', () => { + const ctx = createContext({ + blockData: { 'block-1': { items: ['a', 'b', 'c'] } }, + }) + + const result = resolveBlockReference('start', ['items', '1'], ctx) + expect(result).toEqual({ value: 'b', blockId: 'block-1' }) + }) + + it('should return undefined value when field exists but has no value', () => { + const ctx = createContext({ + blockData: { 'block-1': { input: undefined } }, + blockOutputSchemas: { + 'block-1': { input: { type: 'string' } }, + }, + }) + + const result = resolveBlockReference('start', ['input'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + + it('should return null value when field has null', () => { + const ctx = createContext({ + blockData: { 'block-1': { input: null } }, + }) + + const result = resolveBlockReference('start', ['input'], ctx) + expect(result).toEqual({ value: null, blockId: 'block-1' }) + }) + }) + + describe('schema validation', () => { + it('should throw InvalidFieldError when field not in schema', () => { + const ctx = createContext({ + blockData: { 'block-1': { existing: 'value' } }, + blockOutputSchemas: { + 'block-1': { + input: { type: 'string' }, + conversationId: { type: 'string' }, + }, + }, + }) + + expect(() => resolveBlockReference('start', ['invalid'], ctx)).toThrow(InvalidFieldError) + expect(() => resolveBlockReference('start', ['invalid'], ctx)).toThrow( + /"invalid" doesn't exist on block "start"/ + ) + }) + + it('should include available fields in error message', () => { + const ctx = createContext({ + blockData: { 'block-1': {} }, + blockOutputSchemas: { + 'block-1': { + input: { type: 'string' }, + conversationId: { type: 'string' }, + files: { type: 'files' }, + }, + }, + }) + + try { + resolveBlockReference('start', ['typo'], ctx) + expect.fail('Should have thrown') + } catch (error) { + expect(error).toBeInstanceOf(InvalidFieldError) + const fieldError = error as InvalidFieldError + expect(fieldError.availableFields).toContain('input') + expect(fieldError.availableFields).toContain('conversationId') + expect(fieldError.availableFields).toContain('files') + } + }) + + it('should allow valid field even when value is undefined', () => { + const ctx = createContext({ + blockData: { 'block-1': {} }, + blockOutputSchemas: { + 'block-1': { input: { type: 'string' } }, + }, + }) + + const result = resolveBlockReference('start', ['input'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + + it('should validate path when block has no output yet', () => { + const ctx = createContext({ + blockData: {}, + blockOutputSchemas: { + 'block-1': { input: { type: 'string' } }, + }, + }) + + expect(() => resolveBlockReference('start', ['invalid'], ctx)).toThrow(InvalidFieldError) + }) + + it('should return undefined for valid field when block has no output', () => { + const ctx = createContext({ + blockData: {}, + blockOutputSchemas: { + 'block-1': { input: { type: 'string' } }, + }, + }) + + const result = resolveBlockReference('start', ['input'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + }) + + describe('without schema (pass-through mode)', () => { + it('should return undefined value without throwing when no schema', () => { + const ctx = createContext({ + blockData: { 'block-1': { existing: 'value' } }, + }) + + const result = resolveBlockReference('start', ['missing'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + }) + + describe('file type handling', () => { + it('should allow file property access', () => { + const ctx = createContext({ + blockData: { + 'block-1': { + files: [{ name: 'test.txt', url: 'http://example.com/file' }], + }, + }, + blockOutputSchemas: { + 'block-1': { files: { type: 'files' } }, + }, + }) + + const result = resolveBlockReference('start', ['files', '0', 'name'], ctx) + expect(result).toEqual({ value: 'test.txt', blockId: 'block-1' }) + }) + + it('should validate file property names', () => { + const ctx = createContext({ + blockData: { 'block-1': { files: [] } }, + blockOutputSchemas: { + 'block-1': { files: { type: 'files' } }, + }, + }) + + expect(() => resolveBlockReference('start', ['files', '0', 'invalid'], ctx)).toThrow( + InvalidFieldError + ) + }) + }) +}) + +describe('InvalidFieldError', () => { + it('should have correct properties', () => { + const error = new InvalidFieldError('myBlock', 'invalid.path', ['field1', 'field2']) + + expect(error.blockName).toBe('myBlock') + expect(error.fieldPath).toBe('invalid.path') + expect(error.availableFields).toEqual(['field1', 'field2']) + expect(error.name).toBe('InvalidFieldError') + }) + + it('should format message correctly', () => { + const error = new InvalidFieldError('start', 'typo', ['input', 'files']) + + expect(error.message).toBe( + '"typo" doesn\'t exist on block "start". Available fields: input, files' + ) + }) + + it('should handle empty available fields', () => { + const error = new InvalidFieldError('start', 'field', []) + + expect(error.message).toBe('"field" doesn\'t exist on block "start". Available fields: none') + }) +}) diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts new file mode 100644 index 0000000000..254ec4853b --- /dev/null +++ b/apps/sim/executor/utils/block-reference.ts @@ -0,0 +1,176 @@ +import { normalizeName } from '@/executor/constants' +import { navigatePath } from '@/executor/variables/resolvers/reference' + +export type OutputSchema = Record + +export interface BlockReferenceContext { + blockNameMapping: Record + blockData: Record + blockOutputSchemas?: Record +} + +export interface BlockReferenceResult { + value: unknown + blockId: string +} + +export class InvalidFieldError extends Error { + constructor( + public readonly blockName: string, + public readonly fieldPath: string, + public readonly availableFields: string[] + ) { + super( + `"${fieldPath}" doesn't exist on block "${blockName}". ` + + `Available fields: ${availableFields.length > 0 ? availableFields.join(', ') : 'none'}` + ) + this.name = 'InvalidFieldError' + } +} + +function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): boolean { + if (!schema || pathParts.length === 0) { + return true + } + + const FILE_PROPERTIES = ['name', 'type', 'size', 'url', 'base64', 'mimeType'] + const isFileType = (value: unknown): boolean => { + if (typeof value !== 'object' || value === null) return false + const typed = value as { type?: string } + return typed.type === 'file[]' || typed.type === 'files' + } + + let current: unknown = schema + + for (let i = 0; i < pathParts.length; i++) { + const part = pathParts[i] + + if (current === null || current === undefined) { + return false + } + + if (/^\d+$/.test(part)) { + if (isFileType(current) && i + 1 < pathParts.length) { + return FILE_PROPERTIES.includes(pathParts[i + 1]) + } + continue + } + + const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/) + if (arrayMatch) { + const [, prop] = arrayMatch + const typed = current as Record + + if (prop in typed) { + const fieldDef = typed[prop] + if (isFileType(fieldDef) && i + 1 < pathParts.length) { + return FILE_PROPERTIES.includes(pathParts[i + 1]) + } + current = fieldDef + continue + } + return false + } + + const typed = current as Record + + if (part in typed) { + const nextValue = typed[part] + if (isFileType(nextValue) && i + 1 < pathParts.length) { + if (/^\d+$/.test(pathParts[i + 1]) && i + 2 < pathParts.length) { + return FILE_PROPERTIES.includes(pathParts[i + 2]) + } + return FILE_PROPERTIES.includes(pathParts[i + 1]) + } + current = nextValue + continue + } + + if (typed.properties && typeof typed.properties === 'object') { + const props = typed.properties as Record + if (part in props) { + current = props[part] + continue + } + } + + if (typed.type === 'array' && typed.items && typeof typed.items === 'object') { + const items = typed.items as Record + if (items.properties && typeof items.properties === 'object') { + const itemProps = items.properties as Record + if (part in itemProps) { + current = itemProps[part] + continue + } + } + if (part in items) { + current = items[part] + continue + } + } + + if (isFileType(current) && FILE_PROPERTIES.includes(part)) { + return true + } + + if ( + typeof current === 'object' && + current !== null && + 'type' in current && + typeof (current as { type: unknown }).type === 'string' + ) { + const typedCurrent = current as { type: string; properties?: unknown; items?: unknown } + if (!typedCurrent.properties && !typedCurrent.items) { + return false + } + } + + return false + } + + return true +} + +function getSchemaFieldNames(schema: OutputSchema | undefined): string[] { + if (!schema) return [] + return Object.keys(schema) +} + +export function resolveBlockReference( + blockName: string, + pathParts: string[], + context: BlockReferenceContext +): BlockReferenceResult | undefined { + const normalizedName = normalizeName(blockName) + const blockId = context.blockNameMapping[normalizedName] + + if (!blockId) { + return undefined + } + + const blockOutput = context.blockData[blockId] + const schema = context.blockOutputSchemas?.[blockId] + + if (blockOutput === undefined) { + if (schema && pathParts.length > 0) { + if (!isPathInSchema(schema, pathParts)) { + throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema)) + } + } + return { value: undefined, blockId } + } + + if (pathParts.length === 0) { + return { value: blockOutput, blockId } + } + + const value = navigatePath(blockOutput, pathParts) + + if (value === undefined && schema) { + if (!isPathInSchema(schema, pathParts)) { + throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema)) + } + } + + return { value, blockId } +} diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 2bdee595b1..91470a2f59 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -1,11 +1,15 @@ import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' -import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types' import { isReference, normalizeName, parseReferencePath, SPECIAL_REFERENCE_PREFIXES, } from '@/executor/constants' +import { + InvalidFieldError, + type OutputSchema, + resolveBlockReference, +} from '@/executor/utils/block-reference' import { navigatePath, type ResolutionContext, @@ -14,123 +18,6 @@ import { import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { getTool } from '@/tools/utils' -function isPathInOutputSchema( - outputs: Record | undefined, - pathParts: string[] -): boolean { - if (!outputs || pathParts.length === 0) { - return true - } - - const isFileArrayType = (value: any): boolean => - value?.type === 'file[]' || value?.type === 'files' - - let current: any = outputs - for (let i = 0; i < pathParts.length; i++) { - const part = pathParts[i] - - const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/) - if (arrayMatch) { - const [, prop] = arrayMatch - let fieldDef: any - - if (prop in current) { - fieldDef = current[prop] - } else if (current.properties && prop in current.properties) { - fieldDef = current.properties[prop] - } else if (current.type === 'array' && current.items) { - if (current.items.properties && prop in current.items.properties) { - fieldDef = current.items.properties[prop] - } else if (prop in current.items) { - fieldDef = current.items[prop] - } - } - - if (!fieldDef) { - return false - } - - if (isFileArrayType(fieldDef)) { - if (i + 1 < pathParts.length) { - return USER_FILE_ACCESSIBLE_PROPERTIES.includes(pathParts[i + 1] as any) - } - return true - } - - if (fieldDef.type === 'array' && fieldDef.items) { - current = fieldDef.items - continue - } - - current = fieldDef - continue - } - - if (/^\d+$/.test(part)) { - if (isFileArrayType(current)) { - if (i + 1 < pathParts.length) { - const nextPart = pathParts[i + 1] - return USER_FILE_ACCESSIBLE_PROPERTIES.includes(nextPart as any) - } - return true - } - continue - } - - if (current === null || current === undefined) { - return false - } - - if (part in current) { - const nextCurrent = current[part] - if (nextCurrent?.type === 'file[]' && i + 1 < pathParts.length) { - const nextPart = pathParts[i + 1] - if (/^\d+$/.test(nextPart) && i + 2 < pathParts.length) { - const propertyPart = pathParts[i + 2] - return USER_FILE_ACCESSIBLE_PROPERTIES.includes(propertyPart as any) - } - } - current = nextCurrent - continue - } - - if (current.properties && part in current.properties) { - current = current.properties[part] - continue - } - - if (current.type === 'array' && current.items) { - if (current.items.properties && part in current.items.properties) { - current = current.items.properties[part] - continue - } - if (part in current.items) { - current = current.items[part] - continue - } - } - - if (isFileArrayType(current) && USER_FILE_ACCESSIBLE_PROPERTIES.includes(part as any)) { - return true - } - - if ('type' in current && typeof current.type === 'string') { - if (!current.properties && !current.items) { - return false - } - } - - return false - } - - return true -} - -function getSchemaFieldNames(outputs: Record | undefined): string[] { - if (!outputs) return [] - return Object.keys(outputs) -} - export class BlockResolver implements Resolver { private nameToBlockId: Map private blockById: Map @@ -170,84 +57,83 @@ export class BlockResolver implements Resolver { return undefined } - const block = this.blockById.get(blockId) + const block = this.blockById.get(blockId)! const output = this.getBlockOutput(blockId, context) - if (output === undefined) { - return undefined - } - if (pathParts.length === 0) { - return output - } - - // Try the original path first - let result = navigatePath(output, pathParts) + const blockData: Record = {} + const blockOutputSchemas: Record = {} - // If successful, return it immediately - if (result !== undefined) { - return result + if (output !== undefined) { + blockData[blockId] = output } - // Response block backwards compatibility: - // Old: -> New: - // Only apply fallback if: - // 1. Block type is 'response' - // 2. Path starts with 'response.' - // 3. Output doesn't have a 'response' key (confirming it's the new format) - if ( - block?.metadata?.id === 'response' && - pathParts[0] === 'response' && - output?.response === undefined - ) { - const adjustedPathParts = pathParts.slice(1) - if (adjustedPathParts.length === 0) { - return output - } - result = navigatePath(output, adjustedPathParts) - if (result !== undefined) { - return result - } - } - - // Workflow block backwards compatibility: - // Old: -> New: - // Only apply fallback if: - // 1. Block type is 'workflow' or 'workflow_input' - // 2. Path starts with 'result.response.' - // 3. output.result.response doesn't exist (confirming child used new format) - const isWorkflowBlock = - block?.metadata?.id === 'workflow' || block?.metadata?.id === 'workflow_input' - if ( - isWorkflowBlock && - pathParts[0] === 'result' && - pathParts[1] === 'response' && - output?.result?.response === undefined - ) { - const adjustedPathParts = ['result', ...pathParts.slice(2)] - result = navigatePath(output, adjustedPathParts) - if (result !== undefined) { - return result - } - } - - const blockType = block?.metadata?.id - const params = block?.config?.params as Record | undefined + const blockType = block.metadata?.id + const params = block.config?.params as Record | undefined const subBlocks = params ? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }])) : undefined - const toolId = block?.config?.tool + const toolId = block.config?.tool const toolConfig = toolId ? getTool(toolId) : undefined const outputSchema = - toolConfig?.outputs ?? (blockType ? getBlockOutputs(blockType, subBlocks) : block?.outputs) - const schemaFields = getSchemaFieldNames(outputSchema) - if (schemaFields.length > 0 && !isPathInOutputSchema(outputSchema, pathParts)) { - throw new Error( - `"${pathParts.join('.')}" doesn't exist on block "${blockName}". ` + - `Available fields: ${schemaFields.join(', ')}` - ) - } + toolConfig?.outputs ?? (blockType ? getBlockOutputs(blockType, subBlocks) : block.outputs) + + if (outputSchema && Object.keys(outputSchema).length > 0) { + blockOutputSchemas[blockId] = outputSchema + } + + try { + const result = resolveBlockReference(blockName, pathParts, { + blockNameMapping: Object.fromEntries(this.nameToBlockId), + blockData, + blockOutputSchemas, + })! + + if (result.value !== undefined) { + return result.value + } + + if (output !== undefined && pathParts.length > 0) { + if ( + block.metadata?.id === 'response' && + pathParts[0] === 'response' && + output?.response === undefined + ) { + const adjustedPathParts = pathParts.slice(1) + if (adjustedPathParts.length === 0) { + return output + } + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } + } - return undefined + const isWorkflowBlock = + block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input' + if ( + isWorkflowBlock && + pathParts[0] === 'result' && + pathParts[1] === 'response' && + output?.result?.response === undefined + ) { + const adjustedPathParts = ['result', ...pathParts.slice(2)] + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } + } + } + + return undefined + } catch (error) { + if (error instanceof InvalidFieldError) { + throw new Error( + `"${error.fieldPath}" doesn't exist on block "${error.blockName}". ` + + `Available fields: ${error.availableFields.join(', ')}` + ) + } + throw error + } } private getBlockOutput(blockId: string, context: ResolutionContext): any { diff --git a/apps/sim/lib/execution/isolated-vm-worker.cjs b/apps/sim/lib/execution/isolated-vm-worker.cjs index 53aa5b6fc5..f6c587a15f 100644 --- a/apps/sim/lib/execution/isolated-vm-worker.cjs +++ b/apps/sim/lib/execution/isolated-vm-worker.cjs @@ -130,7 +130,11 @@ async function executeCode(request) { await jail.set('environmentVariables', new ivm.ExternalCopy(envVars).copyInto()) for (const [key, value] of Object.entries(contextVariables)) { - await jail.set(key, new ivm.ExternalCopy(value).copyInto()) + if (value === undefined) { + await jail.set(key, undefined) + } else { + await jail.set(key, new ivm.ExternalCopy(value).copyInto()) + } } const fetchCallback = new ivm.Reference(async (url, optionsJson) => { diff --git a/apps/sim/tools/function/execute.test.ts b/apps/sim/tools/function/execute.test.ts index c5ab2147c3..dc1b6eb20e 100644 --- a/apps/sim/tools/function/execute.test.ts +++ b/apps/sim/tools/function/execute.test.ts @@ -56,6 +56,7 @@ describe('Function Execute Tool', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, + blockOutputSchemas: {}, isCustomTool: false, language: 'javascript', timeout: 5000, @@ -83,6 +84,7 @@ describe('Function Execute Tool', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, + blockOutputSchemas: {}, isCustomTool: false, language: 'javascript', workflowId: undefined, @@ -101,6 +103,7 @@ describe('Function Execute Tool', () => { workflowVariables: {}, blockData: {}, blockNameMapping: {}, + blockOutputSchemas: {}, isCustomTool: false, language: 'javascript', workflowId: undefined, diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 516c701270..d7f59daa89 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -53,6 +53,13 @@ export const functionExecuteTool: ToolConfig blockData?: Record blockNameMapping?: Record + blockOutputSchemas?: Record> _context?: { workflowId?: string } From bdf72897d3414a5b892c9384dc413aba7bea17f4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 11:13:45 -0800 Subject: [PATCH 2/7] fix edge cases --- apps/sim/app/api/function/execute/route.ts | 21 ++-- apps/sim/executor/utils/block-data.ts | 1 - apps/sim/executor/utils/block-reference.ts | 135 ++++++++++++--------- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 193663f43a..0940054b2e 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -480,7 +480,7 @@ function resolveTagVariables( const undefinedLiteral = language === 'python' ? 'None' : 'undefined' const tagPattern = new RegExp( - `${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`, + `${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`, 'g' ) const tagMatches = resolvedCode.match(tagPattern) || [] @@ -508,19 +508,18 @@ function resolveTagVariables( continue } - if ( - typeof tagValue === 'string' && - tagValue.length > 100 && - (tagValue.startsWith('{') || tagValue.startsWith('[')) - ) { - try { - tagValue = JSON.parse(tagValue) - } catch { - // Keep as-is + if (typeof tagValue === 'string') { + const trimmed = tagValue.trimStart() + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + tagValue = JSON.parse(tagValue) + } catch { + // Keep as string if not valid JSON + } } } - const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}` + const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}` contextVariables[safeVarName] = tagValue resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName) } diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index ab783c6976..6941803741 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -26,7 +26,6 @@ export function collectBlockData(ctx: ExecutionContext): BlockDataCollection { blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id } - // Build output schema from block type and params const blockType = workflowBlock.metadata?.id if (blockType) { const params = workflowBlock.config?.params as Record | undefined diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 254ec4853b..8233415ea5 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -28,18 +28,53 @@ export class InvalidFieldError extends Error { } } +const FILE_PROPERTIES = ['name', 'type', 'size', 'url', 'base64', 'mimeType'] as const + +function isFileType(value: unknown): boolean { + if (typeof value !== 'object' || value === null) return false + const typed = value as { type?: string } + return typed.type === 'file[]' || typed.type === 'files' +} + +function isArrayType(value: unknown): value is { type: 'array'; items?: unknown } { + if (typeof value !== 'object' || value === null) return false + return (value as { type?: string }).type === 'array' +} + +function getArrayItems(schema: unknown): unknown { + if (typeof schema !== 'object' || schema === null) return undefined + return (schema as { items?: unknown }).items +} + +function getProperties(schema: unknown): Record | undefined { + if (typeof schema !== 'object' || schema === null) return undefined + const props = (schema as { properties?: unknown }).properties + return typeof props === 'object' && props !== null + ? (props as Record) + : undefined +} + +function lookupField(schema: unknown, fieldName: string): unknown | undefined { + if (typeof schema !== 'object' || schema === null) return undefined + const typed = schema as Record + + if (fieldName in typed) { + return typed[fieldName] + } + + const props = getProperties(schema) + if (props && fieldName in props) { + return props[fieldName] + } + + return undefined +} + function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): boolean { if (!schema || pathParts.length === 0) { return true } - const FILE_PROPERTIES = ['name', 'type', 'size', 'url', 'base64', 'mimeType'] - const isFileType = (value: unknown): boolean => { - if (typeof value !== 'object' || value === null) return false - const typed = value as { type?: string } - return typed.type === 'file[]' || typed.type === 'files' - } - let current: unknown = schema for (let i = 0; i < pathParts.length; i++) { @@ -50,8 +85,12 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): } if (/^\d+$/.test(part)) { - if (isFileType(current) && i + 1 < pathParts.length) { - return FILE_PROPERTIES.includes(pathParts[i + 1]) + if (isFileType(current)) { + const nextPart = pathParts[i + 1] + return !nextPart || FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) + } + if (isArrayType(current)) { + current = getArrayItems(current) } continue } @@ -59,69 +98,45 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/) if (arrayMatch) { const [, prop] = arrayMatch - const typed = current as Record + const fieldDef = lookupField(current, prop) + if (!fieldDef) return false - if (prop in typed) { - const fieldDef = typed[prop] - if (isFileType(fieldDef) && i + 1 < pathParts.length) { - return FILE_PROPERTIES.includes(pathParts[i + 1]) - } - current = fieldDef - continue + if (isFileType(fieldDef)) { + const nextPart = pathParts[i + 1] + return !nextPart || FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) } - return false - } - const typed = current as Record - - if (part in typed) { - const nextValue = typed[part] - if (isFileType(nextValue) && i + 1 < pathParts.length) { - if (/^\d+$/.test(pathParts[i + 1]) && i + 2 < pathParts.length) { - return FILE_PROPERTIES.includes(pathParts[i + 2]) - } - return FILE_PROPERTIES.includes(pathParts[i + 1]) - } - current = nextValue + current = isArrayType(fieldDef) ? getArrayItems(fieldDef) : fieldDef continue } - if (typed.properties && typeof typed.properties === 'object') { - const props = typed.properties as Record - if (part in props) { - current = props[part] - continue - } + if (isFileType(current) && FILE_PROPERTIES.includes(part as (typeof FILE_PROPERTIES)[number])) { + return true } - if (typed.type === 'array' && typed.items && typeof typed.items === 'object') { - const items = typed.items as Record - if (items.properties && typeof items.properties === 'object') { - const itemProps = items.properties as Record - if (part in itemProps) { - current = itemProps[part] - continue + const fieldDef = lookupField(current, part) + if (fieldDef !== undefined) { + if (isFileType(fieldDef)) { + const nextPart = pathParts[i + 1] + if (!nextPart) return true + if (/^\d+$/.test(nextPart)) { + const afterIndex = pathParts[i + 2] + return ( + !afterIndex || FILE_PROPERTIES.includes(afterIndex as (typeof FILE_PROPERTIES)[number]) + ) } + return FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) } - if (part in items) { - current = items[part] - continue - } - } - - if (isFileType(current) && FILE_PROPERTIES.includes(part)) { - return true + current = fieldDef + continue } - if ( - typeof current === 'object' && - current !== null && - 'type' in current && - typeof (current as { type: unknown }).type === 'string' - ) { - const typedCurrent = current as { type: string; properties?: unknown; items?: unknown } - if (!typedCurrent.properties && !typedCurrent.items) { - return false + if (isArrayType(current)) { + const items = getArrayItems(current) + const itemField = lookupField(items, part) + if (itemField !== undefined) { + current = itemField + continue } } From 7fc7b0f56274d9a0d2517b1d0940bd6a82013279 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 11:35:48 -0800 Subject: [PATCH 3/7] use already formatted error --- apps/sim/executor/variables/resolvers/block.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 91470a2f59..2c0fcf4342 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -127,10 +127,7 @@ export class BlockResolver implements Resolver { return undefined } catch (error) { if (error instanceof InvalidFieldError) { - throw new Error( - `"${error.fieldPath}" doesn't exist on block "${error.blockName}". ` + - `Available fields: ${error.availableFields.join(', ')}` - ) + throw new Error(error.message) } throw error } From 6fac012ef734a2a5dd7ac54ba26b940e66728539 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 11:55:09 -0800 Subject: [PATCH 4/7] fix multi index --- .../executor/variables/resolvers/reference.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts index 986ee2ab64..9f4b69eec5 100644 --- a/apps/sim/executor/variables/resolvers/reference.ts +++ b/apps/sim/executor/variables/resolvers/reference.ts @@ -27,23 +27,28 @@ export function navigatePath(obj: any, path: string[]): any { return undefined } - // Handle array indexing like "items[0]" or just numeric indices - const arrayMatch = part.match(/^([^[]+)\[(\d+)\](.*)$/) + const arrayMatch = part.match(/^([^[]+)(\[.+)$/) if (arrayMatch) { - // Handle complex array access like "items[0]" - const [, prop, index] = arrayMatch + const [, prop, bracketsPart] = arrayMatch current = current[prop] if (current === undefined || current === null) { return undefined } - const idx = Number.parseInt(index, 10) - current = Array.isArray(current) ? current[idx] : undefined + + const indices = bracketsPart.match(/\[(\d+)\]/g) + if (indices) { + for (const indexMatch of indices) { + if (current === null || current === undefined) { + return undefined + } + const idx = Number.parseInt(indexMatch.slice(1, -1), 10) + current = Array.isArray(current) ? current[idx] : undefined + } + } } else if (/^\d+$/.test(part)) { - // Handle plain numeric index const index = Number.parseInt(part, 10) current = Array.isArray(current) ? current[index] : undefined } else { - // Handle regular property access current = current[part] } } From cb2667c20beb29d9fb741a3c4c861df89d927d6d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 12:02:44 -0800 Subject: [PATCH 5/7] fix backwards compat reachability --- .../sim/executor/variables/resolvers/block.ts | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 2c0fcf4342..e32e5d6a60 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -88,45 +88,40 @@ export class BlockResolver implements Resolver { blockOutputSchemas, })! - if (result.value !== undefined) { - return result.value - } - - if (output !== undefined && pathParts.length > 0) { - if ( - block.metadata?.id === 'response' && - pathParts[0] === 'response' && - output?.response === undefined - ) { - const adjustedPathParts = pathParts.slice(1) - if (adjustedPathParts.length === 0) { - return output - } - const fallbackResult = navigatePath(output, adjustedPathParts) - if (fallbackResult !== undefined) { - return fallbackResult + return result.value + } catch (error) { + if (error instanceof InvalidFieldError) { + if (output !== undefined && pathParts.length > 0) { + if ( + block.metadata?.id === 'response' && + pathParts[0] === 'response' && + output?.response === undefined + ) { + const adjustedPathParts = pathParts.slice(1) + if (adjustedPathParts.length === 0) { + return output + } + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } } - } - const isWorkflowBlock = - block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input' - if ( - isWorkflowBlock && - pathParts[0] === 'result' && - pathParts[1] === 'response' && - output?.result?.response === undefined - ) { - const adjustedPathParts = ['result', ...pathParts.slice(2)] - const fallbackResult = navigatePath(output, adjustedPathParts) - if (fallbackResult !== undefined) { - return fallbackResult + const isWorkflowBlock = + block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input' + if ( + isWorkflowBlock && + pathParts[0] === 'result' && + pathParts[1] === 'response' && + output?.result?.response === undefined + ) { + const adjustedPathParts = ['result', ...pathParts.slice(2)] + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } } } - } - - return undefined - } catch (error) { - if (error instanceof InvalidFieldError) { throw new Error(error.message) } throw error From b04af42e48d778cb23b07a6389960b10ec44892b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 12:09:51 -0800 Subject: [PATCH 6/7] handle backwards compatibility accurately --- .../sim/executor/variables/resolvers/block.ts | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index e32e5d6a60..a29339b2bf 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -88,39 +88,16 @@ export class BlockResolver implements Resolver { blockOutputSchemas, })! - return result.value + if (result.value !== undefined) { + return result.value + } + + return this.handleBackwardsCompat(block, output, pathParts) } catch (error) { if (error instanceof InvalidFieldError) { - if (output !== undefined && pathParts.length > 0) { - if ( - block.metadata?.id === 'response' && - pathParts[0] === 'response' && - output?.response === undefined - ) { - const adjustedPathParts = pathParts.slice(1) - if (adjustedPathParts.length === 0) { - return output - } - const fallbackResult = navigatePath(output, adjustedPathParts) - if (fallbackResult !== undefined) { - return fallbackResult - } - } - - const isWorkflowBlock = - block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input' - if ( - isWorkflowBlock && - pathParts[0] === 'result' && - pathParts[1] === 'response' && - output?.result?.response === undefined - ) { - const adjustedPathParts = ['result', ...pathParts.slice(2)] - const fallbackResult = navigatePath(output, adjustedPathParts) - if (fallbackResult !== undefined) { - return fallbackResult - } - } + const fallback = this.handleBackwardsCompat(block, output, pathParts) + if (fallback !== undefined) { + return fallback } throw new Error(error.message) } @@ -128,6 +105,49 @@ export class BlockResolver implements Resolver { } } + private handleBackwardsCompat( + block: SerializedBlock, + output: unknown, + pathParts: string[] + ): unknown { + if (output === undefined || pathParts.length === 0) { + return undefined + } + + if ( + block.metadata?.id === 'response' && + pathParts[0] === 'response' && + (output as Record)?.response === undefined + ) { + const adjustedPathParts = pathParts.slice(1) + if (adjustedPathParts.length === 0) { + return output + } + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } + } + + const isWorkflowBlock = + block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input' + const outputRecord = output as Record | undefined> + if ( + isWorkflowBlock && + pathParts[0] === 'result' && + pathParts[1] === 'response' && + outputRecord?.result?.response === undefined + ) { + const adjustedPathParts = ['result', ...pathParts.slice(2)] + const fallbackResult = navigatePath(output, adjustedPathParts) + if (fallbackResult !== undefined) { + return fallbackResult + } + } + + return undefined + } + private getBlockOutput(blockId: string, context: ResolutionContext): any { const stateOutput = context.executionState.getBlockOutput(blockId, context.currentNodeId) if (stateOutput !== undefined) { From b0a6cd7417d55edc8ab2a34971c1fbcbae41f3c7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 22 Jan 2026 12:25:56 -0800 Subject: [PATCH 7/7] use shared constant correctly --- apps/sim/executor/utils/block-reference.ts | 33 +++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 8233415ea5..590e9d869d 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -1,3 +1,4 @@ +import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types' import { normalizeName } from '@/executor/constants' import { navigatePath } from '@/executor/variables/resolvers/reference' @@ -28,8 +29,6 @@ export class InvalidFieldError extends Error { } } -const FILE_PROPERTIES = ['name', 'type', 'size', 'url', 'base64', 'mimeType'] as const - function isFileType(value: unknown): boolean { if (typeof value !== 'object' || value === null) return false const typed = value as { type?: string } @@ -87,7 +86,12 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): if (/^\d+$/.test(part)) { if (isFileType(current)) { const nextPart = pathParts[i + 1] - return !nextPart || FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) + return ( + !nextPart || + USER_FILE_ACCESSIBLE_PROPERTIES.includes( + nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number] + ) + ) } if (isArrayType(current)) { current = getArrayItems(current) @@ -103,14 +107,24 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): if (isFileType(fieldDef)) { const nextPart = pathParts[i + 1] - return !nextPart || FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) + return ( + !nextPart || + USER_FILE_ACCESSIBLE_PROPERTIES.includes( + nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number] + ) + ) } current = isArrayType(fieldDef) ? getArrayItems(fieldDef) : fieldDef continue } - if (isFileType(current) && FILE_PROPERTIES.includes(part as (typeof FILE_PROPERTIES)[number])) { + if ( + isFileType(current) && + USER_FILE_ACCESSIBLE_PROPERTIES.includes( + part as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number] + ) + ) { return true } @@ -122,10 +136,15 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): if (/^\d+$/.test(nextPart)) { const afterIndex = pathParts[i + 2] return ( - !afterIndex || FILE_PROPERTIES.includes(afterIndex as (typeof FILE_PROPERTIES)[number]) + !afterIndex || + USER_FILE_ACCESSIBLE_PROPERTIES.includes( + afterIndex as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number] + ) ) } - return FILE_PROPERTIES.includes(nextPart as (typeof FILE_PROPERTIES)[number]) + return USER_FILE_ACCESSIBLE_PROPERTIES.includes( + nextPart as (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number] + ) } current = fieldDef continue