Tutorials
Reusable JavaScript Functions in Retool: A Practical Guide
If you've ever wanted to write reusable JavaScript functions in Retool — logic you can call from multiple transformers, queries, or component fields without duplicating code — you've probably hit the same wall everyone else does. Retool's transformers are powerful, but they hardcode their inputs to a specific query or component, which means you can't easily reuse the same transformation logic across different tables or forms. This guide covers every viable pattern, from the classic window object workaround to the modern app-level JavaScript feature introduced in Retool v2.66.
Why Retool Doesn't Have First-Class JS Functions (Yet)
Retool's transformers are documented as "JS snippets" — they run JavaScript but they're tightly coupled to whatever data source you point them at. You can't pass arguments into a transformer, and returning a function from one doesn't work. That means if you want the same timestamp-formatting logic to apply to five different tables, you're either copy-pasting five transformers or finding a smarter pattern. The good news: there are three solid options, each with real trade-offs.
Option 1: App-Level JavaScript (Best for Most Cases)
Starting with Retool v2.66, you can add JavaScript directly to an app via the app settings panel. This is the cleanest solution for logic that belongs to a single app and needs to be versioned alongside it. Navigate to your app, open the settings, and add your function definitions there. They'll be scoped to the app, included in version history, and available everywhere inside it.
This is the approach the Retool community was originally asking for, and it's now the recommended starting point. If you're on a recent version of Retool, check the App Settings → Custom JavaScript section and define your shared functions there.
Option 2: The window Object Workaround (For Older Versions)
Before app-level JS existed, the most reliable workaround was attaching functions to the global window object from inside a component field that always renders early — like the "Disable this container?" field of a top-level Tabbed Container. Here's the pattern:
{{ (function () {
window.updateTimestamps = function (recordUpdates) {
recordUpdates.forEach(record => {
record.updated_at = new Date().toISOString()
})
return recordUpdates
}
window.someOtherFunction = function (param1, param2) {
// your logic here
}
return false // never actually disable the container
})() }}
Once defined, you can call window.updateTimestamps(table_1.recordUpdates) from any query input field or component expression in the app. A few things to keep in mind when using this pattern:
- Render order matters. Place this snippet in a component that is guaranteed to render before anything that calls the function. A top-level container is safest.
- Ignore the red underline. Retool will flag calls to
window.updateTimestampsas undefined — this is a known linting bug, not a runtime error. The function will work fine. - Versioning is included. Because this lives inside the app's component tree, it gets versioned with everything else — unlike the global Preloaded JavaScript setting.
Option 3: JS Queries with additionalScope (Best for Parameterized Logic)
If your reusable logic needs to run as part of a triggered workflow — not just inline in an expression — use a JS query combined with additionalScope. This lets you pass dynamic inputs into a query at trigger time and get a return value back via onSuccess.
In your JS query (e.g., updateTimestampsQuery), write:
return recordUpdates.map(record => ({
...record,
updated_at: new Date().toISOString()
}))
Then trigger it from another query or event handler like this:
updateTimestampsQuery.trigger({
additionalScope: {
recordUpdates: table_1.recordUpdates
},
onSuccess: function(data) {
// data is the return value of the query
console.log(data)
}
})
This pattern is clean, fully versioned, and works well when you need to chain logic across multiple queries. The downside is that it's asynchronous, so you can't use it inline inside a component expression the way you can with the window approach.
What to Avoid: Global Preloaded JavaScript
Retool does offer a Preloaded JavaScript field under Settings → Code at the organization level. You can define window.updateTimestamps there and it'll be available across all apps. But this approach has serious drawbacks:
- It is not versioned — changing it affects every app instantly, with no rollback.
- It is global — a naming collision or bug can break unrelated apps.
- You can never safely rename a function without auditing every app that uses it.
Reserve this for truly universal utilities (like a shared date formatting library) that you're confident will never change shape.
Which Pattern Should You Use?
- On Retool v2.66+? Use app-level JavaScript in App Settings. It's the intended solution.
- Need inline expressions with arguments? Use the
windowworkaround in a top-level container field. - Need to chain async logic or trigger from events? Use a JS query with
additionalScope. - Cross-app utility that never changes? Preloaded JS at the org level — but use it sparingly.
The core problem — that Retool doesn't natively support parameterized, reusable JavaScript functions as first-class app objects — has largely been solved by the app-level JS feature. But if you're maintaining older apps or need inline expression support today, the window workaround is battle-tested and safe to ship.
Ready to build?
We scope, design, and ship your Retool app — fast.