progress on files

This commit is contained in:
Vikhyath Mondreti
2026-02-01 11:14:32 -08:00
parent bea0a685ae
commit 1da3407f41
91 changed files with 1130 additions and 345 deletions

View File

@@ -0,0 +1,134 @@
---
title: Passing Files
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Sim makes it easy to work with files throughout your workflows. Blocks can receive files, process them, and pass them to other blocks seamlessly.
## File Objects
When blocks output files (like Gmail attachments, generated images, or parsed documents), they return a standardized file object:
```json
{
"name": "report.pdf",
"url": "https://...",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf",
"size": 245678
}
```
You can access any of these properties when referencing files from previous blocks.
## Passing Files Between Blocks
Reference files from previous blocks using the tag dropdown. Click in any file input field and type `<` to see available outputs.
**Common patterns:**
```
// Single file from a block
<gmail.attachments[0]>
// Pass the whole file object
<file_parser.files[0]>
// Access specific properties
<gmail.attachments[0].name>
<gmail.attachments[0].base64>
```
Most blocks accept the full file object and extract what they need automatically. You don't need to manually extract `base64` or `url` in most cases.
## Triggering Workflows with Files
When calling a workflow via API that expects file input, include files in your request:
<Tabs items={['Base64', 'URL']}>
<Tab value="Base64">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf"
}
}'
```
</Tab>
<Tab value="URL">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"url": "https://example.com/report.pdf",
"type": "application/pdf"
}
}'
```
</Tab>
</Tabs>
The workflow's Start block should have an input field configured to receive the file parameter.
## Receiving Files in API Responses
When a workflow outputs files, they're included in the response:
```json
{
"success": true,
"output": {
"generatedFile": {
"name": "output.png",
"url": "https://...",
"base64": "iVBORw0KGgo...",
"type": "image/png",
"size": 34567
}
}
}
```
Use `url` for direct downloads or `base64` for inline processing.
## Blocks That Work with Files
**File inputs:**
- **File** - Parse documents, images, and text files
- **Vision** - Analyze images with AI models
- **Mistral Parser** - Extract text from PDFs
**File outputs:**
- **Gmail** - Email attachments
- **Slack** - Downloaded files
- **TTS** - Generated audio files
- **Video Generator** - Generated videos
- **Image Generator** - Generated images
**File storage:**
- **Supabase** - Upload/download from storage
- **S3** - AWS S3 operations
- **Google Drive** - Drive file operations
- **Dropbox** - Dropbox file operations
<Callout type="info">
Files are automatically available to downstream blocks. The execution engine handles all file transfer and format conversion.
</Callout>
## Best Practices
1. **Use file objects directly** - Pass the full file object rather than extracting individual properties. Blocks handle the conversion automatically.
2. **Check file types** - Ensure the file type matches what the receiving block expects. The Vision block needs images, the File block handles documents.
3. **Consider file size** - Large files increase execution time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage.

View File

@@ -1,3 +1,3 @@
{
"pages": ["index", "basics", "api", "logging", "costs"]
"pages": ["index", "basics", "files", "api", "logging", "costs"]
}

View File

