Skip to content

Commit 6734052

Browse files
author
octo-patch
committed
fix: use context variables for block outputs in function block code
When a function block references another block's output via <BlockA.result>, the executor previously embedded the full value as a JavaScript literal directly in the code string. For large outputs (>50 KB), this caused the code string to exceed the terminal console display limit, making inputs appear truncated or replaced with { __simTruncated: true } in the UI. Instead, block output references in function block code are now stored as named global variables (__blockRef_N) in the isolated VM context. The code string only contains the compact variable name, keeping it small regardless of the referenced value size. Loop/parallel/env/workflow references are still inlined as literals since the API route has no way to resolve them independently. The _runtimeContextVars key is filtered from sanitizeInputsForLog so it does not appear in execution logs or SSE events. Pre-resolved context variables are merged with any variables produced by the API route resolveCodeVariables, with executor values taking precedence. Fixes #4195
1 parent dcf3302 commit 6734052

6 files changed

Lines changed: 157 additions & 4 deletions

File tree

apps/sim/app/api/function/execute/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ export async function POST(req: NextRequest) {
719719
blockNameMapping = {},
720720
blockOutputSchemas = {},
721721
workflowVariables = {},
722+
contextVariables: preResolvedContextVariables = {},
722723
workflowId,
723724
workspaceId,
724725
isCustomTool = false,
@@ -755,7 +756,10 @@ export async function POST(req: NextRequest) {
755756
lang
756757
)
757758
resolvedCode = codeResolution.resolvedCode
758-
contextVariables = codeResolution.contextVariables
759+
// Merge pre-resolved block output variables from the executor. These take precedence
760+
// because they were produced by the resolver using full execution-state context
761+
// (including loop/parallel scope) and should not be overwritten.
762+
contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables }
759763
}
760764

761765
let jsImports = ''

apps/sim/executor/execution/block-executor.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ import {
4242
} from '@/executor/utils/iteration-context'
4343
import { isJSONString } from '@/executor/utils/json'
4444
import { filterOutputForLog } from '@/executor/utils/output-filter'
45-
import type { VariableResolver } from '@/executor/variables/resolver'
45+
import {
46+
FUNCTION_BLOCK_CONTEXT_VARS_KEY,
47+
type VariableResolver,
48+
} from '@/executor/variables/resolver'
4649
import type { SerializedBlock } from '@/serializer/types'
4750
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
4851

@@ -108,7 +111,13 @@ export class BlockExecutor {
108111
await validateBlockType(ctx.userId, blockType, ctx)
109112
}
110113

111-
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
114+
if (block.metadata?.id === BlockType.FUNCTION) {
115+
const { resolvedInputs: fnInputs, contextVariables } =
116+
this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block)
117+
resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables }
118+
} else {
119+
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
120+
}
112121

