@@ -457,11 +457,236 @@ You can usually find this in the service's brand/press kit page, or copy it from
457457Paste the SVG code here and I'll convert it to a React component.
458458```
459459
460- ## Common Gotchas
460+ ## File Handling
461+
462+ When your integration handles file uploads or downloads, follow these patterns to work with ` UserFile ` objects consistently.
463+
464+ ### What is a UserFile?
465+
466+ A ` UserFile ` is the standard file representation in Sim:
467+
468+ ``` typescript
469+ interface UserFile {
470+ id: string // Unique identifier
471+ name: string // Original filename
472+ url: string // Presigned URL for download
473+ size: number // File size in bytes
474+ type: string // MIME type (e.g., 'application/pdf')
475+ base64? : string // Optional base64 content (if small file)
476+ key? : string // Internal storage key
477+ context? : object // Storage context metadata
478+ }
479+ ```
480+
481+ ### File Input Pattern (Uploads)
482+
483+ 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.
484+
485+ #### 1. Block SubBlocks for File Input
486+
487+ Use the basic/advanced mode pattern:
488+
489+ ``` typescript
490+ // Basic mode: File upload UI
491+ {
492+ id : ' uploadFile' ,
493+ title : ' File' ,
494+ type : ' file-upload' ,
495+ canonicalParamId : ' file' , // Maps to 'file' param
496+ placeholder : ' Upload file' ,
497+ mode : ' basic' ,
498+ multiple : false ,
499+ required : true ,
500+ condition : { field : ' operation' , value : ' upload' },
501+ },
502+ // Advanced mode: Reference from previous block
503+ {
504+ id : ' fileRef' ,
505+ title : ' File' ,
506+ type : ' short-input' ,
507+ canonicalParamId : ' file' , // Same canonical param
508+ placeholder : ' Reference file (e.g., {{file_block.output}})' ,
509+ mode : ' advanced' ,
510+ required : true ,
511+ condition : { field : ' operation' , value : ' upload' },
512+ },
513+ ```
514+
515+ ** Critical:** ` canonicalParamId ` must NOT match any subblock ` id ` .
516+
517+ #### 2. Normalize File Input in Block Config
518+
519+ In ` tools.config.tool ` , use ` normalizeFileInput ` to handle all input variants:
520+
521+ ``` typescript
522+ import { normalizeFileInput } from ' @/blocks/utils'
523+
524+ tools : {
525+ config : {
526+ tool : (params ) => {
527+ // Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
528+ const normalizedFile = normalizeFileInput (
529+ params .uploadFile || params .fileRef || params .fileContent ,
530+ { single: true }
531+ )
532+ if (normalizedFile ) {
533+ params .file = normalizedFile
534+ }
535+ return ` {service}_${params .operation } `
536+ },
537+ },
538+ }
539+ ```
540+
541+ #### 3. Create Internal API Route
542+
543+ Create ` apps/sim/app/api/tools/{service}/{action}/route.ts ` :
544+
545+ ``` typescript
546+ import { createLogger } from ' @sim/logger'
547+ import { NextResponse , type NextRequest } from ' next/server'
548+ import { z } from ' zod'
549+ import { checkInternalAuth } from ' @/lib/auth/hybrid'
550+ import { generateRequestId } from ' @/lib/core/utils/request'
551+ import { FileInputSchema , type RawFileInput } from ' @/lib/uploads/utils/file-schemas'
552+ import { processFilesToUserFiles } from ' @/lib/uploads/utils/file-utils'
553+ import { downloadFileFromStorage } from ' @/lib/uploads/utils/file-utils.server'
554+
555+ const logger = createLogger (' {Service}UploadAPI' )
556+
557+ const RequestSchema = z .object ({
558+ accessToken: z .string (),
559+ file: FileInputSchema .optional ().nullable (),
560+ // Legacy field for backwards compatibility
561+ fileContent: z .string ().optional ().nullable (),
562+ // ... other params
563+ })
564+
565+ export async function POST(request : NextRequest ) {
566+ const requestId = generateRequestId ()
567+
568+ const authResult = await checkInternalAuth (request , { requireWorkflowId: false })
569+ if (! authResult .success ) {
570+ return NextResponse .json ({ success: false , error: ' Unauthorized' }, { status: 401 })
571+ }
572+
573+ const body = await request .json ()
574+ const data = RequestSchema .parse (body )
575+
576+ let fileBuffer: Buffer
577+ let fileName: string
578+
579+ // Prefer UserFile input, fall back to legacy base64
580+ if (data .file ) {
581+ const userFiles = processFilesToUserFiles ([data .file as RawFileInput ], requestId , logger )
582+ if (userFiles .length === 0 ) {
583+ return NextResponse .json ({ success: false , error: ' Invalid file' }, { status: 400 })
584+ }
585+ const userFile = userFiles [0 ]
586+ fileBuffer = await downloadFileFromStorage (userFile , requestId , logger )
587+ fileName = userFile .name
588+ } else if (data .fileContent ) {
589+ // Legacy: base64 string (backwards compatibility)
590+ fileBuffer = Buffer .from (data .fileContent , ' base64' )
591+ fileName = ' file'
592+ } else {
593+ return NextResponse .json ({ success: false , error: ' File required' }, { status: 400 })
594+ }
595+
596+ // Now call external API with fileBuffer
597+ const response = await fetch (' https://api.{service}.com/upload' , {
598+ method: ' POST' ,
599+ headers: { Authorization: ` Bearer ${data .accessToken } ` },
600+ body: new Uint8Array (fileBuffer ), // Convert Buffer for fetch
601+ })
602+
603+ // ... handle response
604+ }
605+ ```
606+
607+ #### 4. Update Tool to Use Internal Route
608+
609+ ``` typescript
610+ export const {service}UploadTool: ToolConfig <Params , Response > = {
611+ id: ' {service}_upload' ,
612+ // ...
613+ params: {
614+ file: { type: ' file' , required: false , visibility: ' user-or-llm' },
615+ fileContent: { type: ' string' , required: false , visibility: ' hidden' }, // Legacy
616+ },
617+ request: {
618+ url: ' /api/tools/{service}/upload' , // Internal route
619+ method: ' POST' ,
620+ body : (params ) => ({
621+ accessToken: params .accessToken ,
622+ file: params .file ,
623+ fileContent: params .fileContent ,
624+ }),
625+ },
626+ }
627+ ```
628+
629+ ### File Output Pattern (Downloads)
630+
631+ For tools that return files, use ` FileToolProcessor ` to store files and return ` UserFile ` objects.
632+
633+ #### In Tool transformResponse
634+
635+ ``` typescript
636+ import { FileToolProcessor } from ' @/executor/utils/file-tool-processor'
637+
638+ transformResponse : async (response , context ) => {
639+ const data = await response .json ()
640+
641+ // Process file outputs to UserFile objects
642+ const fileProcessor = new FileToolProcessor (context )
643+ const file = await fileProcessor .processFileData ({
644+ data: data .content , // base64 or buffer
645+ mimeType: data .mimeType ,
646+ filename: data .filename ,
647+ })
648+
649+ return {
650+ success: true ,
651+ output: { file },
652+ }
653+ }
654+ ```
655+
656+ #### In API Route (for complex file handling)
657+
658+ ``` typescript
659+ // Return file data that FileToolProcessor can handle
660+ return NextResponse .json ({
661+ success: true ,
662+ output: {
663+ file: {
664+ data: base64Content ,
665+ mimeType: ' application/pdf' ,
666+ filename: ' document.pdf' ,
667+ },
668+ },
669+ })
670+ ```
671+
672+ ### Key Helpers Reference
673+
674+ | Helper | Location | Purpose |
675+ | --------| ----------| ---------|
676+ | ` normalizeFileInput ` | ` @/blocks/utils ` | Normalize file params in block config |
677+ | ` processFilesToUserFiles ` | ` @/lib/uploads/utils/file-utils ` | Convert raw inputs to UserFile[ ] |
678+ | ` downloadFileFromStorage ` | ` @/lib/uploads/utils/file-utils.server ` | Get file Buffer from UserFile |
679+ | ` FileToolProcessor ` | ` @/executor/utils/file-tool-processor ` | Process tool output files |
680+ | ` isUserFile ` | ` @/lib/core/utils/user-file ` | Type guard for UserFile objects |
681+ | ` FileInputSchema ` | ` @/lib/uploads/utils/file-schemas ` | Zod schema for file validation |
682+
683+ ### Common Gotchas
461684
4626851 . ** OAuth serviceId must match** - The ` serviceId ` in oauth-input must match the OAuth provider configuration
4636862 . ** Tool IDs are snake_case** - ` stripe_create_payment ` , not ` stripeCreatePayment `
4646873 . ** Block type is snake_case** - ` type: 'stripe' ` , not ` type: 'Stripe' `
4656884 . ** Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
4666895 . ** Required can be conditional** - Use ` required: { field: 'op', value: 'create' } ` instead of always true
4676906 . ** DependsOn clears options** - When a dependency changes, selector options are refetched
691+ 7 . ** Never pass Buffer directly to fetch** - Convert to ` new Uint8Array(buffer) ` for TypeScript compatibility
692+ 8 . ** Always handle legacy file params** - Keep hidden ` fileContent ` params for backwards compatibility
0 commit comments