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

How to Export Data from Retool to CSV

Enable the built-in Export button in the Table Inspector to let users download visible table data as CSV. For custom exports with specific columns, formatting, or filtered data, use utils.downloadFile(csvContent, 'export.csv', 'text/csv') in a JS Query to programmatically generate and trigger a CSV download.

What you'll learn

  • How to enable the built-in CSV export button on a Retool Table component
  • How to generate and download a custom CSV file using utils.downloadFile()
  • How to export only selected or filtered rows rather than all table data
  • How to format data during export (dates, currencies) for spreadsheet compatibility
  • How to build a programmatic CSV export with custom column headers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Enable the built-in Export button in the Table Inspector to let users download visible table data as CSV. For custom exports with specific columns, formatting, or filtered data, use utils.downloadFile(csvContent, 'export.csv', 'text/csv') in a JS Query to programmatically generate and trigger a CSV download.

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

Two Ways to Export CSV from Retool

Retool provides two approaches for CSV export. The built-in table export button is the simplest — toggle it on in the Table Inspector and users get a download button that exports the currently visible table data (respecting filters and current page). For more control over what gets exported, how it's formatted, and which columns are included, use utils.downloadFile() in a JS Query to generate a custom CSV string and trigger a browser download.

This tutorial covers both approaches, plus techniques for exporting all pages (not just the current page), formatting dates and currencies for spreadsheet compatibility, and adding a custom filename with today's date.

Prerequisites

  • A Retool app with a Table component connected to a query
  • Basic familiarity with JavaScript string operations
  • Understanding of Retool JS Queries

Step-by-step guide

1

Enable the Built-in Table Export Button

Select your Table component. In the Inspector, scroll to the Interaction section and find 'Allow downloading as CSV' (or 'Download CSV' toggle depending on your Retool version). Toggle it on. A download icon appears in the table header. Clicking it downloads the currently visible table data as a CSV file. The export includes all visible columns with their current display values, respects any active filters, and uses the column display names as headers.

Expected result: A download CSV icon appears in the table header, and clicking it triggers a browser file download.

2

Create a Custom CSV Export with utils.downloadFile()

For full control over your CSV output, create a JS Query that builds a CSV string and uses utils.downloadFile() to trigger a browser download. This approach lets you: include all rows (not just the current page), choose specific columns, format values, and set a custom filename. uts.downloadFile() takes three arguments: the file content (string), the filename, and the MIME type ('text/csv').

typescript
1// JS Query: exportToCSV
2// Triggered by a Button component
3
4// Get the data to export
5const rows = query1.data || [];
6
7// Define custom columns for export
8const columns = [
9 { key: 'id', header: 'ID' },
10 { key: 'first_name', header: 'First Name' },
11 { key: 'last_name', header: 'Last Name' },
12 { key: 'email', header: 'Email' },
13 { key: 'status', header: 'Status' },
14 { key: 'created_at', header: 'Created Date' },
15 { key: 'revenue', header: 'Revenue (USD)' }
16];
17
18// Build CSV header row
19const headerRow = columns.map(col => col.header).join(',');
20
21// Build CSV data rows
22const dataRows = rows.map(row =>
23 columns.map(col => {
24 let value = row[col.key];
25
26 // Format dates
27 if (col.key === 'created_at' && value) {
28 value = moment(value).format('YYYY-MM-DD');
29 }
30 // Format revenue
31 if (col.key === 'revenue') {
32 value = Number(value || 0).toFixed(2);
33 }
34
35 // Escape CSV: wrap in quotes if contains comma, quote, or newline
36 const str = String(value ?? '');
37 return str.includes(',') || str.includes('"') || str.includes('\n')
38 ? '"' + str.replace(/"/g, '""') + '"'
39 : str;
40 }).join(',')
41);
42
43// Combine header and data rows
44const csvContent = [headerRow, ...dataRows].join('\n');
45
46// Generate filename with today's date
47const today = moment().format('YYYY-MM-DD');
48const filename = `users-export-${today}.csv`;
49
50// Trigger browser download
51utils.downloadFile(csvContent, filename, 'text/csv');
52
53return { exported: rows.length };

Expected result: Clicking the export button downloads a well-formatted CSV file with custom headers and formatted values.

3

Export All Pages (Not Just the Current Page)

With server-side pagination, query1.data only contains the current page. To export all matching rows, create a separate SQL query named 'getAllForExport' that uses the same WHERE filters but without LIMIT/OFFSET. Trigger this query in your export JS Query before building the CSV. Add a loading notification so users know the export is fetching all data.

typescript
1// SQL Query: getAllForExport
2// Same WHERE as main paginated query but no LIMIT/OFFSET
3SELECT id, first_name, last_name, email, status, created_at, revenue
4FROM users
5WHERE
6 ({{ searchInput.value || '' }} = ''
7 OR first_name ILIKE {{ '%' + (searchInput.value || '') + '%' }}
8 OR email ILIKE {{ '%' + (searchInput.value || '') + '%' }})
9 AND ({{ statusFilter.value || '' }} = '' OR status = {{ statusFilter.value }})
10ORDER BY created_at DESC;
11
12// JS Query: exportAllToCSV
13utils.showNotification({
14 title: 'Preparing export...',
15 description: 'Fetching all matching rows',
16 notificationType: 'info',
17 duration: 2
18});
19
20// Fetch all rows
21const result = await getAllForExport.trigger();
22const rows = result.data;
23
24// Build CSV (same logic as previous step)
25// ... (insert CSV building code here)
26
27utils.showNotification({
28 title: 'Export ready',
29 description: rows.length + ' rows exported',
30 notificationType: 'success',
31 duration: 3
32});

Expected result: The export fetches all matching rows (not just the current page) and downloads them as a complete CSV.

4

Export Only Selected Rows

For targeted exports, let users select specific rows in the table and export only those. Use table1.selectedRows (the multi-select array, available when row selection is enabled in the Table Inspector) as the data source for your CSV. Enable multi-row selection in the Table Inspector: Interaction → Selection mode → 'Multiple rows'.

typescript
1// JS Query: exportSelectedRows
2// Uses table1.selectedRows (multi-select array)
3
4const rows = table1.selectedRows;
5
6if (!rows || rows.length === 0) {
7 utils.showNotification({
8 title: 'No rows selected',
9 description: 'Please select at least one row to export.',
10 notificationType: 'warning',
11 duration: 3
12 });
13 return { exported: 0 };
14}
15
16// Build CSV from selected rows only
17const headers = Object.keys(rows[0]).join(',');
18const dataRows = rows.map(row =>
19 Object.values(row).map(v => {
20 const str = String(v ?? '');
21 return str.includes(',') ? `"${str}"` : str;
22 }).join(',')
23);
24
25const csv = [headers, ...dataRows].join('\n');
26utils.downloadFile(csv, 'selected-rows.csv', 'text/csv');
27
28return { exported: rows.length };

Expected result: Only the user-selected rows are included in the CSV download.

5

Add a UTF-8 BOM for Excel Compatibility

CSV files opened in Microsoft Excel may show garbled characters (especially for international characters like accents, Chinese, etc.) without a UTF-8 BOM (Byte Order Mark). Add the BOM character as the first character of your CSV string to ensure Excel reads it correctly.

typescript
1// Add UTF-8 BOM for Excel compatibility:
2const BOM = '\uFEFF';
3const csvWithBOM = BOM + csvContent;
4
5utils.downloadFile(csvWithBOM, 'export.csv', 'text/csv;charset=utf-8;');

Expected result: The CSV file opens correctly in Microsoft Excel with all characters displayed accurately.

6

Add an Export Button with Loading State

Create a Button component for the export action. Set its label to 'Export CSV'. In the Inspector's Interaction section, add an event handler: on Click → Run JS Query → exportToCSV. Set the button's 'Disabled' property to {{ exportToCSV.isFetching }} so it disables during the export to prevent double-clicks. Change the button label dynamically: {{ exportToCSV.isFetching ? 'Exporting...' : 'Export CSV' }}.

typescript
1// Button label expression:
2{{ exportToCSV.isFetching ? 'Exporting...' : 'Export CSV (' + (query1.data?.length || 0) + ' rows)' }}
3
4// Button disabled expression:
5{{ exportToCSV.isFetching }}
6
7// Button tooltip:
8'Download current view as CSV'

Expected result: The export button shows a loading state while the export runs and displays the row count for user clarity.

Complete working example

JS Query: exportToCSV
1// JS Query: exportToCSV
2// Exports current query data with custom formatting
3// Triggered by 'Export CSV' button
4
5const rows = query1.data || [];
6
7if (rows.length === 0) {
8 utils.showNotification({
9 title: 'Nothing to export',
10 description: 'The table has no data.',
11 notificationType: 'warning',
12 duration: 3
13 });
14 return { exported: 0 };
15}
16
17// Column definitions: key = data field, header = CSV column name
18const columns = [
19 { key: 'id', header: 'ID' },
20 { key: 'first_name', header: 'First Name' },
21 { key: 'last_name', header: 'Last Name' },
22 { key: 'email', header: 'Email Address' },
23 { key: 'department', header: 'Department' },
24 { key: 'status', header: 'Status' },
25 { key: 'revenue', header: 'Revenue (USD)' },
26 { key: 'created_at', header: 'Created Date' }
27];
28
29// Helper: escape a value for CSV
30function escapeCSV(value) {
31 const str = String(value ?? '');
32 if (str.includes(',') || str.includes('"') || str.includes('\n')) {
33 return '"' + str.replace(/"/g, '""') + '"';
34 }
35 return str;
36}
37
38// Build header row
39const headerRow = columns.map(c => c.header).join(',');
40
41// Build data rows with formatting
42const dataRows = rows.map(row =>
43 columns.map(col => {
44 let val = row[col.key];
45 // Format dates as YYYY-MM-DD for spreadsheet compatibility
46 if (col.key === 'created_at' && val) {
47 val = moment(val).format('YYYY-MM-DD');
48 }
49 // Format numbers to 2 decimal places
50 if (col.key === 'revenue') {
51 val = Number(val || 0).toFixed(2);
52 }
53 // Uppercase status
54 if (col.key === 'status' && val) {
55 val = String(val).toUpperCase();
56 }
57 return escapeCSV(val);
58 }).join(',')
59);
60
61// UTF-8 BOM + CSV content
62const BOM = '\uFEFF';
63const csv = BOM + [headerRow, ...dataRows].join('\n');
64
65// Filename with timestamp
66const ts = moment().format('YYYY-MM-DD');
67const filename = `users-export-${ts}.csv`;
68
69// Trigger download
70utils.downloadFile(csv, filename, 'text/csv;charset=utf-8;');
71
72return { exported: rows.length, filename };

Common mistakes when exporting Data from Retool to CSV

Why it's a problem: Built-in export only downloads the current page, missing paginated rows

How to avoid: For complete exports with server-side pagination, create a separate SQL query without LIMIT/OFFSET and use it as the source for a programmatic JS Query CSV export.

Why it's a problem: CSV values with commas not being quoted, causing extra columns in the spreadsheet

How to avoid: Always escape CSV values: wrap strings containing commas, quotes, or newlines in double quotes and replace internal double quotes with two double quotes.

Why it's a problem: International characters showing as garbled text when opened in Excel

How to avoid: Add a UTF-8 BOM (\uFEFF) as the first character of the CSV string. Use 'text/csv;charset=utf-8;' as the MIME type in utils.downloadFile().

Why it's a problem: Dates exporting as ISO timestamps that Excel doesn't recognize as dates

How to avoid: Format dates as 'YYYY-MM-DD' using moment(value).format('YYYY-MM-DD') before including them in the CSV. This format is universally recognized by spreadsheet applications.

Best practices

  • Always escape CSV values that contain commas, double quotes, or newlines — wrap them in double quotes and escape internal double quotes by doubling them.
  • Add a UTF-8 BOM (\uFEFF) at the start of your CSV for Excel compatibility, especially when your data contains non-ASCII characters.
  • Use ISO date format (YYYY-MM-DD) in CSV exports rather than localized formats so dates import correctly in any spreadsheet application.
  • For large exports (100,000+ rows), warn users about the expected file size and time, or implement streaming with a server-side export endpoint.
  • Include only necessary columns in exports — omit internal IDs, encrypted fields, or system metadata that users do not need in their spreadsheets.
  • Add a timestamp to the export filename (users-export-2024-11-15.csv) so downloaded files do not overwrite each other.
  • Use the built-in table export for simple cases; build a custom JS Query export only when you need specific formatting, column selection, or multi-page data.

Still stuck?

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

ChatGPT Prompt

I'm building a Retool app and need a custom CSV export. The SQL query returns users with fields: id, first_name, last_name, email, department, status, revenue (float), and created_at (ISO timestamp). Write a Retool JS Query that: (1) builds a properly escaped CSV with custom column headers, (2) formats dates as YYYY-MM-DD using moment.js, (3) formats revenue as a 2-decimal number, (4) adds a UTF-8 BOM for Excel compatibility, (5) names the file with today's date, and (6) uses utils.downloadFile() to trigger the download. Include proper CSV escaping for values containing commas.

Retool Prompt

Create a JS Query named 'exportToCSV' in my Retool app that exports query1.data as a formatted CSV file. Include custom headers (ID, First Name, Last Name, Email, Status, Revenue, Created Date), format created_at dates as YYYY-MM-DD with moment.js, add a UTF-8 BOM, and use utils.downloadFile(). Also add an Export button component that shows 'Exporting...' while the query runs and displays the row count in its label.

Frequently asked questions

How do I export all rows in a Retool table with server-side pagination?

The built-in table export only downloads the current page. Create a separate SQL query with the same WHERE filters but without LIMIT/OFFSET, then trigger it in a JS Query export flow. The JS Query fetches all matching rows and passes them to utils.downloadFile() after building the CSV string.

Why does my Retool CSV export show garbled characters in Excel?

Excel defaults to Windows-1252 encoding when opening CSV files unless a UTF-8 BOM is present. Add the BOM character at the start of your CSV string: const csv = '\uFEFF' + csvContent. Also use 'text/csv;charset=utf-8;' as the MIME type in utils.downloadFile().

Can I export Retool table data to Excel format (.xlsx) instead of CSV?

Retool's utils.downloadFile() exports raw file content — it does not generate binary Excel files natively. For .xlsx exports, use a library like SheetJS (xlsx) loaded via Retool's Preloaded Scripts. This creates a more complex implementation; for most use cases, CSV with UTF-8 BOM opens cleanly in Excel.

How do I include only specific columns in a Retool CSV export?

Define a columns array in your JS Query that specifies which fields to include: [{ key: 'id', header: 'ID' }, { key: 'email', header: 'Email' }]. Then map each row to only those columns. This is more reliable than relying on the built-in export which includes all visible table columns.

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.