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

How to Use Event Handlers in Retool

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.

What you'll learn

  • How to add event handlers in the Inspector panel's Interaction section
  • The difference between component events (onClick, onChange) and query events (onSuccess, onFailure)
  • How multiple event handlers on the same event run in parallel, not sequentially
  • How to chain queries sequentially using query success/failure event handlers
  • How to use 'Only run when' conditions to gate event handler actions
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 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

1

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.

2

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.

typescript
1// Handler configuration (set via Inspector dropdowns):
2// Event: onClick
3// Action: Trigger query
4// Query: deleteRecord
5// Only run when: {{ table1.selectedRow.data !== undefined }}
6
7// Optional: if you need additional params, use a JS Query instead:
8// Action: Run script
9// 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.

3

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.

typescript
1// Query: insertRecord
2// Success handler:
3// Action: Trigger query → getRecords (refresh table)
4// Action: Control component → modal1.close()
5// Action: Show notification → 'Record created'
6
7// 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.

4

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.

typescript
1// PROBLEM: Two onClick handlers run in parallel
2// Handler 1: Trigger query → updateRecord
3// Handler 2: Trigger query → getRecords
4// Result: getRecords may return BEFORE updateRecord finishes!
5
6// SOLUTION A: Use a single JS Query as the onClick handler
7// Handler 1: Run script
8await updateRecord.trigger();
9await getRecords.trigger(); // Guaranteed to run AFTER update
10
11// SOLUTION B: Chain via query success event
12// updateRecord → Success handler: Trigger query → getRecords
13// (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.

5

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.

typescript
1// Text Input: searchInput
2// onChange handler: Trigger query → searchCustomers
3
4// SQL Query: searchCustomers
5// SELECT * FROM customers
6// WHERE name ILIKE {{ '%' + searchInput.value + '%' }}
7// Run when inputs change: enabled
8
9// For debounced search (avoids query on every keystroke):
10// onChange handler: Run script
11clearTimeout(window._searchTimer);
12window._searchTimer = setTimeout(async () => {
13 await searchCustomers.trigger();
14}, 300); // 300ms debounce

Expected result: The search query runs as the user types, filtering the displayed results in real time.

6

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.

typescript
1// Simple navigation handler:
2// Event: onClick
3// Action: Navigate to
4// Page: CustomerDetail
5// URL params: { id: {{ table1.selectedRow.data.id }} }
6
7// 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

JS Query: handleFormSubmitButton (replaces multiple parallel handlers)
1// Single JS Query that replaces multiple parallel onClick handlers
2// Triggered by a 'Save' button's onClick event handler
3// Demonstrates: sequential execution, validation, chaining, notifications
4
5const formData = {
6 name: nameInput.value,
7 email: emailInput.value,
8 status: statusSelect.value,
9 notes: notesInput.value
10};
11
12// === Validation ===
13if (!formData.name || formData.name.trim() === '') {
14 utils.showNotification({ title: 'Name is required', notificationType: 'warning' });
15 return;
16}
17
18if (!formData.email || !formData.email.includes('@')) {
19 utils.showNotification({ title: 'Valid email is required', notificationType: 'warning' });
20 return;
21}
22
23// === Set loading state ===
24await isSaving.setValue(true);
25
26try {
27 const isEditing = !!table1.selectedRow.data?.id;
28
29 if (isEditing) {
30 // Update existing record
31 await updateCustomer.trigger({
32 additionalScope: {
33 id: table1.selectedRow.data.id,
34 ...formData
35 }
36 });
37 utils.showNotification({ title: 'Customer updated', notificationType: 'success' });
38 } else {
39 // Create new record
40 await createCustomer.trigger({
41 additionalScope: formData
42 });
43 utils.showNotification({ title: 'Customer created', notificationType: 'success' });
44 }
45
46 // Sequential: these run AFTER the mutation completes
47 await getCustomers.trigger(); // Refresh table
48 await modal1.close(); // Close form modal
49
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.

ChatGPT Prompt

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?

Retool Prompt

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.

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.