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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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').
1// JS Query: exportToCSV2// Triggered by a Button component34// Get the data to export5const rows = query1.data || [];67// Define custom columns for export8const 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];1718// Build CSV header row19const headerRow = columns.map(col => col.header).join(',');2021// Build CSV data rows22const dataRows = rows.map(row =>23 columns.map(col => {24 let value = row[col.key];2526 // Format dates27 if (col.key === 'created_at' && value) {28 value = moment(value).format('YYYY-MM-DD');29 }30 // Format revenue31 if (col.key === 'revenue') {32 value = Number(value || 0).toFixed(2);33 }3435 // Escape CSV: wrap in quotes if contains comma, quote, or newline36 const str = String(value ?? '');37 return str.includes(',') || str.includes('"') || str.includes('\n')38 ? '"' + str.replace(/"/g, '""') + '"'39 : str;40 }).join(',')41);4243// Combine header and data rows44const csvContent = [headerRow, ...dataRows].join('\n');4546// Generate filename with today's date47const today = moment().format('YYYY-MM-DD');48const filename = `users-export-${today}.csv`;4950// Trigger browser download51utils.downloadFile(csvContent, filename, 'text/csv');5253return { exported: rows.length };Expected result: Clicking the export button downloads a well-formatted CSV file with custom headers and formatted values.
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.
1// SQL Query: getAllForExport2// Same WHERE as main paginated query but no LIMIT/OFFSET3SELECT id, first_name, last_name, email, status, created_at, revenue4FROM users5WHERE6 ({{ 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;1112// JS Query: exportAllToCSV13utils.showNotification({14 title: 'Preparing export...',15 description: 'Fetching all matching rows',16 notificationType: 'info',17 duration: 218});1920// Fetch all rows21const result = await getAllForExport.trigger();22const rows = result.data;2324// Build CSV (same logic as previous step)25// ... (insert CSV building code here)2627utils.showNotification({28 title: 'Export ready',29 description: rows.length + ' rows exported',30 notificationType: 'success',31 duration: 332});Expected result: The export fetches all matching rows (not just the current page) and downloads them as a complete CSV.
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'.
1// JS Query: exportSelectedRows2// Uses table1.selectedRows (multi-select array)34const rows = table1.selectedRows;56if (!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: 312 });13 return { exported: 0 };14}1516// Build CSV from selected rows only17const 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);2425const csv = [headers, ...dataRows].join('\n');26utils.downloadFile(csv, 'selected-rows.csv', 'text/csv');2728return { exported: rows.length };Expected result: Only the user-selected rows are included in the CSV download.
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.
1// Add UTF-8 BOM for Excel compatibility:2const BOM = '\uFEFF';3const csvWithBOM = BOM + csvContent;45utils.downloadFile(csvWithBOM, 'export.csv', 'text/csv;charset=utf-8;');Expected result: The CSV file opens correctly in Microsoft Excel with all characters displayed accurately.
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' }}.
1// Button label expression:2{{ exportToCSV.isFetching ? 'Exporting...' : 'Export CSV (' + (query1.data?.length || 0) + ' rows)' }}34// Button disabled expression:5{{ exportToCSV.isFetching }}67// 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
1// JS Query: exportToCSV2// Exports current query data with custom formatting3// Triggered by 'Export CSV' button45const rows = query1.data || [];67if (rows.length === 0) {8 utils.showNotification({9 title: 'Nothing to export',10 description: 'The table has no data.',11 notificationType: 'warning',12 duration: 313 });14 return { exported: 0 };15}1617// Column definitions: key = data field, header = CSV column name18const 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];2829// Helper: escape a value for CSV30function 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}3738// Build header row39const headerRow = columns.map(c => c.header).join(',');4041// Build data rows with formatting42const dataRows = rows.map(row =>43 columns.map(col => {44 let val = row[col.key];45 // Format dates as YYYY-MM-DD for spreadsheet compatibility46 if (col.key === 'created_at' && val) {47 val = moment(val).format('YYYY-MM-DD');48 }49 // Format numbers to 2 decimal places50 if (col.key === 'revenue') {51 val = Number(val || 0).toFixed(2);52 }53 // Uppercase status54 if (col.key === 'status' && val) {55 val = String(val).toUpperCase();56 }57 return escapeCSV(val);58 }).join(',')59);6061// UTF-8 BOM + CSV content62const BOM = '\uFEFF';63const csv = BOM + [headerRow, ...dataRows].join('\n');6465// Filename with timestamp66const ts = moment().format('YYYY-MM-DD');67const filename = `users-export-${ts}.csv`;6869// Trigger download70utils.downloadFile(csv, filename, 'text/csv;charset=utf-8;');7172return { 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation