Skip to content

Commit 1da3407

Browse files
committed
progress on files
1 parent bea0a68 commit 1da3407

File tree

91 files changed

+1125
-340
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+1125
-340
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
title: Passing Files
3+
---
4+
5+
import { Callout } from 'fumadocs-ui/components/callout'
6+
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
7+
8+
Sim makes it easy to work with files throughout your workflows. Blocks can receive files, process them, and pass them to other blocks seamlessly.
9+
10+
## File Objects
11+
12+
When blocks output files (like Gmail attachments, generated images, or parsed documents), they return a standardized file object:
13+
14+
```json
15+
{
16+
"name": "report.pdf",
17+
"url": "https://...",
18+
"base64": "JVBERi0xLjQK...",
19+
"type": "application/pdf",
20+
"size": 245678
21+
}
22+
```
23+
24+
You can access any of these properties when referencing files from previous blocks.
25+
26+
## Passing Files Between Blocks
27+
28+
Reference files from previous blocks using the tag dropdown. Click in any file input field and type `<` to see available outputs.
29+
30+
**Common patterns:**
31+
32+
```
33+
// Single file from a block
34+
<gmail.attachments[0]>
35+
36+
// Pass the whole file object
37+
<file_parser.files[0]>
38+
39+
// Access specific properties
40+
<gmail.attachments[0].name>
41+
<gmail.attachments[0].base64>
42+
```
43+
44+
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.
45+
46+
## Triggering Workflows with Files
47+
48+
When calling a workflow via API that expects file input, include files in your request:
49+
50+
<Tabs items={['Base64', 'URL']}>
51+
<Tab value="Base64">
52+
```bash
53+
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
54+
-H "Content-Type: application/json" \
55+
-H "x-api-key: YOUR_API_KEY" \
56+
-d '{
57+
"document": {
58+
"name": "report.pdf",
59+
"base64": "JVBERi0xLjQK...",
60+
"type": "application/pdf"
61+
}
62+
}'
63+
```
64+
</Tab>
65+
<Tab value="URL">
66+
```bash
67+
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
68+
-H "Content-Type: application/json" \
69+
-H "x-api-key: YOUR_API_KEY" \
70+
-d '{
71+
"document": {
72+
"name": "report.pdf",
73+
"url": "https://example.com/report.pdf",
74+
"type": "application/pdf"
75+
}
76+
}'
77+
```
78+
</Tab>
79+
</Tabs>
80+
81+
The workflow's Start block should have an input field configured to receive the file parameter.
82+
83+
## Receiving Files in API Responses
84+
85+
When a workflow outputs files, they're included in the response:
86+
87+
```json
88+
{
89+
"success": true,
90+
"output": {
91+
"generatedFile": {
92+
"name": "output.png",
93+
"url": "https://...",
94+
"base64": "iVBORw0KGgo...",
95+
"type": "image/png",
96+
"size": 34567
97+
}
98+
}
99+
}
100+
```
101+
102+
Use `url` for direct downloads or `base64` for inline processing.
103+
104+
## Blocks That Work with Files
105+
106+
**File inputs:**
107+
- **File** - Parse documents, images, and text files
108+
- **Vision** - Analyze images with AI models
109+
- **Mistral Parser** - Extract text from PDFs
110+
111+
**File outputs:**
112+
- **Gmail** - Email attachments
113+
- **Slack** - Downloaded files
114+
- **TTS** - Generated audio files
115+
- **Video Generator** - Generated videos
116+
- **Image Generator** - Generated images
117+
118+
**File storage:**
119+
- **Supabase** - Upload/download from storage
120+
- **S3** - AWS S3 operations
121+
- **Google Drive** - Drive file operations
122+
- **Dropbox** - Dropbox file operations
123+
124+
<Callout type="info">
125+
Files are automatically available to downstream blocks. The execution engine handles all file transfer and format conversion.
126+
</Callout>
127+
128+
## Best Practices
129+
130+
1. **Use file objects directly** - Pass the full file object rather than extracting individual properties. Blocks handle the conversion automatically.
131+
132+
2. **Check file types** - Ensure the file type matches what the receiving block expects. The Vision block needs images, the File block handles documents.
133+
134+
3. **Consider file size** - Large files increase execution time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"pages": ["index", "basics", "api", "logging", "costs"]
2+
"pages": ["index", "basics", "files", "api", "logging", "costs"]
33
}

