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

How to Use Table Transformers in Retool

Table transformers in Retool are JavaScript functions attached directly to a query. They run automatically when the query returns data, using the built-in 'data' variable, and their output becomes query1.data. Unlike standalone transformers, they are colocated with the query. Remember: transformers are read-only — they cannot call query.trigger() or setState().

What you'll learn

  • The difference between a query-attached transformer and a standalone transformer
  • How to enable and write a transformer on a SQL or REST query using the 'data' parameter
  • How to filter, reshape, and augment query results before they reach query1.data
  • How rawData differs from the transformed data in query1.data
  • Why transformers are read-only and cannot trigger queries or set state
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Table transformers in Retool are JavaScript functions attached directly to a query. They run automatically when the query returns data, using the built-in 'data' variable, and their output becomes query1.data. Unlike standalone transformers, they are colocated with the query. Remember: transformers are read-only — they cannot call query.trigger() or setState().

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

Query-Attached Transformers: Reshape Data at the Source

Retool offers two types of transformers. A query-attached transformer lives inside a specific query and runs immediately after that query returns data. Its output replaces what query1.data returns to the rest of the app. A standalone transformer is a separate resource in the Code tab that can reference multiple queries.

Table transformers (query-attached) are ideal when you want to normalize or reshape data from a single query — for example, flattening nested JSON from an API, mapping boolean fields to human-readable strings, or filtering out rows that should never reach the UI. Because they are colocated with the query, they are easier to maintain than separate transformers.

This tutorial covers enabling a transformer on a query, using the built-in 'data' variable, understanding rawData vs data, and writing common transformation patterns for table display.

Prerequisites

  • A Retool app with at least one SQL or REST query returning data
  • Basic JavaScript knowledge (array map, filter, reduce)
  • Understanding of Retool's query editor and Code tab
  • A Table component connected to the query you plan to transform

Step-by-step guide

1

Open the Query and Enable the Transformer

Click on your query in the bottom query panel to open the query editor. Look for the 'Transform' tab (or 'Transformer' toggle depending on your Retool version) next to the query code. Click it to reveal the transformer editor. You will see a default comment showing the 'data' variable is available. The transformer runs after the query executes and its return value becomes what query1.data exposes to the rest of the app.

Expected result: The transformer code editor opens, showing the 'data' variable is available.

2

Understand the 'data' Variable vs rawData

Inside a query-attached transformer, the variable 'data' contains the raw response from the query before any transformation. For SQL queries, this is an array of row objects. For REST API queries, it is the parsed JSON response body. After your transformer runs, query1.data contains the transformer's return value. The original untransformed response is available as query1.rawData — useful for debugging if your transformer changes the shape unexpectedly.

typescript
1// Inside the query transformer:
2// 'data' = the raw query response (before transformation)
3// After transformation, query1.data = what you return here
4// query1.rawData = always the original untransformed response
5
6// Simple passthrough (no transformation):
7return data;
8
9// Verify what raw data looks like:
10console.log('Raw response:', JSON.stringify(data[0]));
11return data;

Expected result: You understand that 'data' is the input and what you return becomes query1.data.

3

Transform a SQL Query Response

SQL queries return an array of row objects. Use array methods to reshape the data. Common operations include filtering rows, renaming fields, adding computed fields, or sorting. The transformer replaces query1.data with whatever you return. Example: rename snake_case fields to camelCase and add a computed display field.

typescript
1// SQL returns: [{ user_id, first_name, last_name, created_at, is_active }]
2// After transformer, query1.data will have camelCase + displayName
3
4return data.map(row => ({
5 id: row.user_id,
6 firstName: row.first_name,
7 lastName: row.last_name,
8 displayName: row.first_name + ' ' + row.last_name,
9 createdAt: row.created_at,
10 isActive: Boolean(row.is_active),
11 statusLabel: row.is_active ? 'Active' : 'Inactive'
12}));

Expected result: query1.data now returns camelCase objects with an added displayName and statusLabel field.

4

Transform a REST API Response

REST APIs often return nested structures that need flattening before a table can display them. For example, an API might return { users: [ { id, profile: { name, email }, ... } ] }. The transformer lets you extract and flatten this structure. Use data.users or data.data (common API envelope patterns) to access the actual array.

