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().
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 20-30 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
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.
Prerequisites
- 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
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.
1// Accept field examples:2// All images: image/*3// PDFs only: application/pdf4// CSV and Excel: .csv,.xlsx5// Images and PDFs: image/*,application/pdf6// Common document types: .pdf,.doc,.docx,.txt78// Allow multiple file selection:9// Enable 'Allow multiple files' toggle in InspectorExpected result: The File Picker's dialog shows only files matching the Accept filter when the user clicks to select.
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.
1// Access file properties:2{{ filePicker1.value[0].name }} // 'document.pdf'3{{ filePicker1.value[0].type }} // 'application/pdf'4{{ filePicker1.value[0].size }} // 245789 (bytes)5{{ filePicker1.value[0].contents }} // 'JVBERi0xLjQK...' (Base64)67// Display file size in KB:8{{ Math.round(filePicker1.value[0]?.size / 1024) + ' KB' }}910// Check if a file is selected:11{{ filePicker1.value?.length > 0 }}1213// For multiple files, iterate:14{{ 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.
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.
1// JS Query: validateAndUpload2// Trigger: Upload button click34const file = filePicker1.value[0];56if (!file) {7 utils.showNotification({8 title: 'No file selected',9 description: 'Please select a file before uploading.',10 notificationType: 'warning',11 });12 return;13}1415// Validate file type16const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf'];17if (!allowedTypes.includes(file.type)) {18 utils.showNotification({19 title: 'Invalid file type',20 description: `Only PNG, JPEG, and PDF files are accepted. You selected: ${file.type}`,21 notificationType: 'error',22 });23 return;24}2526// Validate file size (5MB limit)27const MAX_SIZE_BYTES = 5 * 1024 * 1024;28if (file.size > MAX_SIZE_BYTES) {29 utils.showNotification({30 title: 'File too large',31 description: `Maximum file size is 5MB. Your file is ${Math.round(file.size / 1024 / 1024 * 10) / 10}MB.`,32 notificationType: 'error',33 });34 return;35}3637// 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.
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.
1// SQL query: insertFile2-- Run mode: Manual (triggered from JS Query)3INSERT INTO user_files (user_id, file_name, file_type, file_size, file_data, uploaded_at)4VALUES (5 {{ userId }},6 {{ fileName }},7 {{ fileType }},8 {{ fileSize }},9 {{ fileData }},10 NOW()11)12RETURNING id;1314// JS Query: uploadToDatabase15const file = filePicker1.value[0];16// (validation omitted — see previous step)1718await insertFile.trigger({19 additionalScope: {20 userId: currentUser.id,21 fileName: file.name,22 fileType: file.type,23 fileSize: file.size,24 fileData: file.contents, // Base64 string25 }26});2728utils.showNotification({ title: 'Uploaded', notificationType: 'success' });29filePicker1.clearValue();Expected result: The file data is stored in the database. {{ insertFile.data[0].id }} contains the new record's ID.
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.
1// Step 1 — SQL or REST query: getPresignedUrl2// This calls your backend API that generates an S3 presigned URL3// Returns: { upload_url: 'https://bucket.s3.amazonaws.com/...?X-Amz-Signature=...' }45// JS Query: uploadToS36const file = filePicker1.value[0];7// (validation omitted)89// Step 1: Get presigned URL from backend10await getPresignedUrl.trigger({11 additionalScope: {12 fileName: file.name,13 fileType: file.type,14 }15});1617const presignedUrl = getPresignedUrl.data.upload_url;18const s3Key = getPresignedUrl.data.s3_key;1920// Step 2: Decode Base64 to binary and upload to S321const base64Data = file.contents;22const binaryStr = atob(base64Data);23const bytes = new Uint8Array(binaryStr.length);24for (let i = 0; i < binaryStr.length; i++) {25 bytes[i] = binaryStr.charCodeAt(i);26}27const blob = new Blob([bytes], { type: file.type });2829const uploadResponse = await fetch(presignedUrl, {30 method: 'PUT',31 body: blob,32 headers: { 'Content-Type': file.type },33});3435if (!uploadResponse.ok) {36 throw new Error(`S3 upload failed: ${uploadResponse.status}`);37}3839// Step 3: Store the S3 URL in the database40const publicUrl = `https://your-bucket.s3.amazonaws.com/${s3Key}`;41await saveFileRecord.trigger({42 additionalScope: { fileUrl: publicUrl, fileName: file.name }43});4445utils.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.
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.
1// Image component → Image source expression:2{{ filePicker1.value?.length > 03 ? `data:${filePicker1.value[0].type};base64,${filePicker1.value[0].contents}`4 : '' }}56// Hide the Image component when no file is selected:7// Hidden: {{ !filePicker1.value || filePicker1.value.length === 0 }}89// For CSV files, preview the first few rows in a Table:10// Use a transformer to parse the Base64 CSV to an array of objectsExpected result: An image preview appears below the File Picker immediately after the user selects an image file.
Complete working example
1// uploadToS3WithFallback2// Full file upload flow: validate → presign → upload → save record34const file = filePicker1.value?.[0];56// --- Validation ---7if (!file) {8 utils.showNotification({9 title: 'No file selected',10 description: 'Please choose a file before uploading.',11 notificationType: 'warning',12 });13 return;14}1516const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'application/pdf'];17const MAX_MB = 50;18const MAX_BYTES = MAX_MB * 1024 * 1024;1920if (!ALLOWED_TYPES.includes(file.type)) {21 utils.showNotification({22 title: 'File type not allowed',23 description: `Accepted types: PNG, JPEG, GIF, PDF. Got: ${file.type}`,24 notificationType: 'error',25 });26 return;27}2829if (file.size > MAX_BYTES) {30 utils.showNotification({31 title: 'File too large',32 description: `Maximum size: ${MAX_MB}MB. Your file: ${(file.size / 1024 / 1024).toFixed(1)}MB`,33 notificationType: 'error',34 });35 return;36}3738// --- Get presigned URL from backend ---39await getPresignedUrl.trigger({40 additionalScope: { fileName: file.name, fileType: file.type }41});4243const { upload_url, s3_key, public_url } = getPresignedUrl.data;4445// --- Convert Base64 to binary Blob ---46const base64 = file.contents;47const binary = atob(base64);48const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));49const blob = new Blob([bytes], { type: file.type });5051// --- Upload to S3 ---52const response = await fetch(upload_url, {53 method: 'PUT',54 body: blob,55 headers: { 'Content-Type': file.type },56});5758if (!response.ok) {59 throw new Error(`Upload failed with status ${response.status}`);60}6162// --- Save metadata to database ---63await saveFileMetadata.trigger({64 additionalScope: {65 userId: currentUser.id,66 fileName: file.name,67 fileType: file.type,68 fileSizeBytes: file.size,69 s3Key: s3_key,70 publicUrl: public_url,71 }72});7374utils.showNotification({75 title: 'Upload complete',76 description: `${file.name} uploaded successfully.`,77 notificationType: 'success',78});7980filePicker1.clearValue();Common mistakes
Why it's a problem: Accessing filePicker1.value without checking if it is empty — throws a TypeError when no file is selected
How to avoid: Always guard with optional chaining: filePicker1.value?.[0] or check filePicker1.value?.length > 0 before accessing properties.
Why it's a problem: 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
How to avoid: For files over 5MB, use the S3 presigned URL pattern to upload directly from the browser without routing binary data through Retool's backend.
Why it's a problem: Forgetting that file.contents does not include the 'data:image/png;base64,' prefix needed for Data URLs
How to avoid: 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.
Why it's a problem: Attempting to read filePicker1.value[0].contents inside a transformer and then trigger the upload from within the transformer
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build a file upload feature in Retool. I have a File Picker component named filePicker1 configured to accept images and PDFs. I want to: (1) validate that the file is under 10MB and is one of [image/png, image/jpeg, application/pdf], (2) upload the file to S3 using a presigned URL from my backend REST API (returns { upload_url, s3_key, public_url }), (3) save the metadata to my PostgreSQL database via a query named saveFileMetadata. Write the complete JS Query for this flow including Base64 to Blob conversion and the fetch() PUT request.
I added a File Picker to my Retool app. How do I access the selected file's name, type, and Base64 content? What is the correct expression to show a preview of a selected image in an Image component? Also, how do I clear the file picker after a successful upload?
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation