# How to Create Notifications in Retool

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

## TL;DR

Create toast notifications in Retool using utils.showNotification({ title: 'Done', description: 'Record saved', notificationType: 'success', duration: 3 }). Call this function in JS Queries, or add it as a 'Show notification' action in event handlers on any component or query success/failure callbacks.

## In-App Toast Notifications with utils.showNotification()

Retool's utils.showNotification() displays non-blocking toast notifications in the bottom-right corner of your app. These are the primary way to give users feedback after an action — confirming a save was successful, warning about a data issue, or reporting an error.

Notifications can be triggered from three places: directly in a JS Query, via a 'Show notification' action in an event handler, or from a query's built-in success/failure event handlers. This tutorial covers all three approaches and shows how to customize notification content dynamically.

## Before you start

- A Retool app with at least one query or button component
- Basic familiarity with Retool's event handler system in the Inspector
- Understanding of Retool JS Queries (for dynamic notifications)

## Step-by-step guide

### 1. Call utils.showNotification() in a JS Query

The most flexible way to show a notification is in a JS Query. This lets you use dynamic data in the notification content and show notifications conditionally based on query results.

utils.showNotification() accepts an object with title (required), description (optional), notificationType (success | error | warning | info), and duration (seconds, default 3).

```
// Basic notification types:

// Success (green):
utils.showNotification({
  title: 'Record saved',
  description: 'User profile updated successfully.',
  notificationType: 'success',
  duration: 3
});

// Error (red):
utils.showNotification({
  title: 'Save failed',
  description: 'Please check your input and try again.',
  notificationType: 'error',
  duration: 5
});

// Warning (yellow):
utils.showNotification({
  title: 'Low inventory',
  description: 'Stock level below minimum threshold.',
  notificationType: 'warning',
  duration: 4
});

// Info (blue):
utils.showNotification({
  title: 'Processing',
  description: 'Your export is being prepared.',
  notificationType: 'info',
  duration: 2
});
```

**Expected result:** Toast notifications appear in the bottom-right corner of the app with the appropriate color and icon.

### 2. Add a 'Show Notification' Action to an Event Handler

For simple notifications that don't require dynamic content, use the 'Show notification' action directly in a component's event handler without writing a JS Query. This is available for button clicks, form submissions, and other component events.

In the Inspector's Interaction section, click '+ Add event handler', select the event (e.g. 'Click'), and choose 'Show notification' as the action. Configure the title, description, type, and duration in the action fields.

**Expected result:** Clicking the button shows the configured notification without needing a JS Query.

### 3. Trigger Notifications on Query Success and Failure

Every Retool query has built-in event handlers for success and failure. Open your query, go to its 'Event handlers' section (below the query code), and add handlers for 'Success' and 'Failure' events. Choose 'Show notification' as the action for each.

The success handler runs when the query completes without error; the failure handler runs when the query throws an error. This is the recommended pattern for data mutation queries (INSERT, UPDATE, DELETE).

```
// Query event handlers in the query editor:
// Success → Show notification:
//   Title: 'Saved successfully'
//   Description: {{ updateUser.data.rowCount + ' record(s) updated' }}
//   Type: success
//   Duration: 3

// Failure → Show notification:
//   Title: 'Error saving'
//   Description: {{ updateUser.error.message || 'An unexpected error occurred' }}
//   Type: error
//   Duration: 5

// OR equivalently in a JS Query wrapper:
try {
  await updateUser.trigger();
  utils.showNotification({
    title: 'Saved',
    description: updateUser.data.rowCount + ' record(s) updated.',
    notificationType: 'success',
    duration: 3
  });
} catch (err) {
  utils.showNotification({
    title: 'Save failed',
    description: err.message || 'Unknown error',
    notificationType: 'error',
    duration: 5
  });
}
```

**Expected result:** Success and error notifications appear automatically whenever the query runs, without additional button wiring.

### 4. Show Dynamic Notification Content with Query Data

Include query results or component values in notification messages by building the description string in a JS Query. This is useful for confirming what was created/updated or how many records were affected.

Access query results after a trigger with queryName.data.

```
// After inserting a record, show what was created:
await createOrderQuery.trigger();

const newOrder = createOrderQuery.data?.[0];
utils.showNotification({
  title: 'Order created',
  description: newOrder
    ? `Order #${newOrder.order_number} for $${Number(newOrder.total).toFixed(2)} is confirmed.`
    : 'Order created successfully.',
  notificationType: 'success',
  duration: 4
});

// Show affected row count from UPDATE:
await updateStatusQuery.trigger();
const rowsAffected = updateStatusQuery.data?.rowCount || 0;
utils.showNotification({
  title: rowsAffected > 0 ? 'Status updated' : 'Nothing to update',
  description: rowsAffected + ' record(s) updated.',
  notificationType: rowsAffected > 0 ? 'success' : 'warning',
  duration: 3
});
```

**Expected result:** Notifications include specific details from the query result like order numbers, record counts, or affected items.

### 5. Show a Progress Notification for Long Operations

For operations that take several seconds (like large exports or bulk inserts), show an 'info' notification with a long duration before starting the operation. Then show a success/error notification when it completes. This keeps users informed while waiting.

For very long operations, use the first notification as a 'started' signal and the final notification as a 'done' signal.

```
// JS Query: bulkExportWithNotifications

// Show 'in progress' notification first:
utils.showNotification({
  title: 'Export started',
  description: 'Fetching all records... this may take a moment.',
  notificationType: 'info',
  duration: 30  // Long enough to cover the operation
});

try {
  // Run the long operation:
  const result = await getAllRecordsForExport.trigger();
  const rowCount = result.data?.length || 0;

  // Show completion:
  utils.showNotification({
    title: 'Export ready',
    description: rowCount + ' rows exported successfully.',
    notificationType: 'success',
    duration: 4
  });

  // Trigger the download...
} catch (err) {
  utils.showNotification({
    title: 'Export failed',
    description: err.message,
    notificationType: 'error',
    duration: 5
  });
}
```

**Expected result:** Users see a 'started' notification immediately and a 'done' or 'error' notification when the operation completes.

### 6. Conditionally Show Different Notifications Based on Data

Use if/else logic in a JS Query to show different notification types based on the outcome of an operation. This is useful for operations that can partially succeed (e.g. some rows inserted, some skipped) or when you want different messages based on the data state.

```
// After a data import, show contextual notification:
await importCSVData.trigger();

const result = importCSVData.data;
const inserted = result?.inserted || 0;
const skipped = result?.skipped || 0;
const errors = result?.errors || [];

if (errors.length > 0) {
  utils.showNotification({
    title: 'Import completed with errors',
    description: `${inserted} rows imported, ${errors.length} rows failed. Check logs.`,
    notificationType: 'warning',
    duration: 6
  });
} else if (skipped > 0) {
  utils.showNotification({
    title: 'Import complete',
    description: `${inserted} new rows added. ${skipped} duplicates skipped.`,
    notificationType: 'info',
    duration: 4
  });
} else {
  utils.showNotification({
    title: 'Import successful',
    description: `All ${inserted} rows imported successfully.`,
    notificationType: 'success',
    duration: 3
  });
}
```

**Expected result:** The notification type and message accurately reflect the actual outcome of the operation.

## Complete code example

File: `JS Query: saveWithNotification`

```javascript
// Complete save flow with contextual notifications
// Triggered by a 'Save' button

// Validate before saving
const name = nameInput.value?.trim();
const email = emailInput.value?.trim();

if (!name || !email) {
  utils.showNotification({
    title: 'Validation error',
    description: 'Name and email are required.',
    notificationType: 'error',
    duration: 4
  });
  return;
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
  utils.showNotification({
    title: 'Invalid email',
    description: 'Please enter a valid email address.',
    notificationType: 'warning',
    duration: 4
  });
  return;
}

try {
  // Perform the save
  await saveUserQuery.trigger();

  const rowsAffected = saveUserQuery.data?.rowCount || 0;

  if (rowsAffected > 0) {
    utils.showNotification({
      title: 'Profile saved',
      description: `${name}'s profile has been updated.`,
      notificationType: 'success',
      duration: 3
    });
    // Refresh the main data table
    await query1.trigger();
  } else {
    utils.showNotification({
      title: 'No changes',
      description: 'Nothing was updated.',
      notificationType: 'info',
      duration: 3
    });
  }
} catch (err) {
  utils.showNotification({
    title: 'Save failed',
    description: err.message || 'An unexpected error occurred. Please try again.',
    notificationType: 'error',
    duration: 6
  });
}
```

## Common mistakes

- **Showing a success notification before awaiting the query, which displays success even if the query fails** — undefined Fix: Always show the notification after the await: 'await saveQuery.trigger(); utils.showNotification({...})'. Placing the notification before the await or without await means it shows regardless of success or failure.
- **Using the same short duration for all notification types** — undefined Fix: Error messages need more reading time. Use 3 seconds for success, 4 for warning, 5-6 for error. A 2-second error message often dismisses before the user reads it.
- **Not showing any notification after a delete or save operation, leaving users uncertain** — undefined Fix: Every data mutation should produce a notification. Users expect visual confirmation that their action was processed — without it, they often click the button multiple times.
- **Catching errors but not showing a notification in the catch block** — undefined Fix: Always include a utils.showNotification() in your catch block: catch(err) { utils.showNotification({ title: 'Error', description: err.message, notificationType: 'error' }); }. Without this, errors fail silently.

## Best practices

- Always show a notification after data mutations (INSERT, UPDATE, DELETE) — users need confirmation that their action was processed.
- Use 'error' type for failures, 'success' for completions, 'warning' for partial success or data concerns, and 'info' for status updates that don't require user action.
- Set longer durations for errors (5-8 seconds) and warnings (4-5 seconds) since users need more time to read them than success messages (2-3 seconds).
- Include specific details in error notifications (what failed and why) rather than generic 'An error occurred' messages.
- Use try/catch in JS Queries to catch query errors and show a user-friendly error notification instead of letting Retool show its default error banner.
- For bulk operations, show a count of affected records: '42 records updated successfully.' rather than just 'Done.'
- Notifications are temporary — for critical information users must not miss, use a Modal dialog component instead.

## Frequently asked questions

### Where do Retool toast notifications appear on screen?

Toast notifications from utils.showNotification() appear in the bottom-right corner of the Retool app window. They stack if multiple are shown simultaneously. The position cannot be changed without custom CSS targeting Retool's notification container class.

### Can I show a Retool notification that stays on screen indefinitely?

Set the duration to a very large number (e.g. 999999) to keep a notification visible until the user dismisses it. However, Retool notifications do not have a manual dismiss button in all versions. For persistent important messages that users must acknowledge, use a Modal dialog component instead.

### What is the difference between utils.showNotification() and query error banners in Retool?

Query error banners appear automatically at the top of the page when a query fails — they are Retool's default error display. utils.showNotification() is a programmatic API you call explicitly to show custom messages with your own wording, timing, and type. For better UX, catch query errors and show custom notifications instead of relying on Retool's default error banners.

### Can I use utils.showNotification() in a transformer?

No. Transformers are read-only and synchronous — they cannot call utils.showNotification() or any other side-effect function. Call utils.showNotification() only in JS Queries or event handler actions.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-create-notifications-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-create-notifications-in-retool
