# How to Use Event Handlers in Retool

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

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

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

## Before you start

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

```
// Handler configuration (set via Inspector dropdowns):
// Event: onClick
// Action: Trigger query
// Query: deleteRecord
// Only run when: {{ table1.selectedRow.data !== undefined }}

// Optional: if you need additional params, use a JS Query instead:
// Action: Run script
// Script:
await deleteRecord.trigger({
  additionalScope: { id: table1.selectedRow.data.id }
});
```

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

```
// Query: insertRecord
// Success handler:
//   Action: Trigger query → getRecords (refresh table)
//   Action: Control component → modal1.close()
//   Action: Show notification → 'Record created'

// Failure handler:
//   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.

```
// PROBLEM: Two onClick handlers run in parallel
// Handler 1: Trigger query → updateRecord
// Handler 2: Trigger query → getRecords
// Result: getRecords may return BEFORE updateRecord finishes!

// SOLUTION A: Use a single JS Query as the onClick handler
// Handler 1: Run script
await updateRecord.trigger();
await getRecords.trigger(); // Guaranteed to run AFTER update

// SOLUTION B: Chain via query success event
// updateRecord → Success handler: Trigger query → getRecords
// (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.

```
// Text Input: searchInput
// onChange handler: Trigger query → searchCustomers

// SQL Query: searchCustomers
// SELECT * FROM customers
// WHERE name ILIKE {{ '%' + searchInput.value + '%' }}
// Run when inputs change: enabled

// For debounced search (avoids query on every keystroke):
// onChange handler: Run script
clearTimeout(window._searchTimer);
window._searchTimer = setTimeout(async () => {
  await searchCustomers.trigger();
}, 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.

```
// Simple navigation handler:
// Event: onClick
// Action: Navigate to
// Page: CustomerDetail
// URL params: { id: {{ table1.selectedRow.data.id }} }

// Or use a JS Query for complex navigation with global variable:
await global.setValue('selectedCustomerId', table1.selectedRow.data.id);
await global.setValue('selectedCustomerName', table1.selectedRow.data.name);
utils.openPage('CustomerDetail');
```

**Expected result:** Clicking the button navigates to the target page, with the selected customer's ID available on the destination page.

## Complete code example

File: `JS Query: handleFormSubmitButton (replaces multiple parallel handlers)`

```javascript
// Single JS Query that replaces multiple parallel onClick handlers
// Triggered by a 'Save' button's onClick event handler
// Demonstrates: sequential execution, validation, chaining, notifications

const formData = {
  name: nameInput.value,
  email: emailInput.value,
  status: statusSelect.value,
  notes: notesInput.value
};

// === Validation ===
if (!formData.name || formData.name.trim() === '') {
  utils.showNotification({ title: 'Name is required', notificationType: 'warning' });
  return;
}

if (!formData.email || !formData.email.includes('@')) {
  utils.showNotification({ title: 'Valid email is required', notificationType: 'warning' });
  return;
}

// === Set loading state ===
await isSaving.setValue(true);

try {
  const isEditing = !!table1.selectedRow.data?.id;
  
  if (isEditing) {
    // Update existing record
    await updateCustomer.trigger({
      additionalScope: {
        id: table1.selectedRow.data.id,
        ...formData
      }
    });
    utils.showNotification({ title: 'Customer updated', notificationType: 'success' });
  } else {
    // Create new record
    await createCustomer.trigger({
      additionalScope: formData
    });
    utils.showNotification({ title: 'Customer created', notificationType: 'success' });
  }
  
  // Sequential: these run AFTER the mutation completes
  await getCustomers.trigger();  // Refresh table
  await modal1.close();          // Close form modal
  
} catch (err) {
  utils.showNotification({
    title: 'Save failed',
    description: err.message,
    notificationType: 'error'
  });
} finally {
  await isSaving.setValue(false);
}
```

## Common mistakes

- **Adding multiple onClick handlers expecting them to run sequentially** — undefined Fix: 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.
- **Setting 'Only run when' to a condition but the button still appears clickable and confuses users** — undefined Fix: '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.
- **Using onChange on a text input to trigger a database query on every keystroke** — undefined Fix: Implement debouncing with clearTimeout/setTimeout (300ms is typical) to delay the query until the user stops typing, reducing unnecessary database load.
- **Forgetting that event handlers on components run in parallel with query success handlers** — undefined Fix: 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.

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

---

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