Event handlers in Retool wire user actions (button click, row select, input change) to app behaviors (run query, navigate page, update state). Configure them in the Inspector panel under the Interaction section. Multiple handlers on the same event run in parallel — for sequential execution, use a JS Query with await or chain via query success handlers.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Wiring UI Events to App Actions in Retool
Event handlers are the glue between user interactions and app logic in Retool. Every component exposes a set of events — a Button has onClick, a Table has onRowClick and onRowSelectionChange, a Text Input has onChange and onSubmit — and each event can have one or more handlers attached.
Each handler specifies an action: run a query, set a state variable, open a modal, navigate to a page, copy to clipboard, or run a custom expression. You configure handlers in the Inspector panel under the Interaction section by clicking the + button next to each event.
A critical gotcha: multiple handlers on the same event run in parallel, not in sequence. If you need sequential execution (e.g., validate → update → refresh → notify), either use a single JS Query that chains the steps with await, or chain handlers via query success events.
Prerequisites
- A Retool account (Cloud or Self-hosted)
- A Retool app with at least a Button or Table component
- Basic understanding of Retool queries (SQL or JS Queries)
- Familiarity with the Inspector panel sections
Step-by-step guide
Add an event handler to a Button component
Select a Button component on the canvas. In the Inspector panel on the right, scroll to the Interaction section. You will see an 'Event handlers' area with a + button. Click + to add a new handler. In the handler configuration, choose the Event (onClick for buttons), then choose the Action. The most common actions are: Trigger query, Control component (open/close modal), Set variable, Navigate to page, and Show notification.
Expected result: A new event handler appears under the Interaction section. The handler is configured with an event type and an action.
Configure a handler to trigger a query
In the event handler configuration, set Event to onClick and Action to Trigger query. Select the query name from the dropdown. Optionally set 'Only run when' to a {{ }} expression that must evaluate to true for the handler to fire (e.g., {{ table1.selectedRow.data !== undefined }}). You can also set 'Show success toast' and 'Show failure toast' options — these display automatic notifications without any JS code.
1// Handler configuration (set via Inspector dropdowns):2// Event: onClick3// Action: Trigger query4// Query: deleteRecord5// Only run when: {{ table1.selectedRow.data !== undefined }}67// Optional: if you need additional params, use a JS Query instead:8// Action: Run script9// Script:10await deleteRecord.trigger({11 additionalScope: { id: table1.selectedRow.data.id }12});Expected result: Clicking the button triggers the specified query. If 'Only run when' is set, the query only runs when the condition is true.
Add handlers to query success and failure events
Queries themselves have event handlers for their success and failure states. Select a query in the Code panel. In the query's Settings, you will find Success event handlers and Failure event handlers. These fire after the query completes or fails. Use Success handlers to chain follow-up actions (e.g., refresh a table after an insert). Use Failure handlers to show error notifications or reset state.
1// Query: insertRecord2// Success handler:3// Action: Trigger query → getRecords (refresh table)4// Action: Control component → modal1.close()5// Action: Show notification → 'Record created'67// Failure handler:8// Action: Show notification → 'Insert failed: {{ insertRecord.error.message }}'Expected result: After insertRecord succeeds, getRecords automatically re-runs and the modal closes. After failure, the user sees the error message.
Understand the parallel execution gotcha
When you add multiple event handlers to the same event (e.g., two handlers on a Button's onClick), Retool fires them simultaneously — they run in parallel, not sequentially. This means if handler 1 runs updateRecord and handler 2 runs getRecords, getRecords may complete before updateRecord, returning stale data. To avoid this, either consolidate all logic into a single JS Query that uses await for sequential execution, or chain actions via query success handlers.
1// PROBLEM: Two onClick handlers run in parallel2// Handler 1: Trigger query → updateRecord3// Handler 2: Trigger query → getRecords4// Result: getRecords may return BEFORE updateRecord finishes!56// SOLUTION A: Use a single JS Query as the onClick handler7// Handler 1: Run script8await updateRecord.trigger();9await getRecords.trigger(); // Guaranteed to run AFTER update1011// SOLUTION B: Chain via query success event12// updateRecord → Success handler: Trigger query → getRecords13// (getRecords runs only AFTER updateRecord succeeds)Expected result: When using a JS Query or chaining via success events, the second operation reliably runs after the first completes.
Use onChange handlers for real-time filtering
Text Input and Select components expose an onChange event that fires every time the value changes. This is useful for real-time search and filter patterns. In the Inspector Interaction section, add an onChange handler that triggers a search query. Be aware that onChange fires on every keystroke for text inputs — if your query is expensive, consider using a debounce pattern via a JS Query instead.
1// Text Input: searchInput2// onChange handler: Trigger query → searchCustomers34// SQL Query: searchCustomers5// SELECT * FROM customers6// WHERE name ILIKE {{ '%' + searchInput.value + '%' }}7// Run when inputs change: enabled89// For debounced search (avoids query on every keystroke):10// onChange handler: Run script11clearTimeout(window._searchTimer);12window._searchTimer = setTimeout(async () => {13 await searchCustomers.trigger();14}, 300); // 300ms debounceExpected result: The search query runs as the user types, filtering the displayed results in real time.
Navigate between pages using event handlers
In multipage apps, event handlers can navigate to other pages. Add an onClick handler and set Action to Navigate to. Choose the target page from the dropdown. You can pass URL parameters to the destination page by configuring query params. To pass complex data between pages, use global variables (global.setValue()) in a JS Query instead.
1// Simple navigation handler:2// Event: onClick3// Action: Navigate to4// Page: CustomerDetail5// URL params: { id: {{ table1.selectedRow.data.id }} }67// Or use a JS Query for complex navigation with global variable:8await global.setValue('selectedCustomerId', table1.selectedRow.data.id);9await global.setValue('selectedCustomerName', table1.selectedRow.data.name);10utils.openPage('CustomerDetail');Expected result: Clicking the button navigates to the target page, with the selected customer's ID available on the destination page.
Complete working example
1// Single JS Query that replaces multiple parallel onClick handlers2// Triggered by a 'Save' button's onClick event handler3// Demonstrates: sequential execution, validation, chaining, notifications45const formData = {6 name: nameInput.value,7 email: emailInput.value,8 status: statusSelect.value,9 notes: notesInput.value10};1112// === Validation ===13if (!formData.name || formData.name.trim() === '') {14 utils.showNotification({ title: 'Name is required', notificationType: 'warning' });15 return;16}1718if (!formData.email || !formData.email.includes('@')) {19 utils.showNotification({ title: 'Valid email is required', notificationType: 'warning' });20 return;21}2223// === Set loading state ===24await isSaving.setValue(true);2526try {27 const isEditing = !!table1.selectedRow.data?.id;28 29 if (isEditing) {30 // Update existing record31 await updateCustomer.trigger({32 additionalScope: {33 id: table1.selectedRow.data.id,34 ...formData35 }36 });37 utils.showNotification({ title: 'Customer updated', notificationType: 'success' });38 } else {39 // Create new record40 await createCustomer.trigger({41 additionalScope: formData42 });43 utils.showNotification({ title: 'Customer created', notificationType: 'success' });44 }45 46 // Sequential: these run AFTER the mutation completes47 await getCustomers.trigger(); // Refresh table48 await modal1.close(); // Close form modal49 50} catch (err) {51 utils.showNotification({52 title: 'Save failed',53 description: err.message,54 notificationType: 'error'55 });56} finally {57 await isSaving.setValue(false);58}Common mistakes
Why it's a problem: Adding multiple onClick handlers expecting them to run sequentially
How to avoid: Multiple handlers on the same event run in parallel. Consolidate sequential logic into a single JS Query with await, or chain via query success event handlers.
Why it's a problem: Setting 'Only run when' to a condition but the button still appears clickable and confuses users
How to avoid: 'Only run when' silently prevents the action without disabling the button visually. Also set the button's Disabled property to the same condition so users understand it is inactive.
Why it's a problem: Using onChange on a text input to trigger a database query on every keystroke
How to avoid: Implement debouncing with clearTimeout/setTimeout (300ms is typical) to delay the query until the user stops typing, reducing unnecessary database load.
Why it's a problem: Forgetting that event handlers on components run in parallel with query success handlers
How to avoid: If a component's onClick triggers a query AND that query also has a success handler, both run after the query finishes. This is usually fine but can cause double-refresh issues. Check the full event chain in the Inspector.
Best practices
- Consolidate sequential actions into a single JS Query rather than using multiple parallel event handlers — multiple handlers on the same event run simultaneously, not in order.
- Use 'Only run when' conditions on destructive actions (delete, archive) as a safety gate separate from button disabling.
- Chain follow-up actions (refresh, close modal, notify) via query success event handlers to guarantee they run after the query completes.
- Use query failure event handlers to show meaningful error messages with {{ queryName.error.message }} rather than generic 'Something went wrong' messages.
- For text input onChange handlers that trigger expensive queries, implement a debounce using setTimeout to avoid firing on every keystroke.
- Keep event handler logic minimal — prefer triggering a named JS Query over embedding multi-line script directly in the handler.
- Document non-obvious 'Only run when' conditions with a comment-only text component explaining what the condition checks.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Retool Button component that should: (1) validate that a table row is selected, (2) run an update SQL query with the selected row's ID, (3) refresh the table after the update, (4) close a modal, and (5) show a success notification. I've been adding multiple onClick handlers but they run simultaneously and cause stale data. How do I use a single JS Query to make these run sequentially with proper async/await?
In my Retool app, configure the following event handler setup: (1) Button 'Save' onClick handler that triggers a JS Query named handleSave, (2) the handleSave JS Query should validate textInput1.value is not empty before proceeding, (3) use await updateRecord.trigger() followed by await getRecords.trigger(), and (4) add a Success event handler on updateRecord to close modal1. Show me the JS Query code and the Inspector settings.
Frequently asked questions
Do multiple event handlers on the same button click run in order or simultaneously?
Simultaneously. Retool fires all event handlers on a given event at the same time — they run in parallel, not in sequence. If your handlers have order dependencies (e.g., update then refresh), consolidate them into a single JS Query that chains calls with await, or wire them sequentially via query success event handlers.
Can I add an event handler to a query result (when data loads)?
Yes. Queries have Success and Failure event handlers configured in the query's Settings tab. The Success handler fires each time the query completes successfully — this is a good place to trigger follow-up queries or close modals. The Failure handler fires when the query errors, ideal for showing error notifications.
What is the 'Only run when' field in a Retool event handler?
It is a {{ }} JavaScript expression that must evaluate to true for the event handler's action to execute. If it evaluates to false or undefined, the handler silently does nothing. Use it to gate actions on preconditions like row selection, form validity, or user group membership. Note: it does not visually disable the component — set the Disabled property separately for visual feedback.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation