Skip to content

Commit c1200ef

Browse files
committed
improvement(execution): update execution for passing base64 strings
1 parent 05c4538 commit c1200ef

File tree

15 files changed

+133
-52
lines changed

15 files changed

+133
-52
lines changed

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
1414
import { LoggingSession } from '@/lib/logs/execution/logging-session'
1515
import {
1616
cleanupExecutionBase64Cache,
17-
containsUserFileWithMetadata,
1817
hydrateUserFilesWithBase64,
1918
} from '@/lib/uploads/utils/user-file-base64.server'
2019
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
@@ -438,6 +437,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
438437
snapshot,
439438
callbacks: {},
440439
loggingSession,
440+
includeFileBase64,
441+
base64MaxBytes,
441442
})
442443

443444
const outputWithBase64 = includeFileBase64
@@ -596,15 +597,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
596597
iterationContext?: IterationContext
597598
) => {
598599
const hasError = callbackData.output?.error
599-
const shouldHydrate =
600-
includeFileBase64 && !hasError && containsUserFileWithMetadata(callbackData.output)
601-
const outputWithBase64 = shouldHydrate
602-
? await hydrateUserFilesWithBase64(callbackData.output, {
603-
requestId,
604-
executionId,
605-
maxBytes: base64MaxBytes,
606-
})
607-
: callbackData.output
608600

609601
if (hasError) {
610602
logger.info(`[${requestId}] ✗ onBlockComplete (error) called:`, {
@@ -648,7 +640,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
648640
blockName,
649641
blockType,
650642
input: callbackData.input,
651-
output: outputWithBase64,
643+
output: callbackData.output,
652644
durationMs: callbackData.executionTime || 0,
653645
...(iterationContext && {
654646
iterationCurrent: iterationContext.iterationCurrent,
@@ -733,6 +725,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
733725
},
734726
loggingSession,
735727
abortSignal: abortController.signal,
728+
includeFileBase64,
729+
base64MaxBytes,
736730
})
737731

738732
if (result.status === 'paused') {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -214,40 +214,18 @@ const getOutputTypeForPath = (
214214
outputPath: string,
215215
mergedSubBlocksOverride?: Record<string, any>
216216
): string => {
217-
if (block?.triggerMode && blockConfig?.triggers?.enabled) {
218-
return getBlockOutputType(block.type, outputPath, mergedSubBlocksOverride, true)
219-
}
220-
if (block?.type === 'starter') {
221-
const startWorkflowValue =
222-
mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow')
223-
224-
if (startWorkflowValue === 'chat') {
225-
const chatModeTypes: Record<string, string> = {
226-
input: 'string',
227-
conversationId: 'string',
228-
files: 'files',
229-
}
230-
return chatModeTypes[outputPath] || 'any'
231-
}
232-
const inputFormatValue =
233-
mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat')
234-
if (inputFormatValue && Array.isArray(inputFormatValue)) {
235-
const field = inputFormatValue.find(
236-
(f: { name?: string; type?: string }) => f.name === outputPath
237-
)
238-
if (field?.type) return field.type
239-
}
240-
} else if (blockConfig?.category === 'triggers') {
241-
const blockState = useWorkflowStore.getState().blocks[blockId]
242-
const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {})
243-
return getBlockOutputType(block.type, outputPath, subBlocks)
244-
} else {
217+
const subBlocks =
218+
mergedSubBlocksOverride ?? useWorkflowStore.getState().blocks[blockId]?.subBlocks
219+
const triggerMode = block?.triggerMode && blockConfig?.triggers?.enabled
220+
221+
if (blockConfig?.tools?.config?.tool) {
245222
const operationValue = getSubBlockValue(blockId, 'operation')
246-
if (blockConfig && operationValue) {
223+
if (operationValue) {
247224
return getToolOutputType(blockConfig, operationValue, outputPath)
248225
}
249226
}
250-
return 'any'
227+
228+
return getBlockOutputType(block?.type ?? '', outputPath, subBlocks, triggerMode)
251229
}
252230

253231
/**
@@ -1789,7 +1767,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
17891767
mergedSubBlocks
17901768
)
17911769

1792-
if (fieldType === 'files' || fieldType === 'array') {
1770+
if (fieldType === 'files' || fieldType === 'file[]' || fieldType === 'array') {
17931771
const blockName = parts[0]
17941772
const remainingPath = parts.slice(2).join('.')
17951773
processedTag = `${blockName}.${arrayFieldName}[0].${remainingPath}`

apps/sim/background/schedule-execution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ async function runWorkflowExecution({
208208
snapshot,
209209
callbacks: {},
210210
loggingSession,
211+
includeFileBase64: true,
212+
base64MaxBytes: undefined,
211213
})
212214

213215
if (executionResult.status === 'paused') {

apps/sim/background/webhook-execution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ async function executeWebhookJobInternal(
240240
snapshot,
241241
callbacks: {},
242242
loggingSession,
243+
includeFileBase64: true, // Enable base64 hydration
244+
base64MaxBytes: undefined, // Use default limit
243245
})
244246

245247
if (executionResult.status === 'paused') {
@@ -493,6 +495,7 @@ async function executeWebhookJobInternal(
493495
snapshot,
494496
callbacks: {},
495497
loggingSession,
498+
includeFileBase64: true,
496499
})
497500

498501
if (executionResult.status === 'paused') {

apps/sim/background/workflow-execution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
109109
snapshot,
110110
callbacks: {},
111111
loggingSession,
112+
includeFileBase64: true,
113+
base64MaxBytes: undefined,
112114
})
113115

114116
if (result.status === 'paused') {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { mcpServers } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, inArray, isNull } from 'drizzle-orm'
55
import { getBaseUrl } from '@/lib/core/utils/urls'
6+
import {
7+
containsUserFileWithMetadata,
8+
hydrateUserFilesWithBase64,
9+
} from '@/lib/uploads/utils/user-file-base64.server'
610
import {
711
BlockType,
812
buildResumeApiUrl,
@@ -135,6 +139,14 @@ export class BlockExecutor {
135139
normalizedOutput = this.normalizeOutput(output)
136140
}
137141

142+
if (ctx.includeFileBase64 && containsUserFileWithMetadata(normalizedOutput)) {
143+
normalizedOutput = (await hydrateUserFilesWithBase64(normalizedOutput, {
144+
requestId: ctx.metadata.requestId,
145+
executionId: ctx.executionId,
146+
maxBytes: ctx.base64MaxBytes,
147+
})) as NormalizedBlockOutput
148+
}
149+
138150
const duration = Date.now() - startTime
139151

140152
if (blockLog) {

apps/sim/executor/execution/executor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export class DAGExecutor {
169169
onBlockStart: this.contextExtensions.onBlockStart,
170170
onBlockComplete: this.contextExtensions.onBlockComplete,
171171
abortSignal: this.contextExtensions.abortSignal,
172+
includeFileBase64: this.contextExtensions.includeFileBase64,
173+
base64MaxBytes: this.contextExtensions.base64MaxBytes,
172174
}
173175

174176
if (this.contextExtensions.resumeFromSnapshot) {

apps/sim/executor/execution/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export interface ContextExtensions {
8989
* When aborted, the execution should stop gracefully.
9090
*/
9191
abortSignal?: AbortSignal
92+
includeFileBase64?: boolean
93+
base64MaxBytes?: number
9294
onStream?: (streamingExecution: unknown) => Promise<void>
9395
onBlockStart?: (
9496
blockId: string,

apps/sim/executor/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ export interface ExecutionContext {
237237

238238
// Dynamically added nodes that need to be scheduled (e.g., from parallel expansion)
239239
pendingDynamicNodes?: string[]
240+
241+
/**
242+
* When true, UserFile objects in block outputs will be hydrated with base64 content
243+
* before being stored in execution state. This ensures base64 is available for
244+
* variable resolution in downstream blocks.
245+
*/
246+
includeFileBase64?: boolean
247+
248+
/**
249+
* Maximum file size in bytes for base64 hydration. Files larger than this limit
250+
* will not have their base64 content fetched.
251+
*/
252+
base64MaxBytes?: number
240253
}
241254

242255
export interface ExecutionResult {

apps/sim/executor/variables/resolvers/block.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,54 @@ function isPathInOutputSchema(
2121
return true
2222
}
2323

24+
const isFileArrayType = (value: any): boolean =>
25+
value?.type === 'file[]' || value?.type === 'files'
26+
2427
let current: any = outputs
2528
for (let i = 0; i < pathParts.length; i++) {
2629
const part = pathParts[i]
2730

31+
const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/)
32+
if (arrayMatch) {
33+
const [, prop] = arrayMatch
34+
let fieldDef: any
35+
36+
if (prop in current) {
37+
fieldDef = current[prop]
38+
} else if (current.properties && prop in current.properties) {
39+
fieldDef = current.properties[prop]
40+
} else if (current.type === 'array' && current.items) {
41+
if (current.items.properties && prop in current.items.properties) {
42+
fieldDef = current.items.properties[prop]
43+
} else if (prop in current.items) {
44+
fieldDef = current.items[prop]
45+
}
46+
}
47+
48+
if (!fieldDef) {
49+
return false
50+
}
51+
52+
if (isFileArrayType(fieldDef)) {
53+
if (i + 1 < pathParts.length) {
54+
return USER_FILE_ACCESSIBLE_PROPERTIES.includes(pathParts[i + 1] as any)
55+
}
56+
return true
57+
}
58+
59+
if (fieldDef.type === 'array' && fieldDef.items) {
60+
current = fieldDef.items
61+
continue
62+
}
63+
64+
current = fieldDef
65+
continue
66+
}
67+
2868
// Handle array index access (e.g., [0])
2969
if (/^\d+$/.test(part)) {
3070
// If current is file[] type, next part should be a file property
31-
if (current?.type === 'file[]') {
71+
if (isFileArrayType(current)) {
3272
// Check if next part is a valid file property
3373
if (i + 1 < pathParts.length) {
3474
const nextPart = pathParts[i + 1]
@@ -78,7 +118,7 @@ function isPathInOutputSchema(
78118
}
79119

80120
// Handle file[] type - allow access to file properties after array index
81-
if (current?.type === 'file[]' && USER_FILE_ACCESSIBLE_PROPERTIES.includes(part as any)) {
121+
if (isFileArrayType(current) && USER_FILE_ACCESSIBLE_PROPERTIES.includes(part as any)) {
82122
// Valid file property access
83123
return true
84124
}

0 commit comments

Comments
 (0)