@@ -4,6 +4,7 @@ import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateNumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -15,7 +16,7 @@ const DiscordSendMessageSchema = z.object({
botToken: z.string().min(1, 'Bot token is required'),
channelId: z.string().min(1, 'Channel ID is required'),
content: z.string().optional().nullable(),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import {
@@ -28,7 +29,7 @@ const GmailDraftSchema = z.object({
replyToMessageId: z.string().optional().nullable(),
cc: z.string().optional().nullable(),
bcc: z.string().optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
attachments: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import {
@@ -28,7 +29,7 @@ const GmailSendSchema = z.object({
replyToMessageId: z.string().optional().nullable(),
cc: z.string().optional().nullable(),
bcc: z.string().optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
attachments: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import {
@@ -20,7 +21,7 @@ const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/upload/drive/v3/files'
const GoogleDriveUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
fileName: z.string().min(1, 'File name is required'),
file: z.any().optional().nullable(),
file: RawFileInputSchema.optional().nullable(),
mimeType: z.string().optional().nullable(),
folderId: z.string().optional().nullable(),
})

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -16,7 +17,7 @@ const TeamsWriteChannelSchema = z.object({
teamId: z.string().min(1, 'Team ID is required'),
channelId: z.string().min(1, 'Channel ID is required'),
content: z.string().min(1, 'Message content is required'),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -15,7 +16,7 @@ const TeamsWriteChatSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
chatId: z.string().min(1, 'Chat ID is required'),
content: z.string().min(1, 'Message content is required'),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -18,7 +18,8 @@ const logger = createLogger('MistralParseAPI')
const MistralParseSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
filePath: z.string().min(1, 'File path is required'),
filePath: z.string().min(1, 'File path is required').optional(),
fileData: z.unknown().optional(),
resultType: z.string().optional(),
pages: z.array(z.number()).optional(),
includeImageBase64: z.boolean().optional(),
@@ -49,66 +50,96 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validatedData = MistralParseSchema.parse(body)
const fileData = validatedData.fileData
const filePath = typeof fileData === 'string' ? fileData : validatedData.filePath
if (!fileData && (!filePath || filePath.trim() === '')) {
return NextResponse.json(
{
success: false,
error: 'File input is required',
},
{ status: 400 }
)
}
logger.info(`[${requestId}] Mistral parse request`, {
filePath: validatedData.filePath,
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
hasFileData: Boolean(fileData),
filePath,
isWorkspaceFile: filePath ? isInternalFileUrl(filePath) : false,
userId,
})
let fileUrl = validatedData.filePath
const mistralBody: any = {
model: 'mistral-ocr-latest',
}
if (isInternalFileUrl(validatedData.filePath)) {
try {
const storageKey = extractStorageKey(validatedData.filePath)
const context = inferContextFromKey(storageKey)
const hasAccess = await verifyFileAccess(
storageKey,
userId,
undefined, // customConfig
context, // context
false // isLocal
)
if (!hasAccess) {
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
userId,
key: storageKey,
context,
})
return NextResponse.json(
{
success: false,
error: 'File not found',
},
{ status: 404 }
)
}
fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60)
logger.info(`[${requestId}] Generated presigned URL for ${context} file`)
} catch (error) {
logger.error(`[${requestId}] Failed to generate presigned URL:`, error)
if (fileData && typeof fileData === 'object') {
const base64 = (fileData as { base64?: string }).base64
const mimeType = (fileData as { type?: string }).type || 'application/pdf'
if (!base64) {
return NextResponse.json(
{
success: false,
error: 'Failed to generate file access URL',
error: 'File base64 content is required',
},
{ status: 500 }
{ status: 400 }
)
}
} else if (validatedData.filePath?.startsWith('/')) {
const baseUrl = getBaseUrl()
fileUrl = `${baseUrl}${validatedData.filePath}`
}
const base64Payload = base64.startsWith('data:')
? base64
: `data:${mimeType};base64,${base64}`
mistralBody.document = {
type: 'document_base64',
document_base64: base64Payload,
}
} else if (filePath) {
let fileUrl = filePath
const mistralBody: any = {
model: 'mistral-ocr-latest',
document: {
if (isInternalFileUrl(filePath)) {
try {
const storageKey = extractStorageKey(filePath)
const context = inferContextFromKey(storageKey)
const hasAccess = await verifyFileAccess(storageKey, userId, undefined, context, false)
if (!hasAccess) {
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
userId,
key: storageKey,
context,
})
return NextResponse.json(
{
success: false,
error: 'File not found',
},
{ status: 404 }
)
}
fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60)
logger.info(`[${requestId}] Generated presigned URL for ${context} file`)
} catch (error) {
logger.error(`[${requestId}] Failed to generate presigned URL:`, error)
return NextResponse.json(
{
success: false,
error: 'Failed to generate file access URL',
},
{ status: 500 }
)
}
} else if (filePath.startsWith('/')) {
const baseUrl = getBaseUrl()
fileUrl = `${baseUrl}${filePath}`
}
mistralBody.document = {
type: 'document_url',
document_url: fileUrl,
},
}
}
if (validatedData.pages) {

View File

@@ -5,6 +5,7 @@ import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import {
getExtensionFromMimeType,
processSingleFileToUserFile,
@@ -29,7 +30,7 @@ const ExcelValuesSchema = z.union([
const OneDriveUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
fileName: z.string().min(1, 'File name is required'),
file: z.any().optional(),
file: RawFileInputSchema.optional(),
folderId: z.string().optional().nullable(),
mimeType: z.string().nullish(),
values: ExcelValuesSchema.optional().nullable(),
@@ -88,25 +89,9 @@ export async function POST(request: NextRequest) {
)
}
let fileToProcess
if (Array.isArray(rawFile)) {
if (rawFile.length === 0) {
return NextResponse.json(
{
success: false,
error: 'No file provided',
},
{ status: 400 }
)
}
fileToProcess = rawFile[0]
} else {
fileToProcess = rawFile
}
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
userFile = processSingleFileToUserFile(rawFile, requestId, logger)
} catch (error) {
return NextResponse.json(
{

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -18,7 +19,7 @@ const OutlookDraftSchema = z.object({
contentType: z.enum(['text', 'html']).optional().nullable(),
cc: z.string().optional().nullable(),
bcc: z.string().optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
attachments: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -20,7 +21,7 @@ const OutlookSendSchema = z.object({
bcc: z.string().optional().nullable(),
replyToMessageId: z.string().optional().nullable(),
conversationId: z.string().optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
attachments: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -17,7 +18,7 @@ const S3PutObjectSchema = z.object({
region: z.string().min(1, 'Region is required'),
bucketName: z.string().min(1, 'Bucket name is required'),
objectKey: z.string().min(1, 'Object key is required'),
file: z.any().optional().nullable(),
file: RawFileInputSchema.optional().nullable(),
content: z.string().optional().nullable(),
contentType: z.string().optional().nullable(),
acl: z.string().optional().nullable(),

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import {
@@ -26,14 +27,7 @@ const UploadSchema = z.object({
privateKey: z.string().nullish(),
passphrase: z.string().nullish(),
remotePath: z.string().min(1, 'Remote path is required'),
files: z
.union([z.array(z.any()), z.string(), z.number(), z.null(), z.undefined()])
.transform((val) => {
if (Array.isArray(val)) return val
if (val === null || val === undefined || val === '') return undefined
return undefined
})
.nullish(),
files: RawFileInputArraySchema.optional().nullable(),
fileContent: z.string().nullish(),
fileName: z.string().nullish(),
overwrite: z.boolean().default(true),

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -16,7 +17,7 @@ const SharepointUploadSchema = z.object({
driveId: z.string().optional().nullable(),
folderPath: z.string().optional().nullable(),
fileName: z.string().optional().nullable(),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { sendSlackMessage } from '../utils'
export const dynamic = 'force-dynamic'
@@ -16,7 +17,7 @@ const SlackSendMessageSchema = z
userId: z.string().optional().nullable(),
text: z.string().min(1, 'Message text is required'),
thread_ts: z.string().optional().nullable(),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
})
.refine((data) => data.channel || data.userId, {
message: 'Either channel or userId is required',

View File

@@ -4,6 +4,7 @@ import nodemailer from 'nodemailer'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -28,7 +29,7 @@ const SmtpSendSchema = z.object({
cc: z.string().optional().nullable(),
bcc: z.string().optional().nullable(),
replyTo: z.string().optional().nullable(),
attachments: z.array(z.any()).optional().nullable(),
attachments: RawFileInputArraySchema.optional().nullable(),
})
export async function POST(request: NextRequest) {

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -16,7 +17,7 @@ const SupabaseStorageUploadSchema = z.object({
bucket: z.string().min(1, 'Bucket name is required'),
fileName: z.string().min(1, 'File name is required'),
path: z.string().optional().nullable(),
fileData: z.any(),
fileData: FileInputSchema,
contentType: z.string().optional().nullable(),
upsert: z.boolean().optional().default(false),
})

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { convertMarkdownToHTML } from '@/tools/telegram/utils'
@@ -14,7 +15,7 @@ const logger = createLogger('TelegramSendDocumentAPI')
const TelegramSendDocumentSchema = z.object({
botToken: z.string().min(1, 'Bot token is required'),
chatId: z.string().min(1, 'Chat ID is required'),
files: z.array(z.any()).optional().nullable(),
files: RawFileInputArraySchema.optional().nullable(),
caption: z.string().optional().nullable(),
})

View File

@@ -1,10 +1,13 @@
import { GoogleGenAI } from '@google/genai'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { convertUsageMetadata, extractTextContent } from '@/providers/google/utils'
export const dynamic = 'force-dynamic'
@@ -13,8 +16,8 @@ const logger = createLogger('VisionAnalyzeAPI')
const VisionAnalyzeSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
imageUrl: z.string().optional().nullable(),
imageFile: z.any().optional().nullable(),
model: z.string().optional().default('gpt-4o'),
imageFile: RawFileInputSchema.optional().nullable(),
model: z.string().optional().default('gpt-5.2'),
prompt: z.string().optional().nullable(),
})
@@ -88,7 +91,8 @@ export async function POST(request: NextRequest) {
const defaultPrompt = 'Please analyze this image and describe what you see in detail.'
const prompt = validatedData.prompt || defaultPrompt
const isClaude = validatedData.model.startsWith('claude-3')
const isClaude = validatedData.model.startsWith('claude-')
const isGemini = validatedData.model.startsWith('gemini-')
const apiUrl = isClaude
? 'https://api.anthropic.com/v1/messages'
: 'https://api.openai.com/v1/chat/completions'
@@ -106,6 +110,65 @@ export async function POST(request: NextRequest) {
let requestBody: any
if (isGemini) {
let base64Payload = imageSource
if (!base64Payload.startsWith('data:')) {
const response = await fetch(base64Payload)
if (!response.ok) {
return NextResponse.json(
{ success: false, error: 'Failed to fetch image for Gemini' },
{ status: 400 }
)
}
const contentType =
response.headers.get('content-type') || validatedData.imageFile?.type || 'image/jpeg'
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
base64Payload = `data:${contentType};base64,${base64}`
}
const base64Marker = ';base64,'
const markerIndex = base64Payload.indexOf(base64Marker)
if (!base64Payload.startsWith('data:') || markerIndex === -1) {
return NextResponse.json(
{ success: false, error: 'Invalid base64 image format' },
{ status: 400 }
)
}
const rawMimeType = base64Payload.slice('data:'.length, markerIndex)
const mediaType = rawMimeType.split(';')[0] || 'image/jpeg'
const base64Data = base64Payload.slice(markerIndex + base64Marker.length)
if (!base64Data) {
return NextResponse.json(
{ success: false, error: 'Invalid base64 image format' },
{ status: 400 }
)
}
const ai = new GoogleGenAI({ apiKey: validatedData.apiKey })
const geminiResponse = await ai.models.generateContent({
model: validatedData.model,
contents: [
{
role: 'user',
parts: [{ text: prompt }, { inlineData: { mimeType: mediaType, data: base64Data } }],
},
],
})
const content = extractTextContent(geminiResponse.candidates?.[0])
const usage = convertUsageMetadata(geminiResponse.usageMetadata)
return NextResponse.json({
success: true,
output: {
content,
model: validatedData.model,
tokens: usage.totalTokenCount || undefined,
},
})
}
if (isClaude) {
if (imageSource.startsWith('data:')) {
const base64Match = imageSource.match(/^data:([^;]+);base64,(.+)$/)
@@ -172,7 +235,7 @@ export async function POST(request: NextRequest) {
],
},
],
max_tokens: 1000,
max_completion_tokens: 1000,
}
}

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import {
getFileExtension,
getMimeTypeFromExtension,
@@ -19,7 +20,7 @@ const WORDPRESS_COM_API_BASE = 'https://public-api.wordpress.com/wp/v2/sites'
const WordPressUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
siteId: z.string().min(1, 'Site ID is required'),
file: z.any().optional().nullable(),
file: RawFileInputSchema.optional().nullable(),
filename: z.string().optional().nullable(),
title: z.string().optional().nullable(),
caption: z.string().optional().nullable(),

View File

@@ -179,7 +179,7 @@ export function A2aDeploy({
newFields.push({
id: crypto.randomUUID(),
name: 'files',
type: 'files',
type: 'file[]',
value: '',
collapsed: false,
})

View File

@@ -26,7 +26,7 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/
interface Field {
id: string
name: string
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]'
value?: string
description?: string
collapsed?: boolean
@@ -57,7 +57,7 @@ const TYPE_OPTIONS: ComboboxOption[] = [
{ label: 'Boolean', value: 'boolean' },
{ label: 'Object', value: 'object' },
{ label: 'Array', value: 'array' },
{ label: 'Files', value: 'files' },
{ label: 'Files', value: 'file[]' },
]
/**
@@ -448,7 +448,7 @@ export function FieldFormat({
)
}
if (field.type === 'files') {
if (field.type === 'file[]') {
const lineCount = fieldValue.split('\n').length
const gutterWidth = calculateGutterWidth(lineCount)

View File

@@ -1746,7 +1746,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
mergedSubBlocks
)
if (fieldType === 'files' || fieldType === 'file[]' || fieldType === 'array') {
if (fieldType === 'file' || fieldType === 'file[]' || fieldType === 'array') {
const blockName = parts[0]
const remainingPath = parts.slice(2).join('.')
processedTag = `${blockName}.${arrayFieldName}[0].${remainingPath}`

View File

@@ -188,7 +188,7 @@ export function useBlockOutputFields({
baseOutputs = {
input: { type: 'string', description: 'User message' },
conversationId: { type: 'string', description: 'Conversation ID' },
files: { type: 'files', description: 'Uploaded files' },
files: { type: 'file[]', description: 'Uploaded files' },
}
} else {
const inputFormatValue = mergedSubBlocks?.inputFormat?.value

View File

@@ -417,11 +417,11 @@ async function executeWebhookJobInternal(
if (triggerBlock?.subBlocks?.inputFormat?.value) {
const inputFormat = triggerBlock.subBlocks.inputFormat.value as unknown as Array<{
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]'
}>
logger.debug(`[${requestId}] Processing generic webhook files from inputFormat`)
const fileFields = inputFormat.filter((field) => field.type === 'files')
const fileFields = inputFormat.filter((field) => field.type === 'file[]')
if (fileFields.length > 0 && typeof input === 'object' && input !== null) {
const executionContext = {

View File

@@ -26,7 +26,7 @@ export const ChatTriggerBlock: BlockConfig = {
outputs: {
input: { type: 'string', description: 'User message' },
conversationId: { type: 'string', description: 'Conversation ID' },
files: { type: 'files', description: 'Uploaded files' },
files: { type: 'file[]', description: 'Uploaded files' },
},
triggers: {
enabled: true,

View File

@@ -60,12 +60,25 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
required: true,
},
{
id: 'fileContent',
title: 'File Content',
type: 'long-input',
placeholder: 'Base64 encoded file content or file reference',
condition: { field: 'operation', value: 'dropbox_upload' },
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'fileContent',
placeholder: 'Upload file to send to Dropbox',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'dropbox_upload' },
},
{
id: 'fileContent',
title: 'File',
type: 'short-input',
canonicalParamId: 'fileContent',
placeholder: 'Reference file from previous blocks',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'dropbox_upload' },
},
{
id: 'mode',
@@ -337,7 +350,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
path: { type: 'string', description: 'Path in Dropbox' },
autorename: { type: 'boolean', description: 'Auto-rename on conflict' },
// Upload inputs
fileContent: { type: 'string', description: 'Base64 encoded file content' },
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
fileContent: { type: 'json', description: 'File reference or UserFile object' },
fileName: { type: 'string', description: 'Optional filename' },
mode: { type: 'string', description: 'Write mode: add or overwrite' },
mute: { type: 'boolean', description: 'Mute notifications' },
@@ -360,7 +374,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
},
outputs: {
// Upload/Download outputs
file: { type: 'json', description: 'File metadata' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
content: { type: 'string', description: 'File content (base64)' },
temporaryLink: { type: 'string', description: 'Temporary download link' },
// List folder outputs

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { DocumentIcon } from '@/components/icons'
import type { BlockConfig, SubBlockType } from '@/blocks/types'
import { createVersionedToolSelector } from '@/blocks/utils'
import type { FileParserOutput } from '@/tools/file/types'
import type { FileParserOutput, FileParserV3Output } from '@/tools/file/types'
const logger = createLogger('FileBlock')
@@ -116,7 +116,7 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
},
outputs: {
files: {
type: 'json',
type: 'file[]',
description: 'Array of parsed file objects with content, metadata, and file properties',
},
combinedContent: {
@@ -124,7 +124,7 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
description: 'All file contents merged into a single text string',
},
processedFiles: {
type: 'files',
type: 'file[]',
description: 'Array of UserFile objects for downstream use (attachments, uploads, etc.)',
},
},
@@ -133,9 +133,9 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
export const FileV2Block: BlockConfig<FileParserOutput> = {
...FileBlock,
type: 'file_v2',
name: 'File',
name: 'File (Legacy)',
description: 'Read and parse multiple files',
hideFromToolbar: false,
hideFromToolbar: true,
subBlocks: [
{
id: 'file',
@@ -209,7 +209,7 @@ export const FileV2Block: BlockConfig<FileParserOutput> = {
},
outputs: {
files: {
type: 'json',
type: 'file[]',
description: 'Array of parsed file objects with content, metadata, and file properties',
},
combinedContent: {
@@ -218,3 +218,110 @@ export const FileV2Block: BlockConfig<FileParserOutput> = {
},
},
}
export const FileV3Block: BlockConfig<FileParserV3Output> = {
type: 'file_v3',
name: 'File',
description: 'Read and parse multiple files',
longDescription: 'Upload files or reference files from previous blocks to extract text content.',
docsLink: 'https://docs.sim.ai/tools/file',
category: 'tools',
bgColor: '#40916C',
icon: DocumentIcon,
subBlocks: [
{
id: 'file',
title: 'Files',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'fileInput',
acceptedTypes:
'.pdf,.csv,.doc,.docx,.txt,.md,.xlsx,.xls,.html,.htm,.pptx,.ppt,.json,.xml,.rtf',
placeholder: 'Upload files to process',
multiple: true,
mode: 'basic',
maxSize: 100,
required: true,
},
{
id: 'fileRef',
title: 'Files',
type: 'short-input' as SubBlockType,
canonicalParamId: 'fileInput',
placeholder: 'File reference from previous block',
mode: 'advanced',
required: true,
},
],
tools: {
access: ['file_parser_v3'],
config: {
tool: () => 'file_parser_v3',
params: (params) => {
const fileInput = params.fileInput ?? params.file ?? params.filePath
if (!fileInput) {
logger.error('No file input provided')
throw new Error('File input is required')
}
if (typeof fileInput === 'string') {
return {
filePath: fileInput.trim(),
fileType: params.fileType || 'auto',
workspaceId: params._context?.workspaceId,
workflowId: params._context?.workflowId,
executionId: params._context?.executionId,
}
}
if (Array.isArray(fileInput)) {
const filePaths = fileInput
.map((file) => (file as { url?: string; path?: string }).url || file.path)
.filter((path): path is string => Boolean(path))
if (filePaths.length === 0) {
logger.error('No valid file paths found in file input array')
throw new Error('File input is required')
}
return {
filePath: filePaths.length === 1 ? filePaths[0] : filePaths,
fileType: params.fileType || 'auto',
workspaceId: params._context?.workspaceId,
workflowId: params._context?.workflowId,
executionId: params._context?.executionId,
}
}
if (typeof fileInput === 'object') {
const filePath = (fileInput as { url?: string; path?: string }).url || fileInput.path
if (!filePath) {
logger.error('File input object missing path or url')
throw new Error('File input is required')
}
return {
filePath,
fileType: params.fileType || 'auto',
workspaceId: params._context?.workspaceId,
workflowId: params._context?.workflowId,
executionId: params._context?.executionId,
}
}
logger.error('Invalid file input format')
throw new Error('File input is required')
},
},
},
inputs: {
fileInput: { type: 'json', description: 'File input (upload or UserFile reference)' },
fileType: { type: 'string', description: 'File type' },
},
outputs: {
files: {
type: 'file[]',
description: 'Parsed files as UserFile objects',
},
combinedContent: {
type: 'string',
description: 'All file contents merged into a single text string',
},
},
}

View File

@@ -516,7 +516,7 @@ Return ONLY the search query - no explanations, no extra text.`,
// Tool outputs
content: { type: 'string', description: 'Response content' },
metadata: { type: 'json', description: 'Email metadata' },
attachments: { type: 'json', description: 'Email attachments array' },
attachments: { type: 'file[]', description: 'Email attachments array' },
// Trigger outputs
email_id: { type: 'string', description: 'Gmail message ID' },
thread_id: { type: 'string', description: 'Gmail thread ID' },
@@ -579,7 +579,7 @@ export const GmailV2Block: BlockConfig<GmailToolResponse> = {
date: { type: 'string', description: 'Date' },
body: { type: 'string', description: 'Email body text (best-effort)' },
results: { type: 'json', description: 'Search/read summary results' },
attachments: { type: 'json', description: 'Downloaded attachments (if enabled)' },
attachments: { type: 'file[]', description: 'Downloaded attachments (if enabled)' },
// Draft-specific outputs
draftId: {

View File

@@ -861,7 +861,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
permissionId: { type: 'string', description: 'Permission ID to remove' },
},
outputs: {
file: { type: 'json', description: 'File metadata or downloaded file data' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
files: { type: 'json', description: 'List of files' },
metadata: { type: 'json', description: 'Complete file metadata (from download)' },
content: { type: 'string', description: 'File content as text' },

View File

@@ -315,12 +315,26 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
required: true,
},
{
id: 'imageUrl',
title: 'Image URL',
type: 'short-input',
placeholder: 'Public URL of the image (PNG, JPEG, or GIF)',
condition: { field: 'operation', value: 'add_image' },
id: 'imageFile',
title: 'Image',
type: 'file-upload',
canonicalParamId: 'imageSource',
placeholder: 'Upload image (PNG, JPEG, or GIF)',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.png,.jpg,.jpeg,.gif',
condition: { field: 'operation', value: 'add_image' },
},
{
id: 'imageUrl',
title: 'Image',
type: 'short-input',
canonicalParamId: 'imageSource',
placeholder: 'Reference image from previous blocks or enter URL',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'add_image' },
},
{
id: 'imageWidth',
@@ -809,7 +823,9 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
placeholderIdMappings: { type: 'string', description: 'JSON array of placeholder ID mappings' },
// Add image operation
pageObjectId: { type: 'string', description: 'Slide object ID for image' },
imageUrl: { type: 'string', description: 'Image URL' },
imageFile: { type: 'json', description: 'Uploaded image (UserFile)' },
imageUrl: { type: 'string', description: 'Image URL or reference' },
imageSource: { type: 'json', description: 'Image source (file or URL)' },
imageWidth: { type: 'number', description: 'Image width in points' },
imageHeight: { type: 'number', description: 'Image height in points' },
positionX: { type: 'number', description: 'X position in points' },

View File

@@ -526,7 +526,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
description:
'Single hold object (for create_matters_holds or list_matters_holds with holdId)',
},
file: { type: 'json', description: 'Downloaded export file (UserFile) from execution files' },
file: { type: 'file', description: 'Downloaded export file (UserFile) from execution files' },
nextPageToken: {
type: 'string',
description: 'Token for fetching next page of results (for list operations)',

View File

@@ -149,7 +149,7 @@ export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
},
outputs: {
content: { type: 'string', description: 'Generation response' },
image: { type: 'string', description: 'Generated image URL' },
image: { type: 'file', description: 'Generated image file (UserFile)' },
metadata: { type: 'json', description: 'Generation metadata' },
},
}

View File

@@ -44,7 +44,7 @@ export const ImapBlock: BlockConfig = {
bodyHtml: { type: 'string', description: 'HTML email body' },
mailbox: { type: 'string', description: 'Mailbox/folder where email was received' },
hasAttachments: { type: 'boolean', description: 'Whether email has attachments' },
attachments: { type: 'json', description: 'Array of email attachments' },
attachments: { type: 'file[]', description: 'Array of email attachments' },
timestamp: { type: 'string', description: 'Event timestamp' },
},
triggers: {

View File

@@ -1049,7 +1049,7 @@ Return ONLY the comment text - no explanations.`,
// jira_get_attachments outputs
attachments: {
type: 'json',
type: 'file[]',
description: 'Array of attachments with id, filename, size, mimeType, created, author',
},

View File

@@ -2341,7 +2341,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
cycles: { type: 'json', description: 'Cycles list' },
// Attachment outputs
attachment: { type: 'json', description: 'Attachment data' },
attachments: { type: 'json', description: 'Attachments list' },
attachments: { type: 'file[]', description: 'Attachments list' },
// Relation outputs
relation: { type: 'json', description: 'Issue relation data' },
relations: { type: 'json', description: 'Issue relations list' },

View File

@@ -462,7 +462,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
messages: { type: 'json', description: 'Array of message objects' },
totalAttachments: { type: 'number', description: 'Total number of attachments' },
attachmentTypes: { type: 'json', description: 'Array of attachment content types' },
attachments: { type: 'array', description: 'Downloaded message attachments' },
attachments: { type: 'file[]', description: 'Downloaded message attachments' },
updatedContent: {
type: 'boolean',
description: 'Whether content was successfully updated/sent',

View File

@@ -159,14 +159,16 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
placeholder: 'Upload a PDF document',
mode: 'basic',
maxSize: 50,
required: true,
},
{
id: 'filePath',
title: 'PDF Document',
title: 'File Reference',
type: 'short-input' as SubBlockType,
canonicalParamId: 'document',
placeholder: 'Document URL',
placeholder: 'File reference from previous block',
mode: 'advanced',
required: true,
},
{
id: 'resultType',
@@ -216,7 +218,7 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
throw new Error('PDF document is required')
}
if (typeof documentInput === 'object') {
parameters.fileUpload = documentInput
parameters.fileData = documentInput
} else if (typeof documentInput === 'string') {
parameters.filePath = documentInput.trim()
}
@@ -254,8 +256,8 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'PDF document URL (advanced mode)' },
document: { type: 'json', description: 'Document input (file upload or file reference)' },
filePath: { type: 'string', description: 'File reference (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' },
apiKey: { type: 'string', description: 'Mistral API key' },
resultType: { type: 'string', description: 'Output format type' },

View File

@@ -393,7 +393,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
deleted: { type: 'boolean', description: 'Whether the file was deleted' },
fileId: { type: 'string', description: 'The ID of the deleted file' },
file: {
type: 'json',
type: 'file',
description: 'The OneDrive file object, including details such as id, name, size, and more.',
},
files: {

View File

@@ -440,7 +440,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
sentDateTime: { type: 'string', description: 'Email sent timestamp' },
hasAttachments: { type: 'boolean', description: 'Whether email has attachments' },
attachments: {
type: 'json',
type: 'file[]',
description: 'Email attachments (if includeAttachments is enabled)',
},
isRead: { type: 'boolean', description: 'Whether email is read' },

View File

@@ -803,7 +803,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
outputs: {
deals: { type: 'json', description: 'Array of deal objects' },
deal: { type: 'json', description: 'Single deal object' },
files: { type: 'json', description: 'Array of file objects' },
files: { type: 'file[]', description: 'Array of file objects' },
messages: { type: 'json', description: 'Array of mail message objects' },
pipelines: { type: 'json', description: 'Array of pipeline objects' },
projects: { type: 'json', description: 'Array of project objects' },

View File

@@ -418,6 +418,7 @@ export const S3Block: BlockConfig<S3Response> = {
type: 'string',
description: 'S3 URI (s3://bucket/key) for use with other AWS services',
},
file: { type: 'file', description: 'Downloaded file stored in execution files' },
objects: { type: 'json', description: 'List of objects (for list operation)' },
deleted: { type: 'boolean', description: 'Deletion status' },
metadata: { type: 'json', description: 'Operation metadata' },

View File

@@ -522,7 +522,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description: 'Array of SharePoint list items with fields',
},
uploadedFiles: {
type: 'json',
type: 'file[]',
description: 'Array of uploaded file objects with id, name, webUrl, size',
},
fileCount: {

View File

@@ -859,7 +859,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// slack_download outputs
file: {
type: 'json',
type: 'file',
description: 'Downloaded file stored in execution files',
},

View File

@@ -450,10 +450,24 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
// === PLAYLIST COVER ===
{
id: 'imageBase64',
title: 'Image (Base64)',
type: 'long-input',
placeholder: 'Base64-encoded JPEG image (max 256KB)',
id: 'coverImageFile',
title: 'Cover Image',
type: 'file-upload',
canonicalParamId: 'coverImage',
placeholder: 'Upload cover image (JPEG, max 256KB)',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.jpg,.jpeg',
condition: { field: 'operation', value: 'spotify_add_playlist_cover' },
},
{
id: 'coverImageRef',
title: 'Cover Image',
type: 'short-input',
canonicalParamId: 'coverImage',
placeholder: 'Reference image from previous blocks',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'spotify_add_playlist_cover' },
},
@@ -804,7 +818,9 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
newName: { type: 'string', description: 'New playlist name' },
description: { type: 'string', description: 'Playlist description' },
public: { type: 'boolean', description: 'Whether playlist is public' },
imageBase64: { type: 'string', description: 'Base64-encoded JPEG image' },
coverImage: { type: 'json', description: 'Cover image (UserFile)' },
coverImageFile: { type: 'json', description: 'Cover image upload (basic mode)' },
coverImageRef: { type: 'json', description: 'Cover image reference (advanced mode)' },
range_start: { type: 'number', description: 'Start index for reorder' },
insert_before: { type: 'number', description: 'Insert before index' },
range_length: { type: 'number', description: 'Number of items to move' },

View File

@@ -675,9 +675,9 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
{
id: 'fileContent',
title: 'File Content',
type: 'code',
type: 'short-input',
canonicalParamId: 'fileData',
placeholder: 'Base64 encoded for binary files, or plain text',
placeholder: 'File reference from previous block',
condition: { field: 'operation', value: 'storage_upload' },
mode: 'advanced',
required: true,
@@ -1173,7 +1173,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
description: 'Row count for count operations',
},
file: {
type: 'files',
type: 'file',
description: 'Downloaded file stored in execution files',
},
publicUrl: {

View File

@@ -65,39 +65,91 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
required: true,
condition: { field: 'operation', value: 'telegram_message' },
},
{
id: 'photoFile',
title: 'Photo',
type: 'file-upload',
canonicalParamId: 'photo',
placeholder: 'Upload photo',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.jpg,.jpeg,.png,.gif,.webp',
condition: { field: 'operation', value: 'telegram_send_photo' },
},
{
id: 'photo',
title: 'Photo',
type: 'short-input',
placeholder: 'Enter photo URL or file_id',
description: 'Photo to send. Pass a file_id or HTTP URL',
canonicalParamId: 'photo',
placeholder: 'Reference photo from previous blocks or enter URL/file_id',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'telegram_send_photo' },
},
{
id: 'videoFile',
title: 'Video',
type: 'file-upload',
canonicalParamId: 'video',
placeholder: 'Upload video',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.mp4,.mov,.avi,.mkv,.webm',
condition: { field: 'operation', value: 'telegram_send_video' },
},
{
id: 'video',
title: 'Video',
type: 'short-input',
placeholder: 'Enter video URL or file_id',
description: 'Video to send. Pass a file_id or HTTP URL',
canonicalParamId: 'video',
placeholder: 'Reference video from previous blocks or enter URL/file_id',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'telegram_send_video' },
},
{
id: 'audioFile',
title: 'Audio',
type: 'file-upload',
canonicalParamId: 'audio',
placeholder: 'Upload audio',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.mp3,.m4a,.wav,.ogg,.flac',
condition: { field: 'operation', value: 'telegram_send_audio' },
},
{
id: 'audio',
title: 'Audio',
type: 'short-input',
placeholder: 'Enter audio URL or file_id',
description: 'Audio file to send. Pass a file_id or HTTP URL',
canonicalParamId: 'audio',
placeholder: 'Reference audio from previous blocks or enter URL/file_id',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'telegram_send_audio' },
},
{
id: 'animationFile',
title: 'Animation',
type: 'file-upload',
canonicalParamId: 'animation',
placeholder: 'Upload animation (GIF)',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.gif,.mp4',
condition: { field: 'operation', value: 'telegram_send_animation' },
},
{
id: 'animation',
title: 'Animation',
type: 'short-input',
placeholder: 'Enter animation URL or file_id',
description: 'Animation (GIF) to send. Pass a file_id or HTTP URL',
canonicalParamId: 'animation',
placeholder: 'Reference animation from previous blocks or enter URL/file_id',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'telegram_send_animation' },
},
@@ -215,42 +267,50 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
...commonParams,
messageId: params.messageId,
}
case 'telegram_send_photo':
if (!params.photo) {
throw new Error('Photo URL or file_id is required.')
case 'telegram_send_photo': {
const photoSource = params.photoFile || params.photo
if (!photoSource) {
throw new Error('Photo is required.')
}
return {
...commonParams,
photo: params.photo,
photo: photoSource,
caption: params.caption,
}
case 'telegram_send_video':
if (!params.video) {
throw new Error('Video URL or file_id is required.')
}
case 'telegram_send_video': {
const videoSource = params.videoFile || params.video
if (!videoSource) {
throw new Error('Video is required.')
}
return {
...commonParams,
video: params.video,
video: videoSource,
caption: params.caption,
}
case 'telegram_send_audio':
if (!params.audio) {
throw new Error('Audio URL or file_id is required.')
}
case 'telegram_send_audio': {
const audioSource = params.audioFile || params.audio
if (!audioSource) {
throw new Error('Audio is required.')
}
return {
...commonParams,
audio: params.audio,
audio: audioSource,
caption: params.caption,
}
case 'telegram_send_animation':
if (!params.animation) {
throw new Error('Animation URL or file_id is required.')
}
case 'telegram_send_animation': {
const animationSource = params.animationFile || params.animation
if (!animationSource) {
throw new Error('Animation is required.')
}
return {
...commonParams,
animation: params.animation,
animation: animationSource,
caption: params.caption,
}
}
case 'telegram_send_document': {
// Handle file upload
const fileParam = params.attachmentFiles || params.files
@@ -274,10 +334,14 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
botToken: { type: 'string', description: 'Telegram bot token' },
chatId: { type: 'string', description: 'Chat identifier' },
text: { type: 'string', description: 'Message text' },
photo: { type: 'string', description: 'Photo URL or file_id' },
video: { type: 'string', description: 'Video URL or file_id' },
audio: { type: 'string', description: 'Audio URL or file_id' },
animation: { type: 'string', description: 'Animation URL or file_id' },
photoFile: { type: 'json', description: 'Uploaded photo (UserFile)' },
photo: { type: 'json', description: 'Photo reference or URL/file_id' },
videoFile: { type: 'json', description: 'Uploaded video (UserFile)' },
video: { type: 'json', description: 'Video reference or URL/file_id' },
audioFile: { type: 'json', description: 'Uploaded audio (UserFile)' },
audio: { type: 'json', description: 'Audio reference or URL/file_id' },
animationFile: { type: 'json', description: 'Uploaded animation (UserFile)' },
animation: { type: 'json', description: 'Animation reference or URL/file_id' },
attachmentFiles: {
type: 'json',
description: 'Files to attach (UI upload)',

View File

@@ -578,7 +578,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
outputs: {
audioUrl: { type: 'string', description: 'URL to the generated audio file' },
audioFile: { type: 'json', description: 'Generated audio file object (UserFile)' },
audioFile: { type: 'file', description: 'Generated audio file object (UserFile)' },
duration: {
type: 'number',
description: 'Audio duration in seconds',

View File

@@ -420,7 +420,7 @@ export const VideoGeneratorBlock: BlockConfig<VideoBlockResponse> = {
outputs: {
videoUrl: { type: 'string', description: 'Generated video URL' },
videoFile: { type: 'json', description: 'Video file object with metadata' },
videoFile: { type: 'file', description: 'Video file object with metadata' },
duration: { type: 'number', description: 'Video duration in seconds' },
width: { type: 'number', description: 'Video width in pixels' },
height: { type: 'number', description: 'Video height in pixels' },

View File

@@ -3,10 +3,27 @@ import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { VisionResponse } from '@/tools/vision/types'
const VISION_MODEL_OPTIONS = [
{ label: 'GPT 5.2', id: 'gpt-5.2' },
{ label: 'GPT 5.1', id: 'gpt-5.1' },
{ label: 'GPT 5', id: 'gpt-5' },
{ label: 'GPT 5 Mini', id: 'gpt-5-mini' },
{ label: 'GPT 5 Nano', id: 'gpt-5-nano' },
{ label: 'Claude Opus 4.5', id: 'claude-opus-4-5' },
{ label: 'Claude Sonnet 4.5', id: 'claude-sonnet-4-5' },
{ label: 'Claude Haiku 4.5', id: 'claude-haiku-4-5' },
{ label: 'Gemini 3 Pro Preview', id: 'gemini-3-pro-preview' },
{ label: 'Gemini 3 Flash Preview', id: 'gemini-3-flash-preview' },
{ label: 'Gemini 2.5 Pro', id: 'gemini-2.5-pro' },
{ label: 'Gemini 2.5 Flash', id: 'gemini-2.5-flash' },
{ label: 'Gemini 2.5 Flash Lite', id: 'gemini-2.5-flash-lite' },
]
export const VisionBlock: BlockConfig<VisionResponse> = {
type: 'vision',
name: 'Vision',
name: 'Vision (Legacy)',
description: 'Analyze images with vision models',
hideFromToolbar: true,
authMode: AuthMode.ApiKey,
longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.',
docsLink: 'https://docs.sim.ai/tools/vision',
@@ -47,12 +64,8 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
id: 'model',
title: 'Vision Model',
type: 'dropdown',
options: [
{ label: 'gpt-4o', id: 'gpt-4o' },
{ label: 'claude-3-opus', id: 'claude-3-opus-20240229' },
{ label: 'claude-3-sonnet', id: 'claude-3-sonnet-20240229' },
],
value: () => 'gpt-4o',
options: VISION_MODEL_OPTIONS,
value: () => 'gpt-5.2',
},
{
id: 'prompt',
@@ -87,3 +100,62 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
tokens: { type: 'number', description: 'Token usage' },
},
}
export const VisionV2Block: BlockConfig<VisionResponse> = {
...VisionBlock,
type: 'vision_v2',
name: 'Vision',
description: 'Analyze images with vision models',
hideFromToolbar: false,
subBlocks: [
{
id: 'imageFile',
title: 'Image File',
type: 'file-upload',
canonicalParamId: 'imageFile',
placeholder: 'Upload an image file',
mode: 'basic',
multiple: false,
required: true,
acceptedTypes: '.jpg,.jpeg,.png,.gif,.webp',
},
{
id: 'imageFileReference',
title: 'Image File Reference',
type: 'short-input',
canonicalParamId: 'imageFile',
placeholder: 'Reference an image from previous blocks',
mode: 'advanced',
required: true,
},
{
id: 'model',
title: 'Vision Model',
type: 'dropdown',
options: VISION_MODEL_OPTIONS,
value: () => 'gpt-5.2',
},
{
id: 'prompt',
title: 'Prompt',
type: 'long-input',
placeholder: 'Enter prompt for image analysis',
required: true,
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
},
],
inputs: {
apiKey: { type: 'string', description: 'Provider API key' },
imageFile: { type: 'json', description: 'Image file (UserFile)' },
imageFileReference: { type: 'json', description: 'Image file reference' },
model: { type: 'string', description: 'Vision model' },
prompt: { type: 'string', description: 'Analysis prompt' },
},
}

View File

@@ -28,7 +28,7 @@ import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch'
import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs'
import { EvaluatorBlock } from '@/blocks/blocks/evaluator'
import { ExaBlock } from '@/blocks/blocks/exa'
import { FileBlock, FileV2Block } from '@/blocks/blocks/file'
import { FileBlock, FileV2Block, FileV3Block } from '@/blocks/blocks/file'
import { FirecrawlBlock } from '@/blocks/blocks/firecrawl'
import { FirefliesBlock } from '@/blocks/blocks/fireflies'
import { FunctionBlock } from '@/blocks/blocks/function'
@@ -139,7 +139,7 @@ import { TwilioVoiceBlock } from '@/blocks/blocks/twilio_voice'
import { TypeformBlock } from '@/blocks/blocks/typeform'
import { VariablesBlock } from '@/blocks/blocks/variables'
import { VideoGeneratorBlock, VideoGeneratorV2Block } from '@/blocks/blocks/video_generator'
import { VisionBlock } from '@/blocks/blocks/vision'
import { VisionBlock, VisionV2Block } from '@/blocks/blocks/vision'
import { WaitBlock } from '@/blocks/blocks/wait'
import { WealthboxBlock } from '@/blocks/blocks/wealthbox'
import { WebflowBlock } from '@/blocks/blocks/webflow'
@@ -192,6 +192,7 @@ export const registry: Record<string, BlockConfig> = {
exa: ExaBlock,
file: FileBlock,
file_v2: FileV2Block,
file_v3: FileV3Block,
firecrawl: FirecrawlBlock,
fireflies: FirefliesBlock,
function: FunctionBlock,
@@ -314,6 +315,7 @@ export const registry: Record<string, BlockConfig> = {
video_generator: VideoGeneratorBlock,
video_generator_v2: VideoGeneratorV2Block,
vision: VisionBlock,
vision_v2: VisionV2Block,
wait: WaitBlock,
wealthbox: WealthboxBlock,
webflow: WebflowBlock,

View File

@@ -9,7 +9,8 @@ export type PrimitiveValueType =
| 'boolean'
| 'json'
| 'array'
| 'files'
| 'file'
| 'file[]'
| 'any'
export type BlockCategory = 'blocks' | 'tools' | 'triggers'

View File

@@ -137,7 +137,7 @@ export class BlockExecutor {
normalizedOutput = this.normalizeOutput(output)
}
if (ctx.includeFileBase64 && containsUserFileWithMetadata(normalizedOutput)) {
if (containsUserFileWithMetadata(normalizedOutput)) {
normalizedOutput = (await hydrateUserFilesWithBase64(normalizedOutput, {
requestId: ctx.metadata.requestId,
executionId: ctx.executionId,

View File

@@ -133,7 +133,7 @@ describe('resolveBlockReference', () => {
'block-1': {
input: { type: 'string' },
conversationId: { type: 'string' },
files: { type: 'files' },
files: { type: 'file[]' },
},
},
})
@@ -206,7 +206,7 @@ describe('resolveBlockReference', () => {
},
},
blockOutputSchemas: {
'block-1': { files: { type: 'files' } },
'block-1': { files: { type: 'file[]' } },
},
})
@@ -218,7 +218,7 @@ describe('resolveBlockReference', () => {
const ctx = createContext({
blockData: { 'block-1': { files: [] } },
blockOutputSchemas: {
'block-1': { files: { type: 'files' } },
'block-1': { files: { type: 'file[]' } },
},
})

View File

@@ -32,7 +32,7 @@ export class InvalidFieldError extends Error {
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'
return typed.type === 'file' || typed.type === 'file[]'
}
function isArrayType(value: unknown): value is { type: 'array'; items?: unknown } {

View File

@@ -163,7 +163,7 @@ export async function processInputFileFields(
}
const inputFormat = extractInputFormatFromBlock(startBlock)
const fileFields = inputFormat.filter((field) => field.type === 'files')
const fileFields = inputFormat.filter((field) => field.type === 'file[]')
if (fileFields.length === 0) {
return input

View File

@@ -153,7 +153,7 @@ export function generateToolInputSchema(inputFormat: InputFormatField[]): McpToo
// Handle array types
if (fieldType === 'array') {
if (field.type === 'files') {
if (field.type === 'file[]') {
property.items = {
type: 'object',
properties: {

View File

@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { isUserFileWithMetadata } from '@/lib/core/utils/user-file'
import { StorageService } from '@/lib/uploads'
import type { ExecutionContext } from '@/lib/uploads/contexts/execution/utils'
import { generateExecutionFileKey, generateFileId } from '@/lib/uploads/contexts/execution/utils'
import type { UserFile } from '@/executor/types'
@@ -89,10 +90,7 @@ export async function uploadExecutionFile(
}
try {
const { uploadFile, generatePresignedDownloadUrl } = await import(
'@/lib/uploads/core/storage-service'
)
const fileInfo = await uploadFile({
const fileInfo = await StorageService.uploadFile({
file: fileBuffer,
fileName: storageKey,
contentType,
@@ -102,21 +100,24 @@ export async function uploadExecutionFile(
metadata, // Pass metadata for cloud storage and database tracking
})
// Generate presigned URL for file access (10 minutes expiration)
const fullUrl = await generatePresignedDownloadUrl(fileInfo.key, 'execution', 600)
const presignedUrl = await StorageService.generatePresignedDownloadUrl(
fileInfo.key,
'execution',
5 * 60
)
const userFile: UserFile = {
id: fileId,
name: fileName,
size: fileBuffer.length,
type: contentType,
url: fullUrl, // Presigned URL for external access and downstream workflow usage
url: presignedUrl,
key: fileInfo.key,
context: 'execution', // Preserve context in file object
context: 'execution',
base64: fileBuffer.toString('base64'),
}
logger.info(`Successfully uploaded execution file: ${fileName} (${fileBuffer.length} bytes)`, {
url: fullUrl,
key: fileInfo.key,
})
return userFile
@@ -135,8 +136,7 @@ export async function downloadExecutionFile(userFile: UserFile): Promise<Buffer>
logger.info(`Downloading execution file: ${userFile.name}`)
try {
const { downloadFile } = await import('@/lib/uploads/core/storage-service')
const fileBuffer = await downloadFile({
const fileBuffer = await StorageService.downloadFile({
key: userFile.key,
context: 'execution',
})

View File

@@ -0,0 +1,24 @@
import { z } from 'zod'
export const RawFileInputSchema = z
.object({
id: z.string().optional(),
key: z.string().optional(),
path: z.string().optional(),
url: z.string().optional(),
name: z.string().min(1),
size: z.number().nonnegative(),
type: z.string().optional(),
uploadedAt: z.union([z.string(), z.date()]).optional(),
expiresAt: z.union([z.string(), z.date()]).optional(),
context: z.string().optional(),
base64: z.string().optional(),
})
.passthrough()
.refine((data) => Boolean(data.key || data.path || data.url), {
message: 'File must include key, path, or url',
})
export const RawFileInputArraySchema = z.array(RawFileInputSchema)
export const FileInputSchema = z.union([RawFileInputSchema, z.string()])

View File

@@ -458,17 +458,23 @@ function isCompleteUserFile(file: RawFileInput): file is UserFile {
/**
* Converts a single raw file object to UserFile format
* @param file - Raw file object
* @param file - Raw file object (must be a single file, not an array)
* @param requestId - Request ID for logging
* @param logger - Logger instance
* @returns UserFile object
* @throws Error if file has no storage key
* @throws Error if file is an array or has no storage key
*/
export function processSingleFileToUserFile(
file: RawFileInput,
requestId: string,
logger: Logger
): UserFile {
if (Array.isArray(file)) {
const errorMsg = `Expected a single file but received an array with ${file.length} file(s). Use a file input that accepts multiple files, or select a specific file from the array (e.g., {{block.files[0]}}).`
logger.error(`[${requestId}] ${errorMsg}`)
throw new Error(errorMsg)
}
if (isCompleteUserFile(file)) {
return file
}
@@ -495,21 +501,51 @@ export function processSingleFileToUserFile(
/**
* Converts raw file objects (from file-upload or variable references) to UserFile format
* @param files - Array of raw file objects
* Accepts either a single file or an array of files and normalizes to array output
* @param files - Single file or array of raw file objects
* @param requestId - Request ID for logging
* @param logger - Logger instance
* @returns Array of UserFile objects
*/
export function processFilesToUserFiles(
files: RawFileInput[],
files: RawFileInput | RawFileInput[],
requestId: string,
logger: Logger
): UserFile[] {
const filesArray = Array.isArray(files) ? files : [files]
const userFiles: UserFile[] = []
for (const file of files) {
for (const file of filesArray) {
try {
const userFile = processSingleFileToUserFile(file, requestId, logger)
if (Array.isArray(file)) {
logger.warn(`[${requestId}] Skipping nested array in file input`)
continue
}
if (isCompleteUserFile(file)) {
userFiles.push(file)
continue
}
const storageKey = file.key || (file.path ? extractStorageKey(file.path) : null)
if (!storageKey) {
logger.warn(`[${requestId}] Skipping file without storage key: ${file.name || 'unknown'}`)
continue
}
const userFile: UserFile = {
id: file.id || `file-${Date.now()}`,
name: file.name,
url: file.url || file.path || '',
size: file.size,
type: file.type || 'application/octet-stream',
key: storageKey,
}
logger.info(
`[${requestId}] Converted file to UserFile: ${userFile.name} (key: ${userFile.key})`
)
userFiles.push(userFile)
} catch (error) {
logger.warn(

View File

@@ -77,7 +77,7 @@ export class WebhookAttachmentProcessor {
userId?: string
}
): Promise<UserFile> {
return uploadFileFromRawData(
const userFile = await uploadFileFromRawData(
{
name: attachment.name,
data: attachment.data,
@@ -86,5 +86,14 @@ export class WebhookAttachmentProcessor {
executionContext,
executionContext.userId
)
if (userFile.base64) {
return userFile
}
return {
...userFile,
base64: attachment.data.toString('base64'),
}
}
}

View File

@@ -123,13 +123,13 @@ function filterOutputsByCondition(
const CHAT_OUTPUTS: OutputDefinition = {
input: { type: 'string', description: 'User message' },
conversationId: { type: 'string', description: 'Conversation ID' },
files: { type: 'files', description: 'Uploaded files' },
files: { type: 'file[]', description: 'Uploaded files' },
}
const UNIFIED_START_OUTPUTS: OutputDefinition = {
input: { type: 'string', description: 'Primary user input or message' },
conversationId: { type: 'string', description: 'Conversation thread identifier' },
files: { type: 'files', description: 'User uploaded files' },
files: { type: 'file[]', description: 'User uploaded files' },
}
function applyInputFormatFields(
@@ -341,6 +341,17 @@ function expandFileTypeProperties(path: string): string[] {
return USER_FILE_ACCESSIBLE_PROPERTIES.map((prop) => `${path}.${prop}`)
}
type FileOutputType = 'file' | 'file[]'
function isFileOutputDefinition(value: unknown): value is { type: FileOutputType } {
if (!value || typeof value !== 'object' || !('type' in value)) {
return false
}
const { type } = value as { type?: unknown }
return type === 'file' || type === 'file[]'
}
export function getBlockOutputPaths(
blockType: string,
subBlocks?: Record<string, SubBlockWithValue>,
@@ -373,13 +384,7 @@ function getFilePropertyType(outputs: OutputDefinition, pathParts: string[]): st
current = (current as Record<string, unknown>)[part]
}
if (
current &&
typeof current === 'object' &&
'type' in current &&
((current as { type: unknown }).type === 'files' ||
(current as { type: unknown }).type === 'file[]')
) {
if (isFileOutputDefinition(current)) {
return USER_FILE_PROPERTY_TYPES[lastPart as keyof typeof USER_FILE_PROPERTY_TYPES]
}
@@ -485,7 +490,7 @@ function generateOutputPaths(outputs: Record<string, any>, prefix = ''): string[
paths.push(currentPath)
} else if (typeof value === 'object' && value !== null) {
if ('type' in value && typeof value.type === 'string') {
if (value.type === 'files' || value.type === 'file[]') {
if (isFileOutputDefinition(value)) {
paths.push(...expandFileTypeProperties(currentPath))
continue
}
@@ -546,7 +551,7 @@ function generateOutputPathsWithTypes(
paths.push({ path: currentPath, type: value })
} else if (typeof value === 'object' && value !== null) {
if ('type' in value && typeof value.type === 'string') {
if (value.type === 'files' || value.type === 'file[]') {
if (isFileOutputDefinition(value)) {
paths.push({ path: currentPath, type: value.type })
for (const prop of USER_FILE_ACCESSIBLE_PROPERTIES) {
paths.push({

View File

@@ -98,7 +98,7 @@ export function getInputFormatExample(
case 'array':
exampleData[field.name] = [1, 2, 3]
break
case 'files':
case 'file[]':
exampleData[field.name] = [
{
data: 'data:application/pdf;base64,...',

View File

@@ -1,6 +1,6 @@
export interface InputFormatField {
name?: string
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' | string
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' | string
description?: string
value?: unknown
}

View File

@@ -1,3 +1,5 @@
import type { UserFile } from '@/executor/types'
export interface DiscordMessage {
id: string
content: string
@@ -58,7 +60,7 @@ export interface DiscordSendMessageParams extends DiscordAuthParams {
description?: string
color?: string | number
}
files?: any[]
files?: UserFile[]
}
export interface DiscordGetMessagesParams extends DiscordAuthParams {

View File

@@ -4,7 +4,7 @@ import type { ToolConfig } from '@/tools/types'
export const dropboxDownloadTool: ToolConfig<DropboxDownloadParams, DropboxDownloadResponse> = {
id: 'dropbox_download',
name: 'Dropbox Download File',
description: 'Download a file from Dropbox and get a temporary link',
description: 'Download a file from Dropbox with metadata and content',
version: '1.0.0',
oauth: {
@@ -22,7 +22,7 @@ export const dropboxDownloadTool: ToolConfig<DropboxDownloadParams, DropboxDownl
},
request: {
url: 'https://api.dropboxapi.com/2/files/get_temporary_link',
url: 'https://content.dropboxapi.com/2/files/download',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
@@ -30,45 +30,73 @@ export const dropboxDownloadTool: ToolConfig<DropboxDownloadParams, DropboxDownl
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'Dropbox-API-Arg': JSON.stringify({ path: params.path }),
}
},
body: (params) => ({
path: params.path,
}),
},
transformResponse: async (response) => {
const data = await response.json()
transformResponse: async (response, params) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
error: data.error_summary || data.error?.message || 'Failed to download file',
error: errorText || 'Failed to download file',
output: {},
}
}
const apiResultHeader =
response.headers.get('dropbox-api-result') || response.headers.get('Dropbox-API-Result')
const metadata = apiResultHeader ? JSON.parse(apiResultHeader) : undefined
const contentType = response.headers.get('content-type') || 'application/octet-stream'
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const resolvedName = metadata?.name || params?.path?.split('/').pop() || 'download'
let temporaryLink: string | undefined
if (params?.accessToken) {
try {
const linkResponse = await fetch('https://api.dropboxapi.com/2/files/get_temporary_link', {
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ path: params.path }),
})
if (linkResponse.ok) {
const linkData = await linkResponse.json()
temporaryLink = linkData.link
}
} catch {
temporaryLink = undefined
}
}
return {
success: true,
output: {
file: data.metadata,
content: '', // Content will be available via the temporary link
temporaryLink: data.link,
file: {
name: resolvedName,
mimeType: contentType,
data: buffer.toString('base64'),
size: buffer.length,
},
content: buffer.toString('base64'),
metadata,
temporaryLink,
},
}
},
outputs: {
file: {
type: 'object',
type: 'file',
description: 'Downloaded file stored in execution files',
},
metadata: {
type: 'json',
description: 'The file metadata',
properties: {
id: { type: 'string', description: 'Unique identifier for the file' },
name: { type: 'string', description: 'Name of the file' },
path_display: { type: 'string', description: 'Display path of the file' },
size: { type: 'number', description: 'Size of the file in bytes' },
},
},
temporaryLink: {
type: 'string',

View File

@@ -1,4 +1,4 @@
import type { ToolResponse } from '@/tools/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
// ===== Core Types =====
@@ -91,8 +91,9 @@ export interface DropboxDownloadParams extends DropboxBaseParams {
export interface DropboxDownloadResponse extends ToolResponse {
output: {
file?: DropboxFileMetadata
file?: ToolFileData
content?: string // Base64 encoded file content
metadata?: DropboxFileMetadata
temporaryLink?: string
}
}

View File

@@ -1,4 +1,5 @@
import { fileParserTool, fileParserV2Tool } from '@/tools/file/parser'
import { fileParserTool, fileParserV2Tool, fileParserV3Tool } from '@/tools/file/parser'
export const fileParseTool = fileParserTool
export { fileParserV2Tool }
export { fileParserV3Tool }

View File

@@ -7,6 +7,8 @@ import type {
FileParserInput,
FileParserOutput,
FileParserOutputData,
FileParserV3Output,
FileParserV3OutputData,
} from '@/tools/file/types'
import type { ToolConfig } from '@/tools/types'
@@ -29,6 +31,66 @@ interface ToolBodyParams extends Partial<FileParserInput> {
}
}
const parseFileParserResponse = async (response: Response): Promise<FileParserOutput> => {
logger.info('Received response status:', response.status)
const result = (await response.json()) as FileParseApiResponse | FileParseApiMultiResponse
logger.info('Response parsed successfully')
// Handle multiple files response
if ('results' in result) {
logger.info('Processing multiple files response')
// Extract individual file results
const fileResults: FileParseResult[] = result.results.map((fileResult) => {
return fileResult.output || (fileResult as unknown as FileParseResult)
})
// Collect UserFile objects from results
const processedFiles: UserFile[] = fileResults
.filter((file): file is FileParseResult & { file: UserFile } => Boolean(file.file))
.map((file) => file.file)
// Combine all file contents with clear dividers
const combinedContent = fileResults
.map((file, index) => {
const divider = `\n${'='.repeat(80)}\n`
return file.content + (index < fileResults.length - 1 ? divider : '')
})
.join('\n')
// Create the base output
const output: FileParserOutputData = {
files: fileResults,
combinedContent,
...(processedFiles.length > 0 && { processedFiles }),
}
return {
success: true,
output,
}
}
// Handle single file response
logger.info('Successfully parsed file:', result.output?.name || 'unknown')
const fileOutput: FileParseResult = result.output || (result as unknown as FileParseResult)
// For a single file, create the output with just array format
const output: FileParserOutputData = {
files: [fileOutput],
combinedContent: fileOutput?.content || result.content || '',
...(fileOutput?.file && { processedFiles: [fileOutput.file] }),
}
return {
success: true,
output,
}
}
export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
id: 'file_parser',
name: 'File Parser',
@@ -38,7 +100,7 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
params: {
filePath: {
type: 'string',
required: true,
required: false,
visibility: 'user-only',
description: 'Path to the file(s). Can be a single path, URL, or an array of paths.',
},
@@ -111,65 +173,7 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
},
},
transformResponse: async (response: Response): Promise<FileParserOutput> => {
logger.info('Received response status:', response.status)
const result = (await response.json()) as FileParseApiResponse | FileParseApiMultiResponse
logger.info('Response parsed successfully')
// Handle multiple files response
if ('results' in result) {
logger.info('Processing multiple files response')
// Extract individual file results
const fileResults: FileParseResult[] = result.results.map((fileResult) => {
return fileResult.output || (fileResult as unknown as FileParseResult)
})
// Collect UserFile objects from results
const processedFiles: UserFile[] = fileResults
.filter((file): file is FileParseResult & { file: UserFile } => Boolean(file.file))
.map((file) => file.file)
// Combine all file contents with clear dividers
const combinedContent = fileResults
.map((file, index) => {
const divider = `\n${'='.repeat(80)}\n`
return file.content + (index < fileResults.length - 1 ? divider : '')
})
.join('\n')
// Create the base output
const output: FileParserOutputData = {
files: fileResults,
combinedContent,
...(processedFiles.length > 0 && { processedFiles }),
}
return {
success: true,
output,
}
}
// Handle single file response
logger.info('Successfully parsed file:', result.output?.name || 'unknown')
const fileOutput: FileParseResult = result.output || (result as unknown as FileParseResult)
// For a single file, create the output with just array format
const output: FileParserOutputData = {
files: [fileOutput],
combinedContent: fileOutput?.content || result.content || '',
...(fileOutput?.file && { processedFiles: [fileOutput.file] }),
}
return {
success: true,
output,
}
},
transformResponse: parseFileParserResponse,
outputs: {
files: { type: 'array', description: 'Array of parsed files with content and metadata' },
@@ -186,7 +190,7 @@ export const fileParserV2Tool: ToolConfig<FileParserInput, FileParserOutput> = {
params: fileParserTool.params,
request: fileParserTool.request,
transformResponse: fileParserTool.transformResponse,
transformResponse: parseFileParserResponse,
outputs: {
files: {
@@ -199,3 +203,34 @@ export const fileParserV2Tool: ToolConfig<FileParserInput, FileParserOutput> = {
},
},
}
export const fileParserV3Tool: ToolConfig<FileParserInput, FileParserV3Output> = {
id: 'file_parser_v3',
name: 'File Parser',
description: 'Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)',
version: '3.0.0',
params: fileParserTool.params,
request: fileParserTool.request,
transformResponse: async (response: Response): Promise<FileParserV3Output> => {
const parsed = await parseFileParserResponse(response)
const output = parsed.output as FileParserOutputData
const files =
Array.isArray(output.processedFiles) && output.processedFiles.length > 0
? output.processedFiles
: []
const cleanedOutput: FileParserV3OutputData = {
files,
combinedContent: output.combinedContent,
}
return {
success: true,
output: cleanedOutput,
}
},
outputs: {
files: { type: 'file[]', description: 'Parsed files as UserFile objects' },
combinedContent: { type: 'string', description: 'Combined content of all parsed files' },
},
}

View File

@@ -34,6 +34,17 @@ export interface FileParserOutput extends ToolResponse {
output: FileParserOutputData
}
export interface FileParserV3OutputData {
/** Array of parsed files as UserFile objects */
files: UserFile[]
/** Combined text content from all files */
combinedContent: string
}
export interface FileParserV3Output extends ToolResponse {
output: FileParserV3OutputData
}
/** API response structure for single file parse */
export interface FileParseApiResponse {
success: boolean

View File

@@ -224,14 +224,8 @@ export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownload
outputs: {
file: {
type: 'object',
description: 'Downloaded file data',
properties: {
name: { type: 'string', description: 'File name' },
mimeType: { type: 'string', description: 'MIME type of the file' },
data: { type: 'string', description: 'File content as base64-encoded string' },
size: { type: 'number', description: 'File size in bytes' },
},
type: 'file',
description: 'Downloaded file stored in execution files',
},
metadata: {
type: 'object',

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
// User information returned in various file metadata fields
@@ -322,7 +323,7 @@ export interface GoogleDriveToolParams {
folderSelector?: string
fileId?: string
fileName?: string
file?: any // UserFile object
file?: UserFile
content?: string
mimeType?: string
query?: string

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface MicrosoftTeamsAttachment {
@@ -71,7 +72,7 @@ export interface MicrosoftTeamsToolParams {
teamId?: string
content?: string
includeAttachments?: boolean
files?: any[] // UserFile array for attachments
files?: UserFile[]
reactionType?: string // For reaction operations
}

View File

@@ -3,6 +3,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
import type {
MistralParserInput,
MistralParserOutput,
MistralParserV2Input,
MistralParserV2Output,
} from '@/tools/mistral/types'
import type { ToolConfig } from '@/tools/types'
@@ -420,14 +421,84 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
},
}
export const mistralParserV2Tool: ToolConfig<MistralParserInput, MistralParserV2Output> = {
const mistralParserV2Params = {
fileData: {
type: 'object',
required: false,
visibility: 'hidden',
description: 'File data from a previous block',
},
filePath: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'File path or URL (legacy)',
},
resultType: mistralParserTool.params.resultType,
includeImageBase64: mistralParserTool.params.includeImageBase64,
pages: mistralParserTool.params.pages,
imageLimit: mistralParserTool.params.imageLimit,
imageMinSize: mistralParserTool.params.imageMinSize,
apiKey: mistralParserTool.params.apiKey,
} satisfies ToolConfig['params']
export const mistralParserV2Tool: ToolConfig<MistralParserV2Input, MistralParserV2Output> = {
id: 'mistral_parser_v2',
name: 'Mistral PDF Parser',
description: 'Parse PDF documents using Mistral OCR API',
version: '2.0.0',
params: mistralParserTool.params,
request: mistralParserTool.request,
params: mistralParserV2Params,
request: {
url: '/api/tools/mistral/parse',
method: 'POST',
headers: (params) => {
return {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
},
body: (params) => {
if (!params || typeof params !== 'object') {
throw new Error('Invalid parameters: Parameters must be provided as an object')
}
if (!params.apiKey || typeof params.apiKey !== 'string' || params.apiKey.trim() === '') {
throw new Error('Missing or invalid API key: A valid Mistral API key is required')
}
const fileData = params.fileData ?? params.filePath
if (!fileData) {
throw new Error('File input is required')
}
const requestBody: Record<string, unknown> = {
apiKey: params.apiKey,
resultType: params.resultType || 'markdown',
}
if (typeof fileData === 'string') {
requestBody.filePath = fileData.trim()
} else {
requestBody.fileData = fileData
}
if (params.pages) {
requestBody.pages = params.pages
}
if (params.includeImageBase64 !== undefined) {
requestBody.includeImageBase64 = params.includeImageBase64
}
if (params.imageLimit !== undefined) {
requestBody.imageLimit = params.imageLimit
}
if (params.imageMinSize !== undefined) {
requestBody.imageMinSize = params.imageMinSize
}
return requestBody
},
},
transformResponse: async (response: Response) => {
let ocrResult

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -137,7 +138,7 @@ export const MISTRAL_PARSER_METADATA_OUTPUT: OutputProperty = {
export interface MistralParserInput {
filePath: string
fileUpload?: any
fileUpload?: UserFile
_internalFilePath?: string
apiKey: string
resultType?: 'markdown' | 'text' | 'json'
@@ -147,6 +148,17 @@ export interface MistralParserInput {
imageMinSize?: number
}
export interface MistralParserV2Input {
fileData?: UserFile | string
filePath?: string
apiKey: string
resultType?: 'markdown' | 'text' | 'json'
includeImageBase64?: boolean
pages?: number[]
imageLimit?: number
imageMinSize?: number
}
export interface MistralOcrUsageInfo {
pagesProcessed: number
docSizeBytes: number | null

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface MicrosoftGraphDriveItem {
@@ -91,7 +92,7 @@ export interface OneDriveToolParams {
folderName?: string
fileId?: string
fileName?: string
file?: unknown // UserFile or UserFile array
file?: UserFile
content?: string
mimeType?: string
query?: string

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -117,7 +118,7 @@ export interface OutlookSendParams {
conversationId?: string
cc?: string
bcc?: string
attachments?: any[]
attachments?: UserFile[]
}
export interface OutlookSendResponse extends ToolResponse {
@@ -150,7 +151,7 @@ export interface OutlookDraftParams {
subject: string
body: string
contentType?: 'text' | 'html'
attachments?: any[]
attachments?: UserFile[]
}
export interface OutlookDraftResponse extends ToolResponse {

View File

@@ -239,7 +239,7 @@ import {
exaResearchTool,
exaSearchTool,
} from '@/tools/exa'
import { fileParserV2Tool, fileParseTool } from '@/tools/file'
import { fileParserV2Tool, fileParserV3Tool, fileParseTool } from '@/tools/file'
import {
firecrawlAgentTool,
firecrawlCrawlTool,
@@ -1778,6 +1778,7 @@ export const tools: Record<string, ToolConfig> = {
vision_tool: visionTool,
file_parser: fileParseTool,
file_parser_v2: fileParserV2Tool,
file_parser_v3: fileParserV3Tool,
firecrawl_scrape: firecrawlScrapeTool,
firecrawl_search: firecrawlSearchTool,
firecrawl_crawl: firecrawlCrawlTool,

View File

@@ -50,7 +50,7 @@ export const s3GetObjectTool: ToolConfig = {
)
}
},
method: 'HEAD',
method: 'GET',
headers: (params) => {
try {
// Parse S3 URI if not already parsed
@@ -66,7 +66,7 @@ export const s3GetObjectTool: ToolConfig = {
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
const dateStamp = amzDate.slice(0, 8)
const method = 'HEAD'
const method = 'GET'
const encodedPath = encodeS3PathComponent(params.objectKey)
const canonicalUri = `/${encodedPath}`
const canonicalQueryString = ''
@@ -108,11 +108,18 @@ export const s3GetObjectTool: ToolConfig = {
params.objectKey = objectKey
}
// Get file metadata
if (!response.ok) {
const errorText = await response.text()
throw new Error(
`Failed to download S3 object: ${response.status} ${response.statusText} ${errorText}`
)
}
const contentType = response.headers.get('content-type') || 'application/octet-stream'
const contentLength = Number.parseInt(response.headers.get('content-length') || '0', 10)
const lastModified = response.headers.get('last-modified') || new Date().toISOString()
const fileName = params.objectKey.split('/').pop() || params.objectKey
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Generate pre-signed URL for download
const url = generatePresignedUrl(params, 3600)
@@ -121,9 +128,15 @@ export const s3GetObjectTool: ToolConfig = {
success: true,
output: {
url,
file: {
name: fileName,
mimeType: contentType,
data: buffer.toString('base64'),
size: buffer.length,
},
metadata: {
fileType: contentType,
size: contentLength,
size: buffer.length,
name: fileName,
lastModified: lastModified,
},
@@ -136,6 +149,10 @@ export const s3GetObjectTool: ToolConfig = {
type: 'string',
description: 'Pre-signed URL for downloading the S3 object',
},
file: {
type: 'file',
description: 'Downloaded file stored in execution files',
},
metadata: {
type: 'object',
description: 'File metadata including type, size, name, and last modified date',

View File

@@ -1,8 +1,10 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface S3Response extends ToolResponse {
output: {
url?: string
file?: UserFile
objects?: Array<{
key: string
size: number

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
// Shared type definitions
@@ -48,6 +49,14 @@ export interface SendGridPersonalization {
dynamic_template_data?: Record<string, unknown>
}
export interface SendGridAttachment {
content: string
filename: string
type?: string
disposition?: string
content_id?: string
}
export interface SendGridMailBody {
personalizations: SendGridPersonalization[]
from: { email: string; name?: string }
@@ -55,7 +64,7 @@ export interface SendGridMailBody {
template_id?: string
content?: Array<{ type: 'text/plain' | 'text/html'; value?: string }>
reply_to?: { email: string; name?: string }
attachments?: any[]
attachments?: SendGridAttachment[] | UserFile[]
}
export interface SendGridContactObject {
@@ -95,7 +104,7 @@ export interface SendMailParams extends SendGridBaseParams {
bcc?: string
replyTo?: string
replyToName?: string
attachments?: string
attachments?: UserFile[] | SendGridAttachment[] | string
templateId?: string
dynamicTemplateData?: string
}

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface SftpConnectionConfig {
@@ -12,7 +13,7 @@ export interface SftpConnectionConfig {
// Upload file params
export interface SftpUploadParams extends SftpConnectionConfig {
remotePath: string
files?: any[] // UserFile array from file-upload component
files?: UserFile[]
fileContent?: string // Direct content for text files
fileName?: string // File name when using direct content
overwrite?: boolean

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface SharepointSite {
@@ -180,7 +181,7 @@ export interface SharepointToolParams {
driveId?: string
folderPath?: string
fileName?: string
files?: any[]
files?: UserFile[]
}
export interface GraphApiResponse {

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -516,7 +517,7 @@ export interface SlackMessageParams extends SlackBaseParams {
userId?: string
text: string
thread_ts?: string
files?: any[]
files?: UserFile[]
}
export interface SlackCanvasParams extends SlackBaseParams {

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface SmtpConnectionConfig {
@@ -21,7 +22,7 @@ export interface SmtpSendMailParams extends SmtpConnectionConfig {
cc?: string
bcc?: string
replyTo?: string
attachments?: any[]
attachments?: UserFile[]
}
export interface SmtpSendMailResult extends ToolResponse {

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface TelegramMessage {
@@ -116,7 +117,7 @@ export interface TelegramSendAnimationParams extends TelegramAuthParams {
}
export interface TelegramSendDocumentParams extends TelegramAuthParams {
files?: any
files?: UserFile[]
caption?: string
}

View File

@@ -52,7 +52,7 @@ export const visionTool: ToolConfig<VisionParams, VisionResponse> = {
apiKey: params.apiKey,
imageUrl: params.imageUrl || null,
imageFile: params.imageFile || null,
model: params.model || 'gpt-4o',
model: params.model || 'gpt-5.2',
prompt: params.prompt || null,
}
},

View File

@@ -1,9 +1,10 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface VisionParams {
apiKey: string
imageUrl?: string
imageFile?: any
imageFile?: UserFile
model?: string
prompt?: string
}

View File

@@ -53,7 +53,7 @@ export const genericWebhookTrigger: TriggerConfig = {
title: 'Input Format',
type: 'input-format',
description:
'Define the expected JSON input schema for this webhook (optional). Use type "files" for file uploads.',
'Define the expected JSON input schema for this webhook (optional). Use type "file[]" for file uploads.',
mode: 'trigger',
},
{

View File

@@ -98,7 +98,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
},
message: {
raw: {
attachments: { type: 'array', description: 'Array of attachments' },
attachments: { type: 'file[]', description: 'Array of attachments' },
channelData: {
team: { id: { type: 'string', description: 'Team ID' } },
tenant: { id: { type: 'string', description: 'Tenant ID' } },