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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Intermediate |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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.
1// Transformer: processedUsers2// References: query1.data (auto-tracked as dependency)34const users = query1.data || [];56return users7 .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.
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.
1// Table Inspector → General → Data source:2{{ processedUsers.value }}34// Text component showing count:5{{ processedUsers.value.length + ' active users' }}67// Chart component data:8{{ chartDataTransformer.value }}910// 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.
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.
1// Transformer: usersWithOrders2// Joins query1 (users) with query2 (order counts by user_id)34const users = query1.data || [];5const orderCounts = query2.data || []; // [{ user_id, order_count }]67// Build a lookup map for O(1) access8const orderMap = {};9orderCounts.forEach(item => {10 orderMap[item.user_id] = item.order_count;11});1213return users.map(user => ({14 ...user,15 orderCount: orderMap[user.id] ?? 0,16 isHighValue: (orderMap[user.id] ?? 0) >= 1017}));Expected result: The transformer produces a merged dataset combining user data with order counts from a separate query.
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.
1// Transformer: salesByMonth2// Prepares data for a Bar Chart component34const sales = query1.data || [];56// Group by month and sum revenue7const 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}, {});1415// Convert to chart-ready array16return Object.entries(monthlyTotals)17 .map(([month, total]) => ({ month, total }))18 .sort((a, b) => new Date(a.month) - new Date(b.month));1920// Chart series: x = month, y = totalExpected result: The transformer returns an array of { month, total } objects ready for a Bar Chart's data series.
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.
1// Transformer: filteredByDepartment2// Re-runs when departmentSelect.value changes34const users = query1.data || [];5const selectedDept = departmentSelect.value;67if (!selectedDept || selectedDept === 'all') {8 return users;9}1011return users.filter(user => user.department === selectedDept);Expected result: The transformer reactively filters the user list whenever the department dropdown changes.
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.
1// WRONG — transformer trying to do side effects:2query2.trigger(); // Error: cannot trigger queries in transformer3await myState.setValue(result); // Error: cannot set state45// 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).
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
1// Transformer: dashboardStats2// Computes summary statistics for a dashboard3// Dependencies: query1.data (users), query2.data (orders)4// Access as: {{ dashboardStats.value }}56const users = query1.data || [];7const orders = query2.data || [];89// User stats10const 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});1718// Order stats19const totalRevenue = orders.reduce((sum, o) => sum + Number(o.amount || 0), 0);20const avgOrderValue = orders.length > 0 ? totalRevenue / orders.length : 0;2122// Department breakdown23const 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).length28}));2930return {31 totalUsers: users.length,32 activeUsers: activeUsers.length,33 newThisMonth: newThisMonth.length,34 totalRevenue: totalRevenue.toFixed(2),35 avgOrderValue: avgOrderValue.toFixed(2),36 deptBreakdown37};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.
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 }}.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation