Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bea0a68
improvement(collab): do not refetch active workflow id
icecrasher321 Feb 1, 2026
1da3407
progress on files
icecrasher321 Feb 1, 2026
39ca1f6
more integrations
icecrasher321 Feb 2, 2026
9ec0c8f
separate server and client logic
icecrasher321 Feb 2, 2026
f4a3c94
consolidate more code
icecrasher321 Feb 2, 2026
5a0becf
fix integrations
icecrasher321 Feb 3, 2026
42767fc
fix types
icecrasher321 Feb 3, 2026
5ecbf6c
consolidate more code
icecrasher321 Feb 3, 2026
a65f3b8
fix tests
icecrasher321 Feb 3, 2026
3ceabbb
fix more bugbot comments
icecrasher321 Feb 3, 2026
1ff3540
fix type check
icecrasher321 Feb 3, 2026
7570e50
Merge branch 'staging' into improvement/double-fetch
icecrasher321 Feb 3, 2026
1c857cd
fix circular impport
icecrasher321 Feb 3, 2026
6e642fc
address more bugbot comments
icecrasher321 Feb 3, 2026
cbe0f8a
fix ocr integrations
icecrasher321 Feb 3, 2026
a6ec6a0
fix typing
icecrasher321 Feb 3, 2026
0aeaf6f
remove leftover type
icecrasher321 Feb 3, 2026
4169a25
address bugbot comment
icecrasher321 Feb 3, 2026
66b954d
fix file block adv mode
icecrasher321 Feb 3, 2026
6e5e8de
fix
icecrasher321 Feb 3, 2026
c230e1a
normalize file input
icecrasher321 Feb 3, 2026
2854906
fix v2 blocmks for ocr
icecrasher321 Feb 3, 2026
3b74708
fix for v2 versions
icecrasher321 Feb 3, 2026
ff0753a
fix more v2 blocks
icecrasher321 Feb 3, 2026
47c9604
update single file blocks
icecrasher321 Feb 3, 2026
f256a9f
make interface simpler
icecrasher321 Feb 3, 2026
fa81609
cleanup fireflies
icecrasher321 Feb 3, 2026
dc3d449
remove file only annotation
icecrasher321 Feb 3, 2026
4f2b5a5
accept all types
icecrasher321 Feb 3, 2026
a529f06
added wand to ssh block
waleedlatif1 Feb 3, 2026
ed1ca6e
user files should be passed through
icecrasher321 Feb 3, 2026
4669ec9
Merge branch 'improvement/double-fetch' of github.com:simstudioai/sim…
icecrasher321 Feb 3, 2026
b0457bc
improve docs
icecrasher321 Feb 3, 2026
cfc3604
fix slack to include successful execs
icecrasher321 Feb 3, 2026
bd5866e
fix dropbox upload file
icecrasher321 Feb 3, 2026
2d96ac5
fix sendgrid
icecrasher321 Feb 3, 2026
aa1b158
fix dropbox
icecrasher321 Feb 3, 2026
5a78b55
fix
icecrasher321 Feb 3, 2026
b7ccbc8
fix
icecrasher321 Feb 3, 2026
1282bef
update skills
icecrasher321 Feb 4, 2026
6b95b62
fix uploaded file
icecrasher321 Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .claude/commands/add-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,109 @@ export const {ServiceName}Block: BlockConfig = {
}
```

## File Input Handling

When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.

### Basic/Advanced File Pattern

```typescript
// Basic mode: Visual file upload
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from other blocks
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},
```

**Critical constraints:**
- `canonicalParamId` must NOT match any subblock's `id` in the same block
- Values are stored under subblock `id`, not `canonicalParamId`

### Normalizing File Input in tools.config

Use `normalizeFileInput` to handle all input variants:

```typescript
import { normalizeFileInput } from '@/blocks/utils'

tools: {
access: ['service_upload'],
config: {
tool: (params) => {
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `service_${params.operation}`
},
},
}
```

**Why this pattern?**
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
- `canonicalParamId` only controls UI/schema mapping, not runtime values
- `normalizeFileInput` handles JSON strings from advanced mode template resolution

### File Input Types in `inputs`

Use `type: 'json'` for file inputs:

```typescript
inputs: {
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
fileRef: { type: 'json', description: 'File reference from previous block' },
// Legacy field for backwards compatibility
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
}
```

### Multiple Files

For multiple file uploads:

```typescript
{
id: 'attachments',
title: 'Attachments',
type: 'file-upload',
multiple: true, // Allow multiple files
maxSize: 25, // Max size in MB per file
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
}