apps/sim/app/api/tools/discord/send-message/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { validateNumericId } from '@/lib/core/security/input-validation'
66
import { generateRequestId } from '@/lib/core/utils/request'
7+
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
78
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
89
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
910

@@ -15,7 +16,7 @@ const DiscordSendMessageSchema = z.object({
1516
botToken: z.string().min(1, 'Bot token is required'),
1617
channelId: z.string().min(1, 'Channel ID is required'),
1718
content: z.string().optional().nullable(),
18-
files: z.array(z.any()).optional().nullable(),
19+
files: RawFileInputArraySchema.optional().nullable(),
1920
})
2021

2122
export async function POST(request: NextRequest) {

apps/sim/app/api/tools/gmail/draft/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
67
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
78
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
89
import {
@@ -28,7 +29,7 @@ const GmailDraftSchema = z.object({
2829
replyToMessageId: z.string().optional().nullable(),
2930
cc: z.string().optional().nullable(),
3031
bcc: z.string().optional().nullable(),
31-
attachments: z.array(z.any()).optional().nullable(),
32+
attachments: RawFileInputArraySchema.optional().nullable(),
3233
})
3334

3435
export async function POST(request: NextRequest) {

apps/sim/app/api/tools/gmail/send/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
67
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
78
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
89
import {
@@ -28,7 +29,7 @@ const GmailSendSchema = z.object({
2829
replyToMessageId: z.string().optional().nullable(),
2930
cc: z.string().optional().nullable(),
3031
bcc: z.string().optional().nullable(),
31-
attachments: z.array(z.any()).optional().nullable(),
32+
attachments: RawFileInputArraySchema.optional().nullable(),
3233
})
3334

3435
export async function POST(request: NextRequest) {

apps/sim/app/api/tools/google_drive/upload/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
67
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
78
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
89
import {
@@ -20,7 +21,7 @@ const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/upload/drive/v3/files'
2021
const GoogleDriveUploadSchema = z.object({
2122
accessToken: z.string().min(1, 'Access token is required'),
2223
fileName: z.string().min(1, 'File name is required'),
23-
file: z.any().optional().nullable(),
24+
file: RawFileInputSchema.optional().nullable(),
2425
mimeType: z.string().optional().nullable(),
2526
folderId: z.string().optional().nullable(),
2627
})

apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
67
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
78
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
89
import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -16,7 +17,7 @@ const TeamsWriteChannelSchema = z.object({
1617
teamId: z.string().min(1, 'Team ID is required'),
1718
channelId: z.string().min(1, 'Channel ID is required'),
1819
content: z.string().min(1, 'Message content is required'),
19-
files: z.array(z.any()).optional().nullable(),
20+
files: RawFileInputArraySchema.optional().nullable(),
2021
})
2122

2223
export async function POST(request: NextRequest) {

apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
67
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
78
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
89
import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -15,7 +16,7 @@ const TeamsWriteChatSchema = z.object({
1516
accessToken: z.string().min(1, 'Access token is required'),
1617
chatId: z.string().min(1, 'Chat ID is required'),
1718
content: z.string().min(1, 'Message content is required'),
18-
files: z.array(z.any()).optional().nullable(),
19+
files: RawFileInputArraySchema.optional().nullable(),
1920
})
2021

2122
export async function POST(request: NextRequest) {

apps/sim/app/api/tools/mistral/parse/route.ts

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const logger = createLogger('MistralParseAPI')
1818

1919
const MistralParseSchema = z.object({
2020
apiKey: z.string().min(1, 'API key is required'),
21-
filePath: z.string().min(1, 'File path is required'),
21+
filePath: z.string().min(1, 'File path is required').optional(),
22+
fileData: z.unknown().optional(),
2223
resultType: z.string().optional(),
2324
pages: z.array(z.number()).optional(),
2425
includeImageBase64: z.boolean().optional(),
@@ -49,66 +50,96 @@ export async function POST(request: NextRequest) {
4950
const body = await request.json()
5051
const validatedData = MistralParseSchema.parse(body)
5152

53+
const fileData = validatedData.fileData
54+
const filePath = typeof fileData === 'string' ? fileData : validatedData.filePath
55+
56+
if (!fileData && (!filePath || filePath.trim() === '')) {
57+
return NextResponse.json(
58+
{
59+
success: false,
60+
error: 'File input is required',
61+
},
62+
{ status: 400 }
63+
)
64+
}
65+
5266
logger.info(`[${requestId}] Mistral parse request`, {
53-
filePath: validatedData.filePath,
54-
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
67+
hasFileData: Boolean(fileData),
68+
filePath,
69+
isWorkspaceFile: filePath ? isInternalFileUrl(filePath) : false,
5570
userId,
5671
})
5772

58-
let fileUrl = validatedData.filePath
59-
60-
if (isInternalFileUrl(validatedData.filePath)) {
61-
try {
62-
const storageKey = extractStorageKey(validatedData.filePath)
63-
64-
const context = inferContextFromKey(storageKey)
73+
const mistralBody: any = {
74+
model: 'mistral-ocr-latest',
75+
}
6576

66-
const hasAccess = await verifyFileAccess(
67-
storageKey,
68-
userId,
69-
undefined, // customConfig
70-
context, // context
71-
false // isLocal
77+
if (fileData && typeof fileData === 'object') {
78+
const base64 = (fileData as { base64?: string }).base64
79+
const mimeType = (fileData as { type?: string }).type || 'application/pdf'
80+
if (!base64) {
81+
return NextResponse.json(
82+
{
83+
success: false,
84+
error: 'File base64 content is required',
85+
},
86+
{ status: 400 }
7287
)
73-
74-
if (!hasAccess) {
75-
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
76-
userId,
77-
key: storageKey,
78-
context,
79-
})
88+
}
89+
const base64Payload = base64.startsWith('data:')
90+
? base64
91+
: `data:${mimeType};base64,${base64}`
92+
mistralBody.document = {
93+
type: 'document_base64',
94+
document_base64: base64Payload,
95+
}
96+
} else if (filePath) {
97+
let fileUrl = filePath
98+
99+
if (isInternalFileUrl(filePath)) {
100+
try {
101+
const storageKey = extractStorageKey(filePath)
102+
103+
const context = inferContextFromKey(storageKey)
104+
105+
const hasAccess = await verifyFileAccess(storageKey, userId, undefined, context, false)
106+
107+
if (!hasAccess) {
108+
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
109+
userId,
110+
key: storageKey,
111+
context,
112+
})
113+
return NextResponse.json(
114+
{
115+
success: false,
116+
error: 'File not found',
117+
},
118+
{ status: 404 }
119+
)
120+
}
121+
122+
fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60)
123+
logger.info(`[${requestId}] Generated presigned URL for ${context} file`)
124+
} catch (error) {
125+
logger.error(`[${requestId}] Failed to generate presigned URL:`, error)
80126
return NextResponse.json(
81127
{
82128
success: false,
83-
error: 'File not found',
129+
error: 'Failed to generate file access URL',
84130
},
85-
{ status: 404 }
131+
{ status: 500 }
86132
)
87133
}
88-
89-
fileUrl = await StorageService.generatePresignedDownloadUrl(storageKey, context, 5 * 60)
90-
logger.info(`[${requestId}] Generated presigned URL for ${context} file`)
91-
} catch (error) {
92-
logger.error(`[${requestId}] Failed to generate presigned URL:`, error)
93-
return NextResponse.json(
94-
{
95-
success: false,
96-
error: 'Failed to generate file access URL',
97-
},
98-
{ status: 500 }
99-
)
134+
} else if (filePath.startsWith('/')) {
135+
const baseUrl = getBaseUrl()
136+
fileUrl = `${baseUrl}${filePath}`
100137
}
101-
} else if (validatedData.filePath?.startsWith('/')) {
102-
const baseUrl = getBaseUrl()
103-
fileUrl = `${baseUrl}${validatedData.filePath}`
104-
}
105138

106-
const mistralBody: any = {
107-
model: 'mistral-ocr-latest',
108-
document: {
139+
mistralBody.document = {
109140
type: 'document_url',
110141
document_url: fileUrl,
111-
},
142+
}
112143
}
113144

114145
if (validatedData.pages) {

0 commit comments

Comments
 (0)