# How to Upload Files Using Retool

- Tool: Retool
- Difficulty: Beginner
- Time required: 20-30 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

Retool's File Picker component lets users select files from their computer. The selected file is accessible as {{ filePicker1.value[0] }} with properties: name, type, size, and contents (Base64-encoded string). For small files, insert the Base64 string into your database. For large files, generate an S3 presigned URL via a backend query and upload directly from a JS Query using fetch().

## File Upload Patterns with Retool's File Picker Component

The File Picker component is Retool's primary file input mechanism. When a user selects a file, Retool reads it client-side and makes it available as a Base64-encoded string in {{ filePicker1.value }}. This array contains one object per selected file, each with the file's name, MIME type, size in bytes, and Base64 contents.

This tutorial covers three patterns: (1) uploading small files (images, PDFs under ~5MB) as Base64 directly to a database column, (2) uploading files to a REST API endpoint as multipart form data, and (3) uploading large files to S3 using presigned URLs. The S3 pattern avoids Retool's payload limits and is the recommended approach for production file uploads.

It also covers MIME type filtering to restrict accepted file types, file size validation before submission, and showing upload progress to users.

## Before you start

- A Retool app with at least one resource (database or REST API) configured
- For S3 uploads: AWS credentials and an S3 bucket with appropriate CORS policy
- Basic familiarity with JS Queries and async/await

## Step-by-step guide

### 1. Add a File Picker component and configure MIME type filtering