// In tools.config:
const normalizedFiles = normalizeFileInput(
params.attachments || params.attachmentRefs,
// No { single: true } - returns array
)
if (normalizedFiles) {
params.files = normalizedFiles
}
```

## Condition Syntax

Controls when a field is shown based on other field values.
Expand Down
227 changes: 226 additions & 1 deletion .claude/commands/add-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,11 +457,236 @@ You can usually find this in the service's brand/press kit page, or copy it from
Paste the SVG code here and I'll convert it to a React component.
```

## Common Gotchas
## File Handling

When your integration handles file uploads or downloads, follow these patterns to work with `UserFile` objects consistently.

### What is a UserFile?

A `UserFile` is the standard file representation in Sim:

```typescript
interface UserFile {
id: string // Unique identifier
name: string // Original filename
url: string // Presigned URL for download
size: number // File size in bytes
type: string // MIME type (e.g., 'application/pdf')
base64?: string // Optional base64 content (if small file)
key?: string // Internal storage key
context?: object // Storage context metadata
}
```

### File Input Pattern (Uploads)

For tools that accept file uploads, **always route through an internal API endpoint** rather than calling external APIs directly. This ensures proper file content retrieval.

#### 1. Block SubBlocks for File Input

Use the basic/advanced mode pattern:

```typescript
// Basic mode: File upload UI
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Maps to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from previous block
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Same canonical param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},
```

**Critical:** `canonicalParamId` must NOT match any subblock `id`.

#### 2. Normalize File Input in Block Config

In `tools.config.tool`, use `normalizeFileInput` to handle all input variants:

```typescript
import { normalizeFileInput } from '@/blocks/utils'

tools: {
config: {
tool: (params) => {
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `{service}_${params.operation}`
},
},
}
```

#### 3. Create Internal API Route

Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:

```typescript
import { createLogger } from '@sim/logger'
import { NextResponse, type NextRequest } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'

const logger = createLogger('{Service}UploadAPI')

const RequestSchema = z.object({
accessToken: z.string(),
file: FileInputSchema.optional().nullable(),
// Legacy field for backwards compatibility
fileContent: z.string().optional().nullable(),
// ... other params
})

export async function POST(request: NextRequest) {
const requestId = generateRequestId()

const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const data = RequestSchema.parse(body)

let fileBuffer: Buffer
let fileName: string

// Prefer UserFile input, fall back to legacy base64
if (data.file) {
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
}
const userFile = userFiles[0]
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (data.fileContent) {
// Legacy: base64 string (backwards compatibility)
fileBuffer = Buffer.from(data.fileContent, 'base64')
fileName = 'file'
} else {
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
}

// Now call external API with fileBuffer
const response = await fetch('https://api.{service}.com/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${data.accessToken}` },
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
})

// ... handle response
}
```

#### 4. Update Tool to Use Internal Route

```typescript
export const {service}UploadTool: ToolConfig<Params, Response> = {
id: '{service}_upload',
// ...
params: {
file: { type: 'file', required: false, visibility: 'user-or-llm' },
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
file: params.file,
fileContent: params.fileContent,
}),
},
}
```

### File Output Pattern (Downloads)

For tools that return files, use `FileToolProcessor` to store files and return `UserFile` objects.

#### In Tool transformResponse

```typescript
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'

transformResponse: async (response, context) => {
const data = await response.json()

// Process file outputs to UserFile objects
const fileProcessor = new FileToolProcessor(context)
const file = await fileProcessor.processFileData({
data: data.content, // base64 or buffer
mimeType: data.mimeType,
filename: data.filename,
})

return {
success: true,
output: { file },
}
}
```

#### In API Route (for complex file handling)

```typescript
// Return file data that FileToolProcessor can handle
return NextResponse.json({
success: true,
output: {
file: {
data: base64Content,
mimeType: 'application/pdf',
filename: 'document.pdf',
},
},
})
```

### Key Helpers Reference

| Helper | Location | Purpose |
|--------|----------|---------|
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get file Buffer from UserFile |
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |

### Common Gotchas

1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
Loading