Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Import Data into Retool

Import data into Retool in three ways: (1) connect an external database or API as a resource in Settings → Resources, (2) use Retool Database's built-in CSV import from the Database UI, or (3) accept a file upload with the File Picker component and write the data to your database using a batch insert JS Query.

What you'll learn

  • How to connect a database, REST API, or Retool Database as a data source
  • How to import CSV data into Retool Database using the built-in import tool
  • How to accept user CSV file uploads via the File Picker component and process them
  • How to perform bulk inserts from uploaded data using a JS Query batch pattern
  • The difference between connecting a data source and importing data into Retool's own database
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read20-25 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Import data into Retool in three ways: (1) connect an external database or API as a resource in Settings → Resources, (2) use Retool Database's built-in CSV import from the Database UI, or (3) accept a file upload with the File Picker component and write the data to your database using a batch insert JS Query.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required20-25 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Three Paths for Getting Data into Retool

Retool does not store data itself (except in Retool Database) — it queries and writes to your existing data sources. 'Importing data' in Retool has three distinct meanings depending on your use case.

First, connecting an external database or API resource so Retool can query it — this is the most common setup. Second, importing CSV or JSON data into Retool's built-in Retool Database for lightweight data storage. Third, building an in-app file upload flow where end users upload CSV files and your app processes and inserts the data into a database.

This tutorial covers all three approaches with concrete steps and code examples.

Prerequisites

  • A Retool account with admin access (for creating resources)
  • For external database connection: database credentials (host, port, username, password, database name)
  • For CSV import: a CSV file with a header row
  • Basic familiarity with Retool's editor and query panel

Step-by-step guide

1

Connect an External Database as a Resource

Go to Settings → Resources (or navigate to retool.com/resources). Click '+ Create new'. Choose your database type: PostgreSQL, MySQL, MongoDB, Snowflake, BigQuery, etc. Fill in the connection details: host, port, database name, username, and password. For cloud databases, you may also need to whitelist Retool's IP addresses (listed in the resource connection screen). Once connected, the resource appears in the query editor's Resource dropdown for any app.

Expected result: The resource appears in Settings → Resources with a green 'Connected' indicator.

2

Write Your First Query Against the Connected Resource

Open an app (or create a new one). Click the '+' button in the query panel at the bottom and select your new resource. Write a SELECT query to test the connection and verify data is flowing. Set 'Run query on page load' in the query's Advanced settings to automatically fetch data when the app opens.

typescript
1-- Test query: verify data access
2SELECT *
3FROM information_schema.tables
4WHERE table_schema = 'public'
5LIMIT 10;
6
7-- Or a direct table query:
8SELECT id, name, email, created_at
9FROM users
10ORDER BY created_at DESC
11LIMIT 50;

Expected result: The query runs and returns data visible in the query result panel.

3

Import CSV into Retool Database

Retool Database is Retool's built-in PostgreSQL database — useful for storing app configuration, lookup tables, or small datasets. To import a CSV, navigate to the Retool Database UI (from the main sidebar or retoolapp.com/database). Open or create a table, then click 'Import CSV' from the table menu. Retool Database automatically creates columns matching your CSV headers and detects column types. You can edit the schema before importing.

Expected result: The CSV data is imported into Retool Database and visible as table rows with correct column types.

4

Set Up a File Picker for User CSV Uploads

To let users upload CSV files directly in your app, add a File Picker component to your canvas. In the Inspector, set 'File type' to accept CSV files: text/csv,text/plain. When a user selects a file, it is accessible as filePicker1.value — an array of file objects, each with a name, type, and base64-encoded content. The CSV content is in filePicker1.value[0].base64 as a base64 string that you decode in a JS Query.

typescript
1// File Picker component Inspector settings:
2// - File type: text/csv,text/plain
3// - Max file size: 5 (MB) — adjust based on your needs
4// - Multiple files: Off (for single CSV import)
5
6// In a text component, show the selected file name:
7{{ filePicker1.value?.[0]?.name ?? 'No file selected' }}
8
9// File Picker value structure:
10// filePicker1.value = [{
11// name: 'users.csv',
12// type: 'text/csv',
13// base64: '...base64 encoded CSV content...',
14// size: 12345
15// }]

Expected result: Users can select a CSV file via the File Picker and the selected filename appears in the UI.

5

Parse the CSV Content in a JS Query

Create a JS Query named 'parseCSV' to decode the base64 content and parse it into an array of row objects. This query runs when a user clicks a 'Preview / Import' button. Retool does not have a built-in CSV parser, so use string splitting. For complex CSVs with quoted fields, consider loading Papa Parse via Preloaded Scripts.