113122
if (blockLog) {
114123
blockLog.input = this.sanitizeInputsForLog(resolvedInputs)
@@ -418,7 +427,7 @@ export class BlockExecutor {
418427
const result: Record<string, any> = {}
419428

420429
for (const [key, value] of Object.entries(inputs)) {
421-
if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') {
430+
if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode' || key === FUNCTION_BLOCK_CONTEXT_VARS_KEY) {
422431
continue
423432
}
424433

apps/sim/executor/handlers/function/function-handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
33
import { BlockType } from '@/executor/constants'
44
import type { BlockHandler, ExecutionContext } from '@/executor/types'
55
import { collectBlockData } from '@/executor/utils/block-data'
6+
import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
67
import type { SerializedBlock } from '@/serializer/types'
78
import { executeTool } from '@/tools'
89

@@ -25,6 +26,10 @@ export class FunctionBlockHandler implements BlockHandler {
2526

2627
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
2728

29+
const contextVariables = (inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as
30+
| Record<string, unknown>
31+
| undefined) ?? {}
32+
2833
const result = await executeTool(
2934
'function_execute',
3035
{
@@ -36,6 +41,7 @@ export class FunctionBlockHandler implements BlockHandler {
3641
blockData,
3742
blockNameMapping,
3843
blockOutputSchemas,
44+
contextVariables,
3945
_context: {
4046
workflowId: ctx.workflowId,
4147
workspaceId: ctx.workspaceId,

apps/sim/executor/variables/resolver.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
import { WorkflowResolver } from '@/executor/variables/resolvers/workflow'
1616
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
1717

18+
/** Key used to carry pre-resolved context variables through the inputs map. */
19+
export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars'
20+
1821
const logger = createLogger('VariableResolver')
1922

2023
export class VariableResolver {
@@ -36,6 +39,63 @@ export class VariableResolver {
3639
]
3740
}
3841

42+
/**
43+
* Resolves inputs for function blocks. Block output references in the `code` field
44+
* are stored as named context variables instead of being embedded as JavaScript
45+
* literals, preventing large values from bloating the code string.
46+
*
47+
* Returns the resolved inputs and a `contextVariables` map. Callers should inject
48+
* contextVariables into the function execution request body so the isolated VM can
49+
* access them as global variables.
50+
*/
51+
resolveInputsForFunctionBlock(
52+
ctx: ExecutionContext,
53+
currentNodeId: string,
54+
params: Record<string, any>,
55+
block: SerializedBlock
56+
): { resolvedInputs: Record<string, any>; contextVariables: Record<string, unknown> } {
57+
const contextVariables: Record<string, unknown> = {}
58+
const resolved: Record<string, any> = {}
59+
60+
for (const [key, value] of Object.entries(params)) {
61+
if (key === 'code') {
62+
if (typeof value === 'string') {
63+
resolved[key] = this.resolveCodeWithContextVars(
64+
ctx,
65+
currentNodeId,
66+
value,
67+
undefined,
68+
block,
69+
contextVariables
70+
)
71+
} else if (Array.isArray(value)) {
72+
resolved[key] = value.map((item: any) => {
73+
if (item && typeof item === 'object' && typeof item.content === 'string') {
74+
return {
75+
...item,
76+
content: this.resolveCodeWithContextVars(
77+
ctx,
78+
currentNodeId,
79+
item.content,
80+
undefined,
81+
block,
82+
contextVariables
83+
),
84+
}
85+
}
86+
return item
87+
})
88+
} else {
89+
resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block)
90+
}
91+
} else {
92+
resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block)
93+
}
94+
}
95+
96+
return { resolvedInputs: resolved, contextVariables }
97+
}
98+
3999
resolveInputs(
40100
ctx: ExecutionContext,
41101
currentNodeId: string,
@@ -149,6 +209,70 @@ export class VariableResolver {
149209
}
150210
return value
151211
}
212+
/**
213+
* Resolves a code template for a function block. Block output references are stored
214+
* in `contextVarAccumulator` as named variables (e.g. `__blockRef_0`) and replaced
215+
* with those variable names in the returned code string. Non-block references (loop
216+
* items, workflow variables, env vars) are still inlined as literals so they remain
217+
* available without any extra passing mechanism.
218+
*/
219+
private resolveCodeWithContextVars(
220+
ctx: ExecutionContext,
221+
currentNodeId: string,
222+
template: string,
223+
loopScope: LoopScope | undefined,
224+
block: SerializedBlock,
225+
contextVarAccumulator: Record<string, unknown>
226+
): string {
227+
const resolutionContext: ResolutionContext = {
228+
executionContext: ctx,
229+
executionState: this.state,
230+
currentNodeId,
231+
loopScope,
232+
}
233+
234+
const language = (block.config?.params as Record<string, unknown> | undefined)?.language as
235+
| string
236+
| undefined
237+
238+
let replacementError: Error | null = null
239+
240+
let result = replaceValidReferences(template, (match) => {
241+
if (replacementError) return match
242+
243+
try {
244+
const resolved = this.resolveReference(match, resolutionContext)
245+
if (resolved === undefined) return match
246+
247+
const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved
248+
249+
if (this.blockResolver.canResolve(match)) {
250+
// Block output: store in contextVarAccumulator, replace with variable name
251+
const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}`
252+
contextVarAccumulator[varName] = effectiveValue
253+
return varName
254+
}
255+
256+
// Non-block reference (loop, parallel, workflow, env): embed as literal
257+
return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language)
258+
} catch (error) {
259+
replacementError = error instanceof Error ? error : new Error(String(error))
260+
return match
261+
}
262+
})
263+
264+
if (replacementError !== null) {
265+
throw replacementError
266+
}
267+
268+
result = result.replace(createEnvVarPattern(), (match) => {
269+
const resolved = this.resolveReference(match, resolutionContext)
270+
return typeof resolved === 'string' ? resolved : match
271+
})
272+
273+
return result
274+
}
275+
152276
private resolveTemplate(
153277
ctx: ExecutionContext,
154278
currentNodeId: string,

apps/sim/tools/function/execute.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
128128
blockData: params.blockData || {},
129129
blockNameMapping: params.blockNameMapping || {},
130130
blockOutputSchemas: params.blockOutputSchemas || {},
131+
contextVariables: params.contextVariables || {},
131132
workflowId: params._context?.workflowId,
132133
userId: params._context?.userId,
133134
workspaceId: params._context?.workspaceId,

apps/sim/tools/function/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface CodeExecutionInput {
1717
blockData?: Record<string, unknown>
1818
blockNameMapping?: Record<string, string>
1919
blockOutputSchemas?: Record<string, Record<string, unknown>>
20+
/** Pre-resolved block output variables from the executor, injected as VM globals. */
21+
contextVariables?: Record<string, unknown>
2022
_context?: {
2123
workflowId?: string
2224
userId?: string
@@ -32,3 +34,10 @@ export interface CodeExecutionOutput extends ToolResponse {
3234
stdout: string
3335
}
3436
}
37+
38+
export interface CodeExecutionOutput extends ToolResponse {
39+
output: {
40+
result: any
41+
stdout: string
42+
}
43+
}

0 commit comments

Comments
 (0)