Guides

Retool Export CSV Missing Headers with Dynamic Columns Fix

OTC Team··4 min read
Retool Export CSV Missing Headers with Dynamic Columns Fix

If you've ever exported a CSV from a Retool table with dynamic columns enabled and noticed the first row is completely blank where your headers should be, you've hit a known Retool bug. The Retool export CSV missing headers dynamic columns issue has affected teams for well over a year, and while Retool hasn't shipped an official fix, there are solid workarounds you can implement right now — including one that handles filtered table data correctly.

What Causes the Missing CSV Headers in Retool?

The bug is specific to tables that have Dynamic Columns turned on. When you use Retool's built-in export button on these tables, the resulting CSV (as well as TSV and Excel exports) generates a blank first row instead of a row containing column headers. Tables with standard, statically defined columns are not affected — those export with headers intact.

The root cause is that Retool's export logic doesn't correctly read the column definitions when they're derived dynamically at runtime rather than hardcoded in the table component's configuration. The data rows export fine, but the header row is empty, making the file nearly unusable without manual cleanup.

Why the "Export from Data Source" Workaround Falls Short

The first workaround suggested in the Retool community is to change the table's export behavior to pull directly from the underlying query or transformer that feeds the table, rather than from the table component itself. This works in simple cases, but it has a critical limitation: it ignores any filters the user has applied to the table.

If your users are filtering the table down to a specific subset of rows and then expect the CSV to reflect what they see on screen, exporting from the raw data source will give them everything — not the filtered view. For most internal tools, that's a deal-breaker.

The Best Fix: Use getDisplayedData() and utils.exportData()

The most reliable solution comes from the Retool community and uses two Retool APIs together: getDisplayedData() and utils.exportData(). This approach exports exactly what's visible in the table at the time of export — including any active filters, sorts, or pagination — and it includes the column headers correctly.

Here's how to implement it step by step:

  • Step 1: Create a new JavaScript query in your Retool app. Name it something like exportTableToCSV.
  • Step 2: Set the query type to Run JavaScript code.
  • Step 3: Paste the following code, replacing reportTable with the actual name of your table component:

let reportData = await reportTable.getDisplayedData()
utils.exportData(reportData, "reportOutput", "csv")
return reportData

  • Step 4: Add a Button component to your app and set its onClick event to trigger this query.
  • Step 5: Test it by applying a filter to your table, then clicking the export button. The downloaded CSV should reflect only the filtered rows and include a proper header row.

A key detail here: getDisplayedData() is an async function, which is why you must use await. Without it, the function returns a Promise object rather than the actual data, and your export will be empty or broken. Also note that utils.exportData() is the correct function to use here — not utils.downloadFile(). The exportData function handles the header row automatically by reading the keys from the data objects, which is exactly what's missing from the built-in dynamic column export.

Choosing the Right Export Format

The third argument to utils.exportData() controls the file format. Retool supports the following options:

  • "csv" — comma-separated values, the most universally compatible format
  • "tsv" — tab-separated values
  • "xlsx" — Excel format, useful if your users need spreadsheet-ready output

Swap out "csv" in the query for whichever format your team needs. All three formats will correctly include the headers when using this method.

Should You Wait for an Official Retool Fix?

Based on the community thread history, this bug was first reported and confirmed reproducible by Retool's own support team — and it remained open for over a year and a half with no shipped fix. Whether it gets addressed in a future release is uncertain. For any production internal tool where users rely on CSV exports, waiting is not a viable strategy.

The getDisplayedData() + utils.exportData() pattern is robust, handles filters correctly, and takes less than five minutes to implement. It's the approach we recommend to any team building data-heavy tools in Retool where table exports are part of the workflow.

Quick Reference: Full Working Code Snippet

Here's the complete JavaScript query to copy directly into your Retool app — just replace reportTable with your table's component name:

let reportData = await reportTable.getDisplayedData()
utils.exportData(reportData, "reportOutput", "csv")
return reportData

Wire this to a button, and your users get a properly formatted CSV with headers every time — regardless of whether dynamic columns are enabled or what filters are currently active on the table.

Ready to build?

We scope, design, and ship your Retool app — fast.

Ready to ship your first tool?