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

How to Use Data Transformers in Retool

Retool standalone transformers live in the Code tab and auto-execute whenever their dependencies change. They must return a value, which is accessed as {{ transformer1.value }}. Transformers are read-only — they cannot call query.trigger() or setState(). Use them for pure data reshaping; use JS Queries for side effects.

What you'll learn

  • How to create a standalone transformer in Retool's Code tab
  • How transformers auto-execute when their dependencies (queries, components) change
  • How to reference transformer output as {{ transformer1.value }} anywhere in your app
  • Why transformers are read-only and what to do instead for side effects
  • Practical transformer patterns: joining queries, filtering, and reshaping for charts
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

Retool standalone transformers live in the Code tab and auto-execute whenever their dependencies change. They must return a value, which is accessed as {{ transformer1.value }}. Transformers are read-only — they cannot call query.trigger() or setState(). Use them for pure data reshaping; use JS Queries for side effects.

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

Standalone Transformers: Reactive Data Computation in Retool

A standalone Retool transformer is a JavaScript code block that lives in the Code tab (separate from any specific query). It runs automatically whenever any value it references changes — similar to a computed property in Vue or a useMemo hook in React. Its output is available throughout your app as {{ transformer1.value }}.

Unlike query-attached transformers (which process a single query's response), standalone transformers can reference multiple queries, components, and state variables. This makes them ideal for joining data from two queries, computing aggregated statistics for a chart, or preparing a filtered/sorted dataset that multiple components share.

The critical constraint: transformers are read-only. They cannot trigger queries, update state, or make async calls. They are pure computation — input data in, transformed data out.

Prerequisites

  • A Retool app with at least one query returning data
  • Familiarity with JavaScript array methods (map, filter, reduce)
  • Understanding of Retool's {{ }} expression syntax
  • Optional: knowledge of Lodash, which is bundled and available as _

Step-by-step guide

1

Create a Standalone Transformer in the Code Tab

In the Retool editor, click the '+' button in the bottom panel (or press Cmd+K and search 'Add transformer'). Select 'Transformer' from the options. A new transformer named 'transformer1' appears in the Code tab. Click the name to rename it to something descriptive like 'processedUsers' or 'chartData'. Transformers appear in the Code tab alongside JS Queries. They are visually distinct — transformers show a function icon and always have an explicit return value.

Expected result: A new named transformer appears in the Code tab, ready for editing.

2

Write the Transformer and Reference Dependencies

Inside the transformer, write JavaScript that references query data and component values using standard Retool references. When you reference query1.data, Retool automatically tracks this as a dependency — the transformer will re-run whenever query1 completes or its data changes. Always end with a return statement. A transformer with no return statement will output undefined as its value.

typescript
1// Transformer: processedUsers
2// References: query1.data (auto-tracked as dependency)
3
4const users = query1.data || [];
5
6return users
7 .filter(user => user.is_active)
8 .map(user => ({
9 id: user.id,
10 name: user.first_name + ' ' + user.last_name,
11 email: user.email,
12 department: user.department ?? 'Unassigned',
13 joinYear: new Date(user.created_at).getFullYear()
14 }))
15 .sort((a, b) => a.name.localeCompare(b.name));

Expected result: The transformer runs immediately when the app loads (after query1 resolves) and produces a filtered, sorted user array.

3

Access the Transformer Output with {{ transformer1.value }}

The transformer's output is available anywhere in the app as {{ transformer1.value }} (or {{ processedUsers.value }} if you renamed it). Bind a Table component to it by setting the Table's Data source to {{ processedUsers.value }}. Reference it in text components, charts, or other transformers. The .value suffix is always required for transformers — unlike queries which use .data.

typescript
1// Table Inspector → General → Data source:
2{{ processedUsers.value }}
3
4// Text component showing count:
5{{ processedUsers.value.length + ' active users' }}
6
7// Chart component data:
8{{ chartDataTransformer.value }}
9
10// Another transformer referencing this one:
11const processed = processedUsers.value;
12return processed.filter(u => u.department === departmentFilter.value);

Expected result: Components bound to {{ processedUsers.value }} display the transformer's output and update reactively.

4

Join Data from Multiple Queries

One of the most powerful uses of standalone transformers is joining data from two queries — something a query-attached transformer cannot do. Reference both queries in the transformer and merge them using JavaScript. Example: join users from query1 with their order counts from query2.

typescript
1// Transformer: usersWithOrders
2// Joins query1 (users) with query2 (order counts by user_id)
3
4const users = query1.data || [];
5const orderCounts = query2.data || []; // [{ user_id, order_count }]
6
7// Build a lookup map for O(1) access
8const orderMap = {};
9orderCounts.forEach(item => {
10 orderMap[item.user_id] = item.order_count;
11});
12
13return users.map(user => ({
14 ...user,
15 orderCount: orderMap[user.id] ?? 0,
16 isHighValue: (orderMap[user.id] ?? 0) >= 10
17}));

Expected result: The transformer produces a merged dataset combining user data with order counts from a separate query.

5

Compute Aggregated Statistics for Charts

Transformers are excellent for preparing aggregated data for chart components. Group, sum, or count data from a query and return it in the format your chart expects. Retool's Chart component expects data in a specific shape — transformers let you adapt your query response to that shape.

typescript
1// Transformer: salesByMonth
2// Prepares data for a Bar Chart component
3
4const sales = query1.data || [];
5
6// Group by month and sum revenue
7const monthlyTotals = sales.reduce((acc, sale) => {
8 const month = new Date(sale.sale_date).toLocaleDateString('en-US', {
9 year: 'numeric', month: 'short'
10 });
11 acc[month] = (acc[month] || 0) + Number(sale.amount);
12 return acc;
13}, {});
14
15// Convert to chart-ready array
16return Object.entries(monthlyTotals)
17 .map(([month, total]) => ({ month, total }))
18 .sort((a, b) => new Date(a.month) - new Date(b.month));
19
20// Chart series: x = month, y = total

Expected result: The transformer returns an array of { month, total } objects ready for a Bar Chart's data series.

6

Reference Component Values as Reactive Dependencies

Transformers can reference component values like textInput1.value or selectBox1.value. When those components change, the transformer automatically re-runs. This enables reactive client-side filtering without re-querying the database. This is a good alternative to server-side filtering when your dataset is small enough to load in full.

typescript
1// Transformer: filteredByDepartment
2// Re-runs when departmentSelect.value changes
3
4const users = query1.data || [];
5const selectedDept = departmentSelect.value;
6
7if (!selectedDept || selectedDept === 'all') {
8 return users;
9}
10
11return users.filter(user => user.department === selectedDept);

Expected result: The transformer reactively filters the user list whenever the department dropdown changes.

7

Understand What Transformers Cannot Do

Transformers are read-only and synchronous. They execute in a restricted environment where side effects are not allowed. Specifically: - Cannot call query.trigger() or any query method - Cannot call variable.setValue() or useState() - Cannot use await or Promises - Cannot make fetch() calls or use browser APIs If your logic needs to do any of these, convert it to a JS Query. JS Queries support async/await and can trigger other queries, but they do NOT auto-execute on dependency changes the way transformers do.

typescript
1// WRONG — transformer trying to do side effects:
2query2.trigger(); // Error: cannot trigger queries in transformer
3await myState.setValue(result); // Error: cannot set state
4
5// CORRECT — move side effects to a JS Query:
6// In a JS Query named 'processAndSave':
7const processed = await query1.trigger();
8const result = processed.data.map(/* ... */);
9await myState.setValue(result);
10await saveQuery.trigger({ additionalScope: { data: result } });

Expected result: You understand the boundary between transformers (pure computation) and JS Queries (side effects).

8

Inspect Transformer Output in the State Panel

To debug your transformer, open the State panel (left sidebar → State tab or Cmd+Shift+S). Find your transformer in the list — it shows the current value of transformer1.value. If the value is undefined, check: (1) your transformer has a return statement, (2) the referenced queries have run, and (3) there are no JavaScript errors in the transformer code. You can also click 'Preview' inside the transformer editor to run it immediately and see the output.

Expected result: You can verify transformer1.value in the State panel and identify any issues with the output shape.

Complete working example

Transformer: dashboardStats
1// Transformer: dashboardStats
2// Computes summary statistics for a dashboard
3// Dependencies: query1.data (users), query2.data (orders)
4// Access as: {{ dashboardStats.value }}
5
6const users = query1.data || [];
7const orders = query2.data || [];
8
9// User stats
10const activeUsers = users.filter(u => u.is_active);
11const newThisMonth = users.filter(u => {
12 const created = new Date(u.created_at);
13 const now = new Date();
14 return created.getFullYear() === now.getFullYear() &&
15 created.getMonth() === now.getMonth();
16});
17
18// Order stats
19const totalRevenue = orders.reduce((sum, o) => sum + Number(o.amount || 0), 0);
20const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;
21
22// Department breakdown
23const byDept = _.groupBy(users, 'department');
24const deptBreakdown = Object.entries(byDept).map(([dept, members]) => ({
25 department: dept || 'Unassigned',
26 count: members.length,
27 activeCount: members.filter(u => u.is_active).length
28}));
29
30return {
31 totalUsers: users.length,
32 activeUsers: activeUsers.length,
33 newThisMonth: newThisMonth.length,
34 totalRevenue: totalRevenue.toFixed(2),
35 avgOrderValue: avgOrderValue.toFixed(2),
36 deptBreakdown
37};

Common mistakes

Why it's a problem: Binding a component to {{ transformer1.data }} instead of {{ transformer1.value }}

How to avoid: Transformers expose their output as .value, not .data. Queries use .data; transformers use .value. Update the binding to {{ transformer1.value }}.

Why it's a problem: Transformer returns undefined because it has no return statement

How to avoid: Every path in the transformer must return a value. Add 'return data;' as the last line, or return an empty array '[]' as a fallback for error cases.

Why it's a problem: Calling query.trigger() inside a transformer expecting it to re-fetch data

How to avoid: Transformers are read-only and synchronous. They cannot trigger queries. If you need to trigger a query based on input changes, use a JS Query with an event handler on the triggering component.

Why it's a problem: setValue() is called inside a transformer to update a temporary state variable

How to avoid: setValue() is async and cannot be called from a transformer. Move state updates to a JS Query. Remember: even in JS Queries, setValue() is async — do not read the updated value immediately after calling it.

Best practices

  • Always include a return statement — a transformer without return produces undefined as its value, which silently breaks any component bound to it.
  • Add null guards at the start: 'const data = query1.data || [];' prevents crashes when the query hasn't run yet.
  • Name transformers after their output, not their input — 'activeUsersByDept' is clearer than 'processQuery1'.
  • Access transformer output with .value, not .data — mixing up .data (queries) and .value (transformers) is the most common mistake.
  • Keep transformers focused on one logical output; create multiple transformers if you need different shapes for different components.
  • Transformers are read-only — never try to call query.trigger() or setState() inside them. Move side effects to JS Queries.
  • Use the State panel to verify transformer1.value during development — it updates in real-time as your data and component values change.

Still stuck?

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

ChatGPT Prompt

I'm building a Retool dashboard. I have two SQL queries: query1 returns users (id, first_name, last_name, email, department, is_active, created_at) and query2 returns orders (id, user_id, amount, created_at). I want a standalone transformer called 'dashboardStats' that computes: total users, active users, new users this month, total revenue, average order value, and a department breakdown array. Write the transformer JavaScript using Retool's transformer syntax. The output should be a single object accessed as {{ dashboardStats.value }}.

Retool Prompt

Create a standalone transformer named 'usersWithOrders' in the Code tab. It should join query1.data (users with fields: id, name, email, department) with query2.data (order counts: user_id, order_count) to produce a merged array where each user object includes their orderCount and an isHighValue boolean (true if orderCount >= 10). Use proper null guards and return the joined array.

Frequently asked questions

Does a Retool transformer run automatically or do I need to trigger it?

Standalone transformers run automatically whenever any of their referenced dependencies change — including query data updates and component value changes. You do not need to trigger them manually. They also run once on app load after their dependencies have initial values.

Can I use async/await in a Retool transformer?

No. Transformers are synchronous. You cannot use await, Promises, or async functions inside a transformer. If you need async operations (like fetching additional data), use a JS Query instead. JS Queries support async/await and can be triggered programmatically.

What is the difference between transformer1.value and query1.data?

Queries expose their response as .data (e.g. query1.data), while transformers expose their return value as .value (e.g. transformer1.value). Using .data on a transformer or .value on a query will return undefined. This is one of the most common binding mistakes in Retool.

Can a transformer reference another transformer?

Yes. You can reference another transformer inside a transformer using its .value property, just like referencing a query. This allows you to chain transformations — for example, one transformer cleans the raw data and a second one aggregates it for a chart. Be careful not to create circular dependencies.

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.