Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9bbbe0a
chore(deps): bump mermaid to 11.15.0 for GHSA-ghcm-xqfw-q4vr (#4615)
waleedlatif1 May 15, 2026
bad21cb
improvement(agent, file-block): files in agent block, file block v4 (…
Sg312 May 15, 2026
c9118e7
feat(files): folders, multiselect, vfs update (#4572)
icecrasher321 May 15, 2026
d3d8f9c
fix(logs,workspace): prevent cancelled status overwrite on race and m…
waleedlatif1 May 15, 2026
a64b338
fix(files): fixed resource spacing on files directories pages (#4618)
waleedlatif1 May 15, 2026
cb9c2d5
improvement(files): validations (#4620)
Sg312 May 15, 2026
0dc1611
improvement(providers): align attachment dispatch to vendor SDK types…
waleedlatif1 May 15, 2026
c403faf
fix(cloudwatch): use PutAlarmMuteRule for mute/unmute with duration w…
TheodoreSpeaks May 15, 2026
8d7bbbc
chore(utils): migrate to shared random/ID utilities and add enforceme…
waleedlatif1 May 16, 2026
93f7be4
improvement(redis): strip idempotency body and cap mothership stream …
waleedlatif1 May 16, 2026
674dd8d
fix(mcp): map validation and conflict orchestration errors to 400/409…
waleedlatif1 May 16, 2026
f76e8e6
improvement(copilot): trim copilot_chats reads to lean projections (#…
waleedlatif1 May 16, 2026
fffb879
feat(wait): Async toggle, chained-wait resume fix, execution status A…
TheodoreSpeaks May 16, 2026
f8ae249
improvement(executor): faster, more responsive workflow cancellation …
waleedlatif1 May 16, 2026
3712d1e
feat(mship): make mship block stream output (#4626)
Sg312 May 16, 2026
ff23546
fix(workflows): exclude block locked from diff detection (#4631)
waleedlatif1 May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/sim/app/(landing)/integrations/data/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
file_v4: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies_v2: FirefliesIcon,
gamma: GammaIcon,
Expand Down
14 changes: 9 additions & 5 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4032,18 +4032,22 @@
"tags": ["meeting", "note-taking"]
},
{
"type": "file_v3",
"type": "file_v4",
"slug": "file",
"name": "File",
"description": "Read and write workspace files",
"longDescription": "Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.",
"description": "Read, fetch, write, and append files",
"longDescription": "Read workspace files by picker or canonical ID, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files.",
"bgColor": "#40916C",
"iconName": "DocumentIcon",
"docsUrl": "https://docs.sim.ai/tools/file",
"operations": [
{
"name": "Read",
"description": "Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)"
"description": "Get a workspace file object from a selected file or canonical workspace file ID."
},
{
"name": "Fetch",
"description": "Parse a file from a URL with optional custom headers for authenticated downloads."
},
{
"name": "Write",
Expand All @@ -4054,7 +4058,7 @@
"description": "Append content to an existing workspace file. The file must already exist. Content is added to the end of the file."
}
],
"operationCount": 3,
"operationCount": 4,
"triggers": [],
"triggerCount": 0,
"authType": "none",
Expand Down
34 changes: 34 additions & 0 deletions apps/sim/app/api/files/parse/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
inputValidationMockFns,
permissionsMock,
permissionsMockFns,
storageServiceMock,
Expand Down Expand Up @@ -310,6 +311,39 @@ describe('File Parse API Route', () => {
expect(data.results).toHaveLength(2)
})

it('should pass custom headers when fetching external URLs', async () => {
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
isValid: true,
resolvedIP: '203.0.113.10',
})
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue(
new Response('private file content', {
status: 200,
headers: { 'content-type': 'text/plain' },
})
)

const headers = { Authorization: 'Bearer xoxb-test-token' }
const req = createMockRequest('POST', {
filePath: 'https://files.slack.com/files-pri/T000-F000/download/report.txt',
headers,
})

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledWith(
'https://files.slack.com/files-pri/T000-F000/download/report.txt',
'203.0.113.10',
expect.objectContaining({
timeout: 30000,
headers,
})
)
})

it('should process execution file URLs with context query param', async () => {
setupFileApiMocks({
cloudEnabled: true,
Expand Down
24 changes: 18 additions & 6 deletions apps/sim/app/api/files/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
if (!parsed.success) return parsed.response

const { filePath, fileType, workspaceId, workflowId, executionId } = parsed.data.body
const { filePath, fileType, headers, workspaceId, workflowId, executionId } = parsed.data.body

if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) {
return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 })
Expand All @@ -128,6 +128,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
workspaceId,
userId,
hasExecutionContext: !!executionContext,
hasHeaders: Boolean(headers && Object.keys(headers).length > 0),
})

if (Array.isArray(filePath)) {
Expand All @@ -146,7 +147,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
fileType,
workspaceId,
userId,
executionContext
executionContext,
headers
)
if (result.metadata) {
result.metadata.processingTime = Date.now() - startTime
Expand Down Expand Up @@ -180,7 +182,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
}

const result = await parseFileSingle(filePath, fileType, workspaceId, userId, executionContext)
const result = await parseFileSingle(
filePath,
fileType,
workspaceId,
userId,
executionContext,
headers
)

if (result.metadata) {
result.metadata.processingTime = Date.now() - startTime
Expand Down Expand Up @@ -225,7 +234,8 @@ async function parseFileSingle(
fileType: string,
workspaceId: string,
userId: string,
executionContext?: ExecutionContext
executionContext?: ExecutionContext,
headers?: Record<string, string>
): Promise<ParseResult> {
logger.info('Parsing file:', filePath)

Expand All @@ -251,7 +261,7 @@ async function parseFileSingle(
}

if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return handleExternalUrl(filePath, fileType, workspaceId, userId, executionContext)
return handleExternalUrl(filePath, fileType, workspaceId, userId, executionContext, headers)
}

if (isUsingCloudStorage()) {
Expand Down Expand Up @@ -298,7 +308,8 @@ async function handleExternalUrl(
fileType: string,
workspaceId: string,
userId: string,
executionContext?: ExecutionContext
executionContext?: ExecutionContext,
headers?: Record<string, string>
): Promise<ParseResult> {
try {
logger.info('Fetching external URL:', url)
Expand Down Expand Up @@ -382,6 +393,7 @@ async function handleExternalUrl(

const response = await secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, {
timeout: DOWNLOAD_TIMEOUT_MS,
...(headers && Object.keys(headers).length > 0 && { headers }),
})
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`)
Expand Down
149 changes: 140 additions & 9 deletions apps/sim/app/api/tools/file/manage/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,100 @@ export const dynamic = 'force-dynamic'

const logger = createLogger('FileManageAPI')

const workspaceFileToUserFile = (file: Awaited<ReturnType<typeof getWorkspaceFile>>) => {
if (!file) return null

return {
id: file.id,
name: file.name,
url: ensureAbsoluteUrl(file.path),
size: file.size,
type: file.type,
key: file.key,
context: 'workspace',
}
}

const fileInputToUserFile = (fileInput: unknown) => {
if (!fileInput || typeof fileInput !== 'object' || Array.isArray(fileInput)) return null

const record = fileInput as Record<string, unknown>
const id =
typeof record.id === 'string'
? record.id.trim()
: typeof record.fileId === 'string'
? record.fileId.trim()
: ''

// Objects with ids are resolved through workspace metadata. This fallback is for
// picker/upload values that only carry storage fields.
if (id) return null

const key = typeof record.key === 'string' ? record.key.trim() : ''
const path = typeof record.path === 'string' ? record.path.trim() : ''
const url = typeof record.url === 'string' ? record.url.trim() : ''
const fileUrl =
url || path || (key ? `/api/files/serve/${encodeURIComponent(key)}?context=workspace` : '')

if (!fileUrl && !key) return null

return {
id: key || fileUrl,
name:
typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'workspace-file',
url: fileUrl ? ensureAbsoluteUrl(fileUrl) : '',
size: typeof record.size === 'number' ? record.size : 0,
type:
typeof record.type === 'string' && record.type.trim()
? record.type.trim()
: 'application/octet-stream',
key,
context: 'workspace',
}
}

const normalizeFileIdList = (value: unknown): string[] => {
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) return []

try {
return normalizeFileIdList(JSON.parse(trimmed))
} catch {
return [trimmed]
}
}

if (!Array.isArray(value)) return []

return value
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((id) => id.length > 0)
}

const extractUserFilesFromInput = (fileInput: unknown) => {
const inputs = Array.isArray(fileInput) ? fileInput : fileInput ? [fileInput] : []
return inputs
.map((input) => fileInputToUserFile(input))
.filter((file): file is NonNullable<ReturnType<typeof fileInputToUserFile>> => Boolean(file))
}

const extractFileIdsFromInput = (fileInput: unknown): string[] => {
const inputs = Array.isArray(fileInput) ? fileInput : fileInput ? [fileInput] : []

return inputs
.flatMap((input) => {
if (typeof input === 'string') return normalizeFileIdList(input)
if (input && typeof input === 'object') {
const record = input as Record<string, unknown>
if (typeof record.id === 'string') return normalizeFileIdList(record.id)
if (typeof record.fileId === 'string') return normalizeFileIdList(record.fileId)
}
return []
})
.filter((id) => id.length > 0)
}

export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
if (!auth.success) {
Expand Down Expand Up @@ -76,15 +170,52 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({
success: true,
data: {
file: {
id: file.id,
name: file.name,
url: ensureAbsoluteUrl(file.path),
size: file.size,
type: file.type,
key: file.key,
context: 'workspace',
},
file: workspaceFileToUserFile(file),
},
})
}

case 'read': {
const { fileId, fileInput } = body
const selectedFileIds = Array.isArray(fileId)
? fileId.map((id) => id.trim()).filter(Boolean)
: fileId
? normalizeFileIdList(fileId)
: extractFileIdsFromInput(fileInput)
const selectedInputFiles = fileId ? [] : extractUserFilesFromInput(fileInput)

if (selectedFileIds.length === 0 && selectedInputFiles.length === 0) {
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
}

const files = await Promise.all(
selectedFileIds.map((id) => getWorkspaceFile(workspaceId, id))
)
const missingFileId = selectedFileIds.find((_, index) => !files[index])
if (missingFileId) {
return NextResponse.json(
{ success: false, error: `File not found: "${missingFileId}"` },
{ status: 404 }
)
}

const userFiles = files
.map((file) => workspaceFileToUserFile(file))
.filter((file): file is NonNullable<ReturnType<typeof workspaceFileToUserFile>> =>
Boolean(file)
)
.concat(selectedInputFiles)

logger.info('Files retrieved', {
count: userFiles.length,
fileIds: userFiles.map((file) => file.id),
})

return NextResponse.json({
success: true,
data: {
file: userFiles[0],
files: userFiles,
},
})
}
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ async function handleExecutePost(
includeFileBase64,
base64MaxBytes,
workflowStateOverride,
executionId: requestedExecutionId,
triggerBlockId: parsedTriggerBlockId,
startBlockId,
stopAfterBlockId,
Expand Down Expand Up @@ -508,7 +509,8 @@ async function handleExecutePost(
)
}

const executionId = generateId()
const executionId =
isClientSession && requestedExecutionId ? requestedExecutionId : generateId()
Comment thread
Sg312 marked this conversation as resolved.
reqLogger = reqLogger.withMetadata({ userId, executionId })

reqLogger.info('Starting server-side execution', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export function useWorkflowExecution() {
size: fileData.file.size,
type: fileData.file.type,
key: result.key,
context: 'execution',
})
} catch (uploadError) {
if (
Expand Down Expand Up @@ -565,6 +566,7 @@ export function useWorkflowExecution() {
size: r.size,
type: r.type,
key: r.key,
context: r.context || 'execution',
uploadedAt: r.uploadedAt,
expiresAt: r.expiresAt,
})
Expand Down Expand Up @@ -1126,6 +1128,7 @@ export function useWorkflowExecution() {
await executionStream.execute({
workflowId: activeWorkflowId,
input: finalWorkflowInput,
executionId,
startBlockId,
selectedOutputs,
triggerType: overrideTriggerType || 'manual',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ export async function executeWorkflowWithFullLogging(
triggerType: options.overrideTriggerType || 'manual',
useDraftState: options.useDraftState ?? true,
isClientSession: true,
...(options.executionId ? { executionId: options.executionId } : {}),
...(options.triggerBlockId ? { triggerBlockId: options.triggerBlockId } : {}),
...(options.stopAfterBlockId ? { stopAfterBlockId: options.stopAfterBlockId } : {}),
...(options.runFromBlock
Expand Down
Loading
Loading