typescript
1// REST API returns: { users: [ { id, profile: { name, email }, role, created_at } ] }
2// Flatten for table display:
3
4const users = data.users || data.data || data;
5
6return users.map(user => ({
7 id: user.id,
8 name: user.profile?.name ?? 'Unknown',
9 email: user.profile?.email ?? '',
10 role: user.role,
11 createdAt: new Date(user.created_at).toLocaleDateString('en-US'),
12 isAdmin: user.role === 'admin'
13}));

Expected result: The nested API response is flattened into a simple array of display-ready objects.

5

Filter and Sort Inside the Transformer

You can filter out unwanted rows and sort the result directly in the transformer. This is useful for hiding internal records, sorting by a computed field, or deduplicating results. Note that transformer-based filtering is client-side only — all rows are fetched from the database first, then filtered in JavaScript. For large datasets, prefer SQL WHERE clauses for filtering.

typescript
1// Filter inactive users and sort by last name:
2return data
3 .filter(row => row.is_active !== false)
4 .sort((a, b) => (a.last_name ?? '').localeCompare(b.last_name ?? ''))
5 .map(row => ({
6 ...row,
7 displayName: row.first_name + ' ' + row.last_name
8 }));
9
10// Deduplicate by id:
11const seen = new Set();
12return data.filter(row => {
13 if (seen.has(row.id)) return false;
14 seen.add(row.id);
15 return true;
16});

Expected result: query1.data contains only active users, sorted by last name, with no duplicates.

6

Use Lodash in the Transformer

Retool includes Lodash as a built-in library accessible via the _ variable in transformers. Use lodash for grouping, sorting, deduplication, and complex data reshaping. No import statement is needed — _ is globally available.

typescript
1// Group users by department using lodash:
2const grouped = _.groupBy(data, 'department');
3
4// Return a flat list with department added as a label row:
5return Object.entries(grouped).flatMap(([dept, rows]) => [
6 { _isDeptHeader: true, department: dept, count: rows.length },
7 ...rows.map(row => ({ ...row, _isDeptHeader: false }))
8]);
9
10// Or simply pick specific fields:
11return data.map(row => _.pick(row, ['id', 'name', 'email', 'status']));

Expected result: The transformer produces a reshaped array using lodash utilities without needing to import any libraries.

7

Understand the Read-Only Constraint

Transformers are read-only — they run synchronously after query execution and can only return a value. They CANNOT: - Call query.trigger() to run another query - Call setState() or variable.setValue() - Make async API calls or use await - Access browser APIs like fetch() or localStorage If you need to trigger a side effect (like running a second query after transformation), use a JS Query instead of a transformer. JS Queries support async/await and can trigger other queries.

typescript
1// WRONG — this will silently fail:
2query2.trigger(); // transformers cannot trigger queries
3await myVar.setValue('test'); // transformers cannot set state
4
5// CORRECT — move side effects to a JS Query:
6// In a JS Query:
7const result = await query1.trigger();
8// query1.data is now the transformed result
9await query2.trigger({ additionalScope: { inputData: result.data } });

Expected result: You understand that transformers are for data shaping only, and side effects belong in JS Queries.

8

Bind the Transformed Data to Your Table

Once your transformer is set up, the Table component automatically uses query1.data (which now contains the transformed data). Set the Table's Data source in the Inspector to {{ query1.data }}. The table columns are auto-generated from the keys of the first object in the returned array. Transformed field names (like displayName) will appear as column headers.

typescript
1// Table Inspector → General → Data source:
2{{ query1.data }}
3
4// The column headers will reflect the keys returned by your transformer,
5// not the original SQL column names.

Expected result: The table displays columns matching your transformer's output field names.

Complete working example

Query transformer: getUsers (Transform tab)
1// Query: getUsers (SQL)
2// SELECT id, first_name, last_name, email, role, is_active,
3// created_at, department, score
4// FROM users
5// ORDER BY created_at DESC
6// LIMIT 1000;
7
8// Transform tab code:
9// 'data' = raw SQL response array
10
11return data
12 // Filter out system/internal accounts
13 .filter(row => !row.email.endsWith('@internal.company.com'))
14 // Reshape and augment each row
15 .map(row => ({
16 id: row.id,
17 displayName: `${row.first_name} ${row.last_name}`,
18 email: row.email,
19 role: row.role,
20 department: row.department ?? 'Unassigned',
21 isActive: Boolean(row.is_active),
22 statusLabel: row.is_active ? 'Active' : 'Inactive',
23 performanceTier:
24 row.score >= 90 ? 'Excellent' :
25 row.score >= 70 ? 'Good' :
26 row.score >= 50 ? 'Average' : 'Needs Improvement',
27 joinedDate: new Date(row.created_at).toLocaleDateString('en-US', {
28 year: 'numeric', month: 'short', day: 'numeric'
29 })
30 }))
31 // Sort by display name
32 .sort((a, b) => a.displayName.localeCompare(b.displayName));