Drag a File Picker component onto the canvas. In the Inspector's General section, find the Accept field. Enter a comma-separated list of accepted MIME types or file extensions. This filters the system file picker dialog to only show matching files. Common values: 'image/*' (all images), '.pdf' (PDFs only), 'image/png,image/jpeg' (PNG and JPEG), '.csv,.xlsx' (spreadsheets). Setting this does not prevent users from selecting other files via drag-and-drop — always validate server-side.

```
// Accept field examples:
// All images: image/*
// PDFs only: application/pdf
// CSV and Excel: .csv,.xlsx
// Images and PDFs: image/*,application/pdf
// Common document types: .pdf,.doc,.docx,.txt

// Allow multiple file selection:
// Enable 'Allow multiple files' toggle in Inspector
```

**Expected result:** The File Picker's dialog shows only files matching the Accept filter when the user clicks to select.

### 2. Access uploaded file data with filePicker1.value

After the user selects a file, the File Picker's value property is an array of file objects. Access the first file with {{ filePicker1.value[0] }}. Each file object has: name (filename string), type (MIME type string), size (bytes integer), and contents (Base64-encoded string). The contents field contains the complete file data as Base64, which you will use for database storage or API submission.

```
// Access file properties:
{{ filePicker1.value[0].name }}       // 'document.pdf'
{{ filePicker1.value[0].type }}       // 'application/pdf'
{{ filePicker1.value[0].size }}       // 245789 (bytes)
{{ filePicker1.value[0].contents }}   // 'JVBERi0xLjQK...' (Base64)

// Display file size in KB:
{{ Math.round(filePicker1.value[0]?.size / 1024) + ' KB' }}

// Check if a file is selected:
{{ filePicker1.value?.length > 0 }}

// For multiple files, iterate:
{{ filePicker1.value.map(f => f.name).join(', ') }}
```

**Expected result:** After file selection, {{ filePicker1.value[0].name }} shows the filename and {{ filePicker1.value[0].size }} shows the byte count.

### 3. Validate file size before uploading

Large Base64 strings slow down database inserts and may exceed Retool's query payload limits. Validate file size in the JS Query before attempting the upload. A 5MB file produces approximately 6.7MB of Base64 data. For database storage, limit to 2-5MB. For S3 uploads, you can allow larger files since you are not routing data through Retool's backend.

```
// JS Query: validateAndUpload
// Trigger: Upload button click

const file = filePicker1.value[0];

if (!file) {
  utils.showNotification({
    title: 'No file selected',
    description: 'Please select a file before uploading.',
    notificationType: 'warning',
  });
  return;
}

// Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf'];
if (!allowedTypes.includes(file.type)) {
  utils.showNotification({
    title: 'Invalid file type',
    description: `Only PNG, JPEG, and PDF files are accepted. You selected: ${file.type}`,
    notificationType: 'error',
  });
  return;
}

// Validate file size (5MB limit)
const MAX_SIZE_BYTES = 5 * 1024 * 1024;
if (file.size > MAX_SIZE_BYTES) {
  utils.showNotification({
    title: 'File too large',
    description: `Maximum file size is 5MB. Your file is ${Math.round(file.size / 1024 / 1024 * 10) / 10}MB.`,
    notificationType: 'error',
  });
  return;
}

// Proceed with upload...
```

**Expected result:** Attempting to upload a file larger than 5MB or with an invalid type shows an error notification and stops the upload.

### 4. Upload a small file as Base64 to a database

For files under 5MB (profile photos, small documents), store the Base64 contents directly in a database column of type TEXT or BYTEA. Create a SQL Resource Query named insertFile that takes the file properties as parameters. In a JS Query, validate the file then trigger insertFile with additionalScope. The Base64 string can be decoded back to binary for display using a Data URL prefix.

```
// SQL query: insertFile
-- Run mode: Manual (triggered from JS Query)
INSERT INTO user_files (user_id, file_name, file_type, file_size, file_data, uploaded_at)
VALUES (
  {{ userId }},
  {{ fileName }},
  {{ fileType }},
  {{ fileSize }},
  {{ fileData }},
  NOW()
)
RETURNING id;

// JS Query: uploadToDatabase
const file = filePicker1.value[0];
// (validation omitted — see previous step)

await insertFile.trigger({
  additionalScope: {
    userId: currentUser.id,
    fileName: file.name,
    fileType: file.type,
    fileSize: file.size,
    fileData: file.contents,  // Base64 string
  }
});

utils.showNotification({ title: 'Uploaded', notificationType: 'success' });
filePicker1.clearValue();
```

**Expected result:** The file data is stored in the database. {{ insertFile.data[0].id }} contains the new record's ID.

### 5. Upload a large file to S3 using a presigned URL

For files over 5MB or high-volume uploads, use S3 presigned URLs. The pattern: (1) call a backend API or Retool REST resource to generate a presigned PUT URL for the target S3 path, (2) use fetch() in a JS Query to upload the file directly from the browser to S3 using the presigned URL, (3) store the resulting S3 URL in your database. This bypasses Retool's payload limits entirely.

```
// Step 1 — SQL or REST query: getPresignedUrl
// This calls your backend API that generates an S3 presigned URL
// Returns: { upload_url: 'https://bucket.s3.amazonaws.com/...?X-Amz-Signature=...' }

// JS Query: uploadToS3
const file = filePicker1.value[0];
// (validation omitted)

// Step 1: Get presigned URL from backend
await getPresignedUrl.trigger({
  additionalScope: {
    fileName: file.name,
    fileType: file.type,
  }
});

const presignedUrl = getPresignedUrl.data.upload_url;
const s3Key = getPresignedUrl.data.s3_key;

// Step 2: Decode Base64 to binary and upload to S3
const base64Data = file.contents;
const binaryStr = atob(base64Data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
  bytes[i] = binaryStr.charCodeAt(i);
}
const blob = new Blob([bytes], { type: file.type });

const uploadResponse = await fetch(presignedUrl, {
  method: 'PUT',
  body: blob,
  headers: { 'Content-Type': file.type },
});

if (!uploadResponse.ok) {
  throw new Error(`S3 upload failed: ${uploadResponse.status}`);
}

// Step 3: Store the S3 URL in the database
const publicUrl = `https://your-bucket.s3.amazonaws.com/${s3Key}`;
await saveFileRecord.trigger({
  additionalScope: { fileUrl: publicUrl, fileName: file.name }
});

utils.showNotification({ title: 'File uploaded successfully', notificationType: 'success' });
```

**Expected result:** Large files upload directly to S3 from the browser. The S3 URL is stored in the database for later retrieval.

### 6. Display an uploaded image preview

To preview an uploaded image before submitting, use an Image component and bind its Image source to a Data URL constructed from the Base64 content. Data URLs have the format: 'data:{mimeType};base64,{base64Content}'. Set the Image component's Image source to this expression.

```
// Image component → Image source expression:
{{ filePicker1.value?.length > 0
   ? `data:${filePicker1.value[0].type};base64,${filePicker1.value[0].contents}`
   : '' }}

// Hide the Image component when no file is selected:
// Hidden: {{ !filePicker1.value || filePicker1.value.length === 0 }}

// For CSV files, preview the first few rows in a Table:
// Use a transformer to parse the Base64 CSV to an array of objects
```

**Expected result:** An image preview appears below the File Picker immediately after the user selects an image file.

## Complete code example

File: `JS Query: uploadToS3WithFallback`

```javascript
// uploadToS3WithFallback
// Full file upload flow: validate → presign → upload → save record

const file = filePicker1.value?.[0];

// --- Validation ---
if (!file) {
  utils.showNotification({
    title: 'No file selected',
    description: 'Please choose a file before uploading.',
    notificationType: 'warning',
  });
  return;
}

const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'application/pdf'];
const MAX_MB = 50;
const MAX_BYTES = MAX_MB * 1024 * 1024;

if (!ALLOWED_TYPES.includes(file.type)) {
  utils.showNotification({
    title: 'File type not allowed',
    description: `Accepted types: PNG, JPEG, GIF, PDF. Got: ${file.type}`,
    notificationType: 'error',
  });
  return;
}

if (file.size > MAX_BYTES) {
  utils.showNotification({
    title: 'File too large',
    description: `Maximum size: ${MAX_MB}MB. Your file: ${(file.size / 1024 / 1024).toFixed(1)}MB`,
    notificationType: 'error',
  });
  return;
}

// --- Get presigned URL from backend ---
await getPresignedUrl.trigger({
  additionalScope: { fileName: file.name, fileType: file.type }
});

const { upload_url, s3_key, public_url } = getPresignedUrl.data;

// --- Convert Base64 to binary Blob ---
const base64 = file.contents;
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const blob = new Blob([bytes], { type: file.type });

// --- Upload to S3 ---
const response = await fetch(upload_url, {
  method: 'PUT',
  body: blob,
  headers: { 'Content-Type': file.type },
});

if (!response.ok) {
  throw new Error(`Upload failed with status ${response.status}`);
}

// --- Save metadata to database ---
await saveFileMetadata.trigger({
  additionalScope: {
    userId: currentUser.id,
    fileName: file.name,
    fileType: file.type,
    fileSizeBytes: file.size,
    s3Key: s3_key,
    publicUrl: public_url,
  }
});

utils.showNotification({
  title: 'Upload complete',
  description: `${file.name} uploaded successfully.`,
  notificationType: 'success',
});

filePicker1.clearValue();
```

## Common mistakes

- **Accessing filePicker1.value without checking if it is empty — throws a TypeError when no file is selected** — undefined Fix: Always guard with optional chaining: filePicker1.value?.[0] or check filePicker1.value?.length > 0 before accessing properties.
- **Trying to upload a file directly from a SQL query by passing the Base64 string as a parameter — works for small files but fails or is extremely slow for files over 1MB** — undefined Fix: For files over 5MB, use the S3 presigned URL pattern to upload directly from the browser without routing binary data through Retool's backend.
- **Forgetting that file.contents does not include the 'data:image/png;base64,' prefix needed for Data URLs** — undefined Fix: When building a Data URL for display, prepend the MIME type header: `data:${file.type};base64,${file.contents}`. The contents field is the raw Base64 data without the header.
- **Attempting to read filePicker1.value[0].contents inside a transformer and then trigger the upload from within the transformer** — undefined Fix: Transformers are read-only and cannot trigger queries or perform side effects like HTTP requests. Move all upload logic to a JS Query.

## Best practices

- Always validate file type and size in the JS Query before uploading — the Accept filter is UX-only and can be bypassed
- Use S3 presigned URLs for files over 5MB — routing large Base64 strings through Retool's queries is slow and may hit payload limits
- Clear the File Picker after successful upload with filePicker1.clearValue() to reset the UI for the next upload
- Store file metadata (name, type, size, S3 URL) in a database record rather than just the file — makes files retrievable and auditable
- Generate presigned URLs immediately before upload, not on page load — they have expiry times typically between 15 minutes and 1 hour
- Display an upload preview before submission for image files — users want to confirm they selected the right file
- For CSV uploads, parse the Base64 contents in a transformer to preview the data in a Table component before committing

## Frequently asked questions

### Can users upload multiple files at once with the File Picker?

Yes. Enable the 'Allow multiple files' toggle in the Inspector. {{ filePicker1.value }} then returns an array with one object per file. Iterate over it with filePicker1.value.forEach() or .map() in a JS Query to upload each file individually.

### What is the maximum file size Retool can handle through the File Picker?

There is no hard limit on the file size the File Picker can read — it reads into browser memory. However, Retool query payloads have practical limits around 50MB. For large files, use the S3 presigned URL approach to upload directly from the browser to S3, bypassing Retool's query payload entirely.

### How do I upload a CSV file and import its data into a database table?

Read the CSV Base64 content from filePicker1.value[0].contents, decode it with atob(), then parse the CSV rows using a transformer or JS Query with a CSV parsing approach (split by newlines and commas, or use the Papa Parse library if loaded via Preloaded JS). Then use a bulk INSERT query with the parsed rows as parameters.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-upload-files-using-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-upload-files-using-retool
