# How to Use Data Transformers in Retool

- Tool: Retool
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## 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.

## 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.

## Before you start

- 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.

```
// Transformer: processedUsers
// References: query1.data (auto-tracked as dependency)

const users = query1.data || [];

return users
  .filter(user => user.is_active)
  .map(user => ({
    id: user.id,
    name: user.first_name + ' ' + user.last_name,
    email: user.email,
    department: user.department ?? 'Unassigned',
    joinYear: new Date(user.created_at).getFullYear()
  }))
  .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.

```
// Table Inspector → General → Data source:
{{ processedUsers.value }}

// Text component showing count:
{{ processedUsers.value.length + ' active users' }}

// Chart component data:
{{ chartDataTransformer.value }}

// Another transformer referencing this one:
const processed = processedUsers.value;
return 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.

```
// Transformer: usersWithOrders
// Joins query1 (users) with query2 (order counts by user_id)

const users = query1.data || [];
const orderCounts = query2.data || []; // [{ user_id, order_count }]

// Build a lookup map for O(1) access
const orderMap = {};
orderCounts.forEach(item => {
  orderMap[item.user_id] = item.order_count;
});

return users.map(user => ({
  ...user,
  orderCount: orderMap[user.id] ?? 0,
  isHighValue: (orderMap[user.id] ?? 0) >= 10
}));
```

**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.

```
// Transformer: salesByMonth
// Prepares data for a Bar Chart component

const sales = query1.data || [];

// Group by month and sum revenue
const monthlyTotals = sales.reduce((acc, sale) => {
  const month = new Date(sale.sale_date).toLocaleDateString('en-US', {
    year: 'numeric', month: 'short'
  });
  acc[month] = (acc[month] || 0) + Number(sale.amount);
  return acc;
}, {});

// Convert to chart-ready array
return Object.entries(monthlyTotals)
  .map(([month, total]) => ({ month, total }))
  .sort((a, b) => new Date(a.month) - new Date(b.month));

// 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.

```
// Transformer: filteredByDepartment
// Re-runs when departmentSelect.value changes

const users = query1.data || [];
const selectedDept = departmentSelect.value;

if (!selectedDept || selectedDept === 'all') {
  return users;
}

return 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.

```
// WRONG — transformer trying to do side effects:
query2.trigger(); // Error: cannot trigger queries in transformer
await myState.setValue(result); // Error: cannot set state

// CORRECT — move side effects to a JS Query:
// In a JS Query named 'processAndSave':
const processed = await query1.trigger();
const result = processed.data.map(/* ... */);
await myState.setValue(result);
await 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 code example

File: `Transformer: dashboardStats`

```javascript
// Transformer: dashboardStats
// Computes summary statistics for a dashboard
// Dependencies: query1.data (users), query2.data (orders)
// Access as: {{ dashboardStats.value }}

const users = query1.data || [];
const orders = query2.data || [];

// User stats
const activeUsers = users.filter(u => u.is_active);
const newThisMonth = users.filter(u => {
  const created = new Date(u.created_at);
  const now = new Date();
  return created.getFullYear() === now.getFullYear() &&
         created.getMonth() === now.getMonth();
});

// Order stats
const totalRevenue = orders.reduce((sum, o) => sum + Number(o.amount || 0), 0);
const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;

// Department breakdown
const byDept = _.groupBy(users, 'department');
const deptBreakdown = Object.entries(byDept).map(([dept, members]) => ({
  department: dept || 'Unassigned',
  count: members.length,
  activeCount: members.filter(u => u.is_active).length
}));

return {
  totalUsers: users.length,
  activeUsers: activeUsers.length,
  newThisMonth: newThisMonth.length,
  totalRevenue: totalRevenue.toFixed(2),
  avgOrderValue: avgOrderValue.toFixed(2),
  deptBreakdown
};
```

## Common mistakes

- **Binding a component to {{ transformer1.data }} instead of {{ transformer1.value }}** — undefined Fix: Transformers expose their output as .value, not .data. Queries use .data; transformers use .value. Update the binding to {{ transformer1.value }}.
- **Transformer returns undefined because it has no return statement** — undefined Fix: 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.
- **Calling query.trigger() inside a transformer expecting it to re-fetch data** — undefined Fix: 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.
- **setValue() is called inside a transformer to update a temporary state variable** — undefined Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-use-data-transformers-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-use-data-transformers-in-retool