typescript
1// JS Query: parseCSV
2// Triggered by 'Preview CSV' button
3
4const file = filePicker1.value?.[0];
5if (!file) {
6 utils.showNotification({
7 title: 'No file selected',
8 notificationType: 'warning',
9 duration: 3
10 });
11 return [];
12}
13
14// Decode base64 to text
15const base64Content = file.base64;
16const csvText = atob(base64Content);
17
18// Split into lines and parse
19const lines = csvText.split('\n').filter(line => line.trim());
20if (lines.length < 2) {
21 utils.showNotification({
22 title: 'Empty CSV',
23 description: 'File has no data rows.',
24 notificationType: 'warning'
25 });
26 return [];
27}
28
29// Parse header row
30const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
31
32// Parse data rows
33const rows = lines.slice(1).map(line => {
34 const values = line.split(',').map(v => v.trim().replace(/"/g, ''));
35 const row = {};
36 headers.forEach((header, i) => {
37 row[header] = values[i] ?? '';
38 });
39 return row;
40});
41
42// Save to a temporary state variable for preview
43await parsedCSVData.setValue(rows);
44// Note: setValue() is async — do not read parsedCSVData immediately after
45
46return rows;

Expected result: The CSV is decoded and parsed into an array of row objects, stored in parsedCSVData state variable.

6

Preview the Parsed Data in a Table

Before inserting data, show users a preview table so they can verify the parsed content. Add a Table component and bind its Data source to {{ parsedCSVData.value }}. This lets users confirm the data looks correct before the bulk insert. Add a row count label: {{ (parsedCSVData.value || []).length + ' rows ready to import' }}

Expected result: A preview table shows the parsed CSV data before the user confirms the import.

7

Bulk Insert Parsed Rows with a JS Query

After preview, create a JS Query that inserts all parsed rows into your database. For large datasets, process in batches to avoid query timeouts. Use INSERT ... VALUES for each batch. For PostgreSQL, build a multi-row INSERT or loop over individual inserts with error handling.

typescript
1// JS Query: importCSVData
2// Triggered by 'Confirm Import' button
3
4const rows = parsedCSVData.value;
5if (!rows || rows.length === 0) {
6 utils.showNotification({
7 title: 'No data to import',
8 notificationType: 'warning'
9 });
10 return;
11}
12
13// Process in batches of 100 to avoid timeouts
14const BATCH_SIZE = 100;
15let inserted = 0;
16let errors = [];
17
18for (let i = 0; i < rows.length; i += BATCH_SIZE) {
19 const batch = rows.slice(i, i + BATCH_SIZE);
20
21 try {
22 // Trigger the SQL insert query for this batch
23 await insertUsersBatch.trigger({
24 additionalScope: { batchRows: batch }
25 });
26 inserted += batch.length;
27 } catch (err) {
28 errors.push({ batch: i, error: err.message });
29 }
30}
31
32const message = errors.length > 0
33 ? `Imported ${inserted} rows with ${errors.length} batch errors`
34 : `Successfully imported ${inserted} rows`;
35
36utils.showNotification({
37 title: 'Import complete',
38 description: message,
39 notificationType: errors.length > 0 ? 'warning' : 'success'
40});
41
42// Refresh the main data table
43await query1.trigger();
44
45return { inserted, errors };
46
47// ----
48// SQL Query: insertUsersBatch
49// (Called in a loop above via additionalScope)
50// This requires a dynamic INSERT — use Retool's 'Bulk Insert' query type
51// or construct the VALUES dynamically with a JS Query.

Expected result: All CSV rows are inserted into the database in batches, with a success/error notification showing the result.

Complete working example

JS Query: importCSVComplete
1// Complete CSV import flow:
2// 1. Read file from File Picker (filePicker1)
3// 2. Parse CSV
4// 3. Preview in table (bound to parsedRows state)
5// 4. Insert to database in batches
6
7// ---- Step 1 & 2: Parse CSV ----
8const file = filePicker1.value?.[0];
9if (!file) throw new Error('No file selected');
10
11const csvText = atob(file.base64);
12const allLines = csvText.split('\n').map(l => l.trim()).filter(Boolean);
13if (allLines.length < 2) throw new Error('CSV has no data rows');
14
15const headers = allLines[0]
16 .split(',')
17 .map(h => h.trim().replace(/^"|"$/g, ''));
18
19const rows = allLines.slice(1).map(line => {
20 // Basic CSV split (handles simple quoted fields)
21 const values = line.match(/(?:"([^"]*)"|([^,]*))/g)
22 ?.map(v => v.replace(/^"|"$/g, '').trim()) ?? [];
23 const row = {};
24 headers.forEach((h, i) => { row[h] = values[i] ?? ''; });
25 return row;
26});
27
28// Validate required columns exist
29const required = ['first_name', 'last_name', 'email'];
30const missing = required.filter(r => !headers.includes(r));
31if (missing.length > 0) {
32 throw new Error('CSV missing required columns: ' + missing.join(', '));
33}
34
35// ---- Step 3: Store for preview ----
36await parsedRows.setValue(rows);
37// Note: setValue() is async — subsequent reads must await
38
39utils.showNotification({
40 title: `${rows.length} rows parsed`,
41 description: 'Review the preview table then click Confirm Import.',
42 notificationType: 'info',
43 duration: 5
44});
45
46return { rowCount: rows.length, headers };

Common mistakes

Why it's a problem: Accessing filePicker1.value[0].content expecting plain text instead of base64

How to avoid: The File Picker stores file content as base64 in filePicker1.value[0].base64. Decode with atob(base64Content) to get the plain text CSV string.

Why it's a problem: Inserting all rows in a single query causing a timeout for large CSVs

How to avoid: Process rows in batches of 50-200 using a for loop in a JS Query. Each iteration triggers the insert query for a batch and awaits completion before the next.

Why it's a problem: setValue() on the parsed data state variable, then immediately reading it in the same query

How to avoid: setValue() is async. Call 'await parsedRows.setValue(rows)' before attempting to read parsedRows.value in the same JS Query.

Why it's a problem: Not validating CSV headers, causing silent insert failures when the file has wrong column names

How to avoid: Check for required headers at the start of the parse JS Query: const missing = requiredHeaders.filter(h => !actualHeaders.includes(h)). Show a clear error if any required columns are missing.

Best practices

  • Always preview parsed CSV data before inserting — show users a sample table so they can catch parsing errors before committing to the database.
  • Validate required columns exist in the CSV header row before attempting inserts — give a clear error message listing which columns are missing.
  • Insert in batches of 50-200 rows rather than one row at a time to balance performance and timeout risk.
  • For production database connections, use a dedicated Retool database user with minimal permissions — SELECT plus INSERT on specific tables only.
  • Handle CSV encoding issues: add a UTF-8 BOM check and trim whitespace from all parsed values.
  • For complex CSVs with quoted fields containing commas or newlines, load Papa Parse via Preloaded Scripts instead of writing a custom parser.
  • After a bulk import, trigger a re-run of the main data query so the table reflects the newly inserted rows.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Retool app that lets users upload a CSV file and bulk import the data into a PostgreSQL database. The CSV has these columns: first_name, last_name, email, department, role. Write the complete implementation including: (1) File Picker component setup, (2) a JS Query that decodes the base64 file content and parses the CSV into an array of row objects, (3) column validation to check required fields exist, (4) a batch insert JS Query that inserts rows in groups of 100, and (5) error handling with utils.showNotification().

Retool Prompt

Set up a CSV import flow in my Retool app. Add a File Picker component (accept text/csv). Create a JS Query named 'parseCSV' that reads filePicker1.value[0].base64, decodes with atob(), parses into an array of row objects using the header row as keys, stores the result in a temporary state variable named 'parsedRows' (using setValue()), and shows a notification with the row count. Include validation for required columns: first_name, last_name, email.

Frequently asked questions

How do I read a CSV file uploaded via File Picker in Retool?

The File Picker stores file content in filePicker1.value[0].base64 as a base64-encoded string. Decode it with atob(filePicker1.value[0].base64) in a JS Query to get the plain text CSV content. Then split by newlines and commas to parse the rows and headers.

What is Retool Database and how is it different from connecting my own database?

Retool Database is a managed PostgreSQL database built into Retool — useful for storing small datasets, configuration tables, or reference data without needing your own database. Connecting your own database (PostgreSQL, MySQL, etc.) via Settings → Resources lets Retool query your existing production or staging data without moving it into Retool.

How many rows can I import in a single Retool CSV import?

For Retool Database's built-in CSV import, there is no strict row limit but performance degrades with very large files. For programmatic imports via JS Query, Retool's query timeout (default 30-120 seconds) is the limiting factor. Use batch inserts (50-200 rows per batch) to stay within timeout limits for files with thousands of rows.

Can I import data from a Google Sheet into Retool?

Yes, in two ways. First, connect Google Sheets as a resource in Settings → Resources — Retool has a native Google Sheets integration that lets you query sheet data directly. Second, export your Google Sheet as CSV and use the file upload approach described in this tutorial. The native integration is simpler for ongoing syncing.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.