# How to Integrate Retool with Notion

- Tool: Retool
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: April 2026

## TL;DR

Connect Retool to Notion using a REST API Resource with a Notion integration token. Configure the base URL and authorization header in Retool's Resources tab, then build queries that filter, sort, create, and update Notion database pages. Retool's Table and Form components replace Notion's limited table views with powerful, customizable interfaces that your team can actually use as a full operations dashboard.

## Replace Notion's Limited Tables with a Powerful Retool Admin Panel

Notion databases are powerful for storing and organizing data, but Notion's built-in table views have significant limitations for operations teams: no advanced filtering across multiple fields simultaneously, limited sorting options, no built-in charts or analytics, and no way to execute complex actions (like sending emails or updating CRM records) directly from a table row. By connecting Retool to Notion's API, you get the flexibility of Notion as a data store while gaining Retool's far more capable query, display, and action infrastructure.

Notion's REST API provides full CRUD access to databases and pages. You can query a database with compound filter objects (combining multiple property filters with AND/OR logic), sort by any property, paginate through large datasets, and create or update pages with rich property values. Properties can be text, numbers, selects, multi-selects, dates, checkboxes, URLs, emails, relations, and rollups — all accessible and writable through the API.

The most common Retool-Notion pattern is building an operations dashboard that reads from one or more Notion databases, displays the data in Retool's Table component with advanced filtering and search, and uses Retool Forms to create or edit Notion pages directly. Teams use this to build customer management panels on top of Notion contact databases, project status dashboards over Notion project databases, and content calendars that offer better filtering and bulk editing than Notion's native views provide.

## Before you start

- A Notion workspace with at least one database you want to connect to Retool
- A Notion internal integration created at notion.so/my-integrations with an API token
- The Notion database ID for each database you want to query (visible in the database page URL)
- The integration must be added to each Notion database via 'Add connections' in the database's three-dot menu
- A Retool account with permission to create Resources

## Step-by-step guide

### 1. Create a Notion internal integration and get your API token

Go to notion.so/my-integrations and click 'New integration'. Give it a descriptive name like 'Retool Dashboard' and select the Notion workspace where your target databases live. Under 'Capabilities', select the permissions your Retool app needs: Read content is required for querying; Update content and Insert content are needed if Retool will write back to Notion; Read user information is optional.

After creating the integration, Notion displays the Internal Integration Token — a long string starting with 'secret_'. Copy this token immediately and store it securely. This token is your API credential for all Retool queries.

IMPORTANT: Creating the integration alone is not enough. You must explicitly grant the integration access to each Notion database (or page containing that database) you want to query. To do this, open the Notion database in your browser. Click the three-dot menu (⋯) in the top right corner, select 'Connections', search for your integration name, and click 'Confirm'. If you skip this step, Retool queries will return a 404 'Could not find database' error even with a valid token.

Note the database ID for each database you connect. The database ID is found in the URL when viewing the database: notion.so/workspace/DatabaseTitle-{DATABASE_ID}?v=... — it's the 32-character string after the last hyphen in the database title and before the query parameter.

**Expected result:** You have a Notion internal integration token and have added the integration to all target databases. You have noted the database IDs for each database you'll query.

### 2. Configure the Notion REST API Resource in Retool

In Retool, navigate to the Resources tab and click 'Create a resource'. Select 'REST API'. Name the resource 'Notion API'.

Set the base URL to https://api.notion.com/v1. This is Notion's API v1 base URL — all query paths will be relative to this URL.

For authentication, select 'Bearer Token' from the authentication dropdown. Paste your Notion internal integration token (the 'secret_...' string). Retool will send this as an Authorization: Bearer secret_... header with every request.

Notion requires a version header to maintain backward compatibility. Under 'Headers', click 'Add header'. Set the name to Notion-Version and the value to 2022-06-28 (the latest stable version as of 2026). This header is required for all Notion API requests — without it, you'll receive a 400 error.

Also add a Content-Type header with value application/json, which is required for POST and PATCH requests that send JSON body data.

Save the resource. Unlike OAuth-based resources, Notion internal integration tokens don't expire, so there's no OAuth flow to complete. The resource is ready to use immediately. Click 'Test connection' or run a simple query to verify the configuration is correct.