Common mistakes

Why it's a problem: Returning nothing (undefined) from the transformer, making query1.data empty

How to avoid: Every transformer must explicitly return a value. The final line must be 'return data' or 'return transformedArray'. If there is no return statement, query1.data will be undefined.

Why it's a problem: Trying to call query.trigger() inside a transformer

How to avoid: Transformers are read-only and synchronous — they cannot trigger queries or set state. Move any side effects to a JS Query that runs after query1 completes using query1's success event handler.

Why it's a problem: Accessing a nested field without null checking and getting a transformer crash

How to avoid: Use optional chaining: row.profile?.name ?? 'Unknown'. A single null value in the API response will crash the entire transformer without proper null guards.

Why it's a problem: Expecting rawData and data to always be the same

How to avoid: query1.rawData is the original API response; query1.data is the transformer's output. They are the same only if your transformer returns data unchanged. Inspect both in the State panel to debug unexpected shapes.

Best practices

  • Always use optional chaining (?.) and nullish coalescing (??) when accessing nested fields in the transformer to prevent errors when API data is inconsistent.
  • Keep transformers focused on one query's data — if you need to join data from two queries, use a standalone transformer or a JS Query instead.
  • Return early with 'return data;' while debugging, then add transformation logic incrementally, checking query1.data in the State panel after each change.
  • Avoid heavy computation inside transformers — they run synchronously and can block the UI. For compute-intensive operations, use a JS Query with async processing.
  • Transformers are read-only: never try to call query.trigger() or setState() inside them. Move side effects to JS Queries.
  • Use query1.rawData for debugging — it always contains the untransformed response even after the transformer changes query1.data.
  • For REST API responses wrapped in envelopes (e.g. { data: [...] }), always unwrap at the start: const rows = data.data || data.results || data;

Still stuck?

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

ChatGPT Prompt

I have a Retool SQL query that returns user rows with snake_case fields: user_id, first_name, last_name, email, is_active, created_at, department, and score. I want to use the query's Transform tab to: (1) rename fields to camelCase, (2) add a displayName combining first and last name, (3) add a statusLabel field ('Active'/'Inactive' based on is_active), (4) add a performanceTier field based on score ranges (90+ Excellent, 70+ Good, 50+ Average, else Needs Improvement), and (5) filter out any rows where email ends with '@internal.company.com'. Write the transformer JavaScript.

Retool Prompt

In my Retool query getUsers, open the Transform tab and write a transformer that: maps snake_case SQL fields to camelCase, adds a displayName field (first_name + ' ' + last_name), adds a statusLabel field ('Active' if is_active else 'Inactive'), and sorts results by last name alphabetically. The transformer must use the 'data' variable and return the transformed array. Do not use query.trigger() inside the transformer.

Frequently asked questions

Where is the Transform tab in Retool's query editor?

The Transform tab appears as a secondary tab next to your query code (SQL or REST settings). Click on your query in the bottom panel, then look for 'Transform' alongside 'General' or 'Advanced'. If you do not see it, try saving the query first. The transformer is enabled only when you add code to it.

Can a table transformer reference other queries or components?

No. A query-attached transformer only has access to the 'data' variable (the query's own raw response) and built-in globals like Lodash (_) and moment. It cannot reference other queries like query2.data or components like textInput1.value. For cross-query data joining, use a standalone transformer in the Code tab.

Does the transformer run every time the query runs?

Yes. The query-attached transformer runs automatically every time the query executes and returns new data. You do not need to manually trigger the transformer — it is part of the query execution pipeline.

What happens if my transformer throws an error?

If the transformer throws a JavaScript error, query1.data will be undefined or empty and an error will appear in the query's result panel. The error is shown in the query UI and in the browser DevTools console. Check the State panel to see if query1.rawData still has data — this confirms whether the query itself succeeded and only the transformer failed.

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.