**Expected result:** The Notion API resource is configured in Retool with Bearer authentication and the required Notion-Version header. The resource is ready for queries.

### 3. Build a query to filter and list Notion database pages

Create a new query in your Retool app. Select the Notion API resource. Set the method to POST and the path to /databases/{DATABASE_ID}/query, replacing {DATABASE_ID} with your actual Notion database ID. Notion's database query endpoint uses POST (not GET) because filter and sort parameters are sent in the request body.

In the Body tab, set the body type to JSON. The body structure supports filter and sorts objects for server-side filtering, and page_size and start_cursor for pagination. A complete query body with filters looks like the code example below.

For dynamic filtering based on Retool components, reference component values inside the filter object using {{ }} syntax. For example, if you have a Select component named statusFilter, reference its value in the filter object. When statusFilter.value is empty, you want no filter — use a conditional approach with a JavaScript query that constructs the body object only when filter values are present.

Enable 'Run query automatically' so the dashboard loads on open. Set debounce to 300ms for any search inputs connected to this query to avoid excessive API calls.

```
// POST /databases/{DATABASE_ID}/query
// Request body for filtering and sorting a Notion database
{{
  JSON.stringify({
    filter: statusFilter.value ? {
      property: 'Status',
      status: {
        equals: statusFilter.value
      }
    } : undefined,
    sorts: [
      {
        property: 'Created Date',
        direction: 'descending'
      }
    ],
    page_size: 50
  })
}}
```

**Expected result:** The query returns a JSON object with a 'results' array containing Notion page objects, each with an id, properties object, and metadata. The results respect the filter and sort parameters you configured.

### 4. Transform Notion's response and bind to Retool components

Notion's API response structure is significantly more complex than typical REST APIs. Each page has a properties object where each property is itself an object containing a type field and a nested value. For example, a Title property looks like: { 'type': 'title', 'title': [{ 'text': { 'content': 'My Page Title' } }] }. A Select property looks like: { 'type': 'select', 'select': { 'name': 'Active', 'color': 'green' } }.

Create a transformer to flatten this structure into simple key-value objects suitable for binding to a Table component. The transformer logic varies by property type — title and rich_text properties store content in an array of text blocks; select and status properties have a name field; number properties have a number field; date properties have a start and optional end; checkbox is a boolean.

After transforming, bind the transformer's output to a Table component: set the Table's data to {{ notionQueryTransformer.value }}. Configure Table columns to match your Notion database properties. Add TextInput for search and Select components for property filters, connecting them to the query body in the previous step.

For the selected row detail view, add a panel on the right side with individual form fields showing the selected Notion page's properties. Use {{ databaseTable.selectedRow?.propertyName }} to populate each field.

```
// JavaScript transformer to flatten Notion database query response
const pages = data.results || [];

return pages.map(page => {
  const props = page.properties;
  
  // Helper functions for each Notion property type
  const getTitle = (prop) => prop?.title?.[0]?.text?.content || '';
  const getRichText = (prop) => prop?.rich_text?.[0]?.text?.content || '';
  const getSelect = (prop) => prop?.select?.name || '';
  const getMultiSelect = (prop) => prop?.multi_select?.map(s => s.name).join(', ') || '';
  const getNumber = (prop) => prop?.number ?? null;
  const getDate = (prop) => prop?.date?.start || '';
  const getCheckbox = (prop) => prop?.checkbox ?? false;
  const getStatus = (prop) => prop?.status?.name || '';
  const getEmail = (prop) => prop?.email || '';
  const getUrl = (prop) => prop?.url || '';

  return {
    id: page.id,
    // Map your specific Notion database property names:
    name: getTitle(props['Name'] || props['Title']),
    status: getSelect(props['Status']) || getStatus(props['Status']),
    assignee: getSelect(props['Assignee']),
    dueDate: getDate(props['Due Date']),
    priority: getSelect(props['Priority']),
    tags: getMultiSelect(props['Tags']),
    notes: getRichText(props['Notes']),
    notionUrl: page.url
  };
});
```

**Expected result:** The transformer flattens Notion's complex property structure into simple objects. The Table component displays clean, readable data from your Notion database with all relevant columns visible.

### 5. Build create and update queries for writing back to Notion

Build write-back functionality so your Retool app can create new Notion pages and update existing ones. For creating pages, create a query with POST method and path /pages. The request body must include a parent (specifying the database ID), and properties matching your database's schema.

For updating pages, create a query with PATCH method and path /pages/{{ databaseTable.selectedRow?.id }}. The body only needs to include the properties you want to change — Notion performs partial updates.

Build a Retool Form component connected to the create query. Add form fields for each required Notion property (Title, Status, Due Date, etc.). Configure the Form's submit button to trigger the create query. Add an event handler on the query's On Success to run your main database query again to refresh the table, and show a success notification.

For inline editing in the table, use the Table component's edit functionality: enable 'Editable rows' on the Table, set up an updatePage query for the PATCH call, and configure a 'Row save' event handler that triggers the PATCH query with the edited row's data. For complex Retool-Notion integrations with multiple databases, relation properties, and workflow automation, RapidDev's team can help build out the full solution.

```
// POST /pages — Create a new Notion page in a database
// Body for creating a new page with common property types:
{
  "parent": {
    "database_id": "your-database-id-here"
  },
  "properties": {
    "Name": {
      "title": [
        {
          "text": {
            "content": "{{ createNameInput.value }}"
          }
        }
      ]
    },
    "Status": {
      "select": {
        "name": "{{ createStatusSelect.value }}"
      }
    },
    "Due Date": {
      "date": {
        "start": "{{ createDueDatePicker.value }}"
      }
    },
    "Notes": {
      "rich_text": [
        {
          "text": {
            "content": "{{ createNotesInput.value }}"
          }
        }
      ]
    }
  }
}
```

**Expected result:** The Retool app can create new Notion pages via a form and update existing pages inline in the table. Changes appear in Notion immediately and the table refreshes to show updated data.

## Best practices

- Store your Notion integration token in a Retool configuration variable marked as secret rather than pasting it directly in the Resource Bearer Token field
- Filter data at the Notion API level using the filter object rather than loading all pages and filtering in Retool — Notion's filter support is comprehensive and reduces data transfer for large databases
- Cache your Notion queries with a 2-5 minute TTL for read-heavy dashboards — Notion data rarely changes by the second and caching significantly reduces API call volume
- Create separate integration connections for different databases rather than one integration with access to everything — follow the principle of least privilege for data access
- Use Notion's sorts object for server-side sorting rather than sorting transformer output in JavaScript — this is more efficient and respects Notion's data types for date and number sorting
- Handle Notion's pagination in your transformer by checking has_more and implementing a 'Load more' button that passes next_cursor to the query rather than trying to fetch all records at once
- For write operations (create/update pages), always validate form input in Retool before submitting — Notion's API returns descriptive validation errors but showing these to users is a poor experience compared to client-side validation

## Use cases

### Customer database admin panel

Build a Retool interface over a Notion customer database that shows contacts, filters by status and tier, allows bulk updates, and lets team members add notes — all with better performance and more filtering options than Notion's table view.

Prompt example:

```
Build a Retool app that queries a Notion customer database and shows a Table with columns for customer name, status, tier, last contact date, and assigned rep. Add TextInput and Select filters that update the Notion query filters. Include a Form panel that opens when a row is selected, allowing editing of customer properties and saving back to Notion.
```

### Project status dashboard with multi-database rollup

Combine data from multiple Notion databases (projects, tasks, team members) into a unified Retool dashboard. Use JavaScript queries to join the datasets and display a project health view showing completion percentages, overdue tasks, and team assignments across all active projects.

Prompt example:

```
Build a Retool dashboard that queries a Notion projects database and a tasks database. Use a transformer to calculate completion percentage per project (completed tasks / total tasks). Show a Table with project name, owner, deadline, completion %, and status. Clicking a row shows a filtered task list for that project.
```

### Content calendar management panel

Build a content operations panel over a Notion content calendar database. Filter content by status (Draft, In Review, Scheduled, Published), owner, and date range. Allow bulk status updates from Retool and add a form for creating new content entries directly into Notion.

Prompt example:

```
Build a Retool content calendar panel that queries a Notion database filtered by content status. Show Title, Author, Publish Date, Channel, and Status columns. Add a multi-select filter for status and a date range picker. Include a 'Create New Content' button that opens a Form to add a new Notion page with all required properties.
```

## Troubleshooting

### Notion API returns 404 with message 'Could not find database with ID' even though the database exists

Cause: The Notion internal integration has not been added to the database. Creating the integration alone does not grant access — you must explicitly connect the integration to each database.

Solution: In Notion, open the database page. Click the three-dot menu (⋯) in the top right → Connections → find your integration name → click Confirm. If the database is inside a page, you may need to add the integration to the parent page. After connecting, retry the Retool query — it should work immediately.

### Notion query returns 400 Bad Request with 'body failed validation: body.filter should be defined' when trying to query without filters

Cause: The request body contains an undefined or null filter property. When no filter is needed, the filter key should be omitted entirely rather than set to null or undefined.

Solution: In your query body, use a JavaScript expression that conditionally includes the filter only when a value is present. If using a static JSON body, remove the filter key entirely when no filtering is needed. The simplest approach is a JavaScript query that constructs the body object programmatically.

```
// Construct query body only with non-empty filters
const body = {
  sorts: [{ property: 'Created', direction: 'descending' }],
  page_size: 50
};

// Only add filter if status is selected
if (statusFilter.value) {
  body.filter = {
    property: 'Status',
    select: { equals: statusFilter.value }
  };
}

return body;
```

### Notion database query only returns 100 results even though the database has more records

Cause: Notion's API has a maximum page_size of 100 per request. With more than 100 records, you need to use pagination via the next_cursor field in the response.

Solution: Check the response for a 'has_more' boolean and a 'next_cursor' string. Pass next_cursor as the start_cursor parameter in the next query to get the following page. For dashboards displaying more than 100 records, implement server-side pagination by passing the cursor value to subsequent query calls, or use a Retool Workflow to fetch all pages and aggregate results.

### Rich text or title properties appear as empty strings in the transformer even though Notion shows content

Cause: Rich text and title properties store content in an array of content blocks, not as simple strings. Accessing the wrong array index or missing nested fields returns undefined.

Solution: Access rich text content with the pattern prop?.title?.[0]?.text?.content or prop?.rich_text?.[0]?.text?.content. Use optional chaining (?.) to avoid errors when any level of the nested structure is null. If a property has multiple text segments, join them: prop.rich_text.map(t => t.text.content).join('').

```
// Safely extract rich text content from Notion property
const getRichText = (prop) => {
  if (!prop || !prop.rich_text) return '';
  return prop.rich_text.map(t => t.text?.content || '').join('');
};
```

## Frequently asked questions

### Why does Notion's database query endpoint use POST instead of GET?

Notion designed the /databases/{id}/query endpoint as POST because filter and sort parameters can be complex nested JSON objects that would be unwieldy as URL query parameters. The POST method allows you to send a structured JSON body with compound filters combining multiple properties. In Retool, this means you configure the query as POST with the filter/sort logic in the Body tab rather than URL Parameters.

### Can I query multiple Notion databases in one Retool app?

Yes, create one query per database you want to access. Each query targets the /databases/{DATABASE_ID}/query endpoint with its own database ID. You can then join data across databases using a JavaScript query that references both query results. Ensure your Notion integration has been added to all target databases via the Connections menu in each database.

### Does the Notion integration token expire?

Notion internal integration tokens do not expire — they remain valid until you manually revoke them in the notion.so/my-integrations settings. This is one advantage of using internal integrations over OAuth for server-side Retool use cases. The token only becomes invalid if you delete the integration or manually regenerate the secret.

### How do I access Notion page content (text blocks) rather than just database properties?

Notion page content (the blocks inside a page) is accessed via a separate endpoint: GET /blocks/{PAGE_ID}/children. This returns all the block-level content of a page. Combined with the database query (which gives you page IDs and properties), you can build a two-query system: the first query lists database pages, and a second query triggered on row selection loads the full page content for the selected item.

### Is Retool a good replacement for Notion's native table views?

Retool excels at displaying and manipulating Notion database data compared to Notion's native views when you need advanced multi-field filtering, compound sorting, bulk operations, charts and analytics, or actions that span multiple systems. Notion's native views are better for editorial workflows, page creation, and casual browsing — use both tools for what they do best, with Retool handling the operational/analytics side.

---

Source: https://www.rapidevelopers.com/retool-integrations/notion
© RapidDev — https://www.rapidevelopers.com/retool-integrations/notion
