Tutorial

Retool Tutorial: Build Internal Tools Fast [2025]

House of Loops TeamOctober 3, 20258 min read
Retool Tutorial: Build Internal Tools Fast [2025]

Retool Tutorial: Build Internal Tools Fast

Building internal tools typically takes weeks of development time – time your engineering team could spend on customer-facing features. Retool changes this equation completely. In this tutorial, you'll learn how to build professional internal tools in minutes instead of weeks.

What is Retool?

Retool is a low-code platform specifically designed for building internal tools. Think admin panels, dashboards, database GUIs, approval workflows, and operational tools that your team uses every day.

What Makes Retool Special:

  • Pre-built components: 100+ UI components ready to use
  • Database-first: Native integrations with every major database
  • Write SQL/JS: Drop down to code when you need it
  • Enterprise-ready: Permissions, SSO, audit logs built-in
  • Self-hostable: Deploy on your infrastructure

Common Use Cases:

  • Customer support dashboards
  • Data administration panels
  • Approval workflows
  • Inventory management
  • Analytics dashboards
  • Order processing tools
  • Content moderation panels

Retool vs Other Tools

  • vs Bubble: Retool is for internal tools, Bubble for external apps
  • vs Custom Code: 10x faster development, easier maintenance
  • vs Spreadsheets: More powerful, better permissions, cleaner UX
  • vs Traditional Admin Panels: Better UX, faster to build

Prerequisites

Before we start:

  • A free Retool account (sign up here)
  • A database to connect (we'll use PostgreSQL with Supabase)
  • Basic SQL knowledge (helpful but not required)

Our Project: Customer Support Dashboard

We'll build a real-world customer support tool with:

  • View all customer tickets
  • Filter and search functionality
  • Edit ticket status and priority
  • View customer details
  • Send notifications to customers
  • Analytics dashboard

This covers all Retool essentials you'll use in real projects.

Step 1: Create Your Retool App

  1. Log into your Retool dashboard
  2. Click "Create new""App"
  3. Name it: "Customer Support Dashboard"
  4. Choose "From scratch"

You'll see the Retool editor with:

  • Left sidebar: Component library
  • Center canvas: Your app interface
  • Right panel: Component properties
  • Bottom panel: Queries and code

Step 2: Connect Your Database

First, we need a database. If you don't have one, follow these quick steps:

Set Up Supabase Database (5 minutes)

  1. Create a free Supabase project
  2. In Supabase SQL Editor, run:
-- Create customers table
CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  status TEXT DEFAULT 'active'
);

-- Create tickets table
CREATE TABLE tickets (
  id SERIAL PRIMARY KEY,
  customer_id INTEGER REFERENCES customers(id),
  subject TEXT NOT NULL,
  description TEXT,
  status TEXT DEFAULT 'open',
  priority TEXT DEFAULT 'medium',
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Insert sample data
INSERT INTO customers (email, name) VALUES
  ('john@example.com', 'John Smith'),
  ('sarah@example.com', 'Sarah Johnson'),
  ('mike@example.com', 'Mike Chen');

INSERT INTO tickets (customer_id, subject, description, status, priority) VALUES
  (1, 'Login issues', 'Cannot access my account', 'open', 'high'),
  (1, 'Billing question', 'Need invoice for last month', 'pending', 'low'),
  (2, 'Feature request', 'Would love dark mode', 'open', 'medium'),
  (3, 'Bug report', 'Page not loading correctly', 'in_progress', 'high');

Connect Supabase to Retool

  1. In Retool, click "Resources" (bottom left)

  2. Click "Create new""PostgreSQL"

  3. Configure connection:

    • Name: Supabase Production
    • Host: Your Supabase host (Settings → Database → Host)
    • Port: 5432
    • Database name: postgres
    • Database username: postgres
    • Database password: Your password
    • SSL: Enabled
  4. Click "Test connection""Save"

You're now connected!

Step 3: Create Tickets Table Component

Let's display all tickets in a table:

  1. Drag a "Table" component onto the canvas
  2. Rename it to ticketsTable (bottom panel)
  3. Resize to take up most of the screen

Create Tickets Query

  1. Click "+ New""Resource query"
  2. Select your Supabase resource
  3. Name: getTickets
  4. Query:
SELECT
  t.id,
  t.subject,
  t.description,
  t.status,
  t.priority,
  t.created_at,
  t.updated_at,
  c.name as customer_name,
  c.email as customer_email
FROM tickets t
LEFT JOIN customers c ON t.customer_id = c.id
ORDER BY t.created_at DESC
  1. Click "Run" to test
  2. Click "Save"

Bind Data to Table

  1. Select the ticketsTable component
  2. In the Data section (right panel), set:
    • Data source: {{ getTickets.data }}

Your table now shows all tickets with customer information!

Customize Table Columns

In the table properties:

  1. Click "Columns" tab

  2. Hide columns you don't want (like customer_id)

  3. Customize column headers:

    • id → "ID"
    • subject → "Subject"
    • customer_name → "Customer"
    • status → "Status"
    • priority → "Priority"
    • created_at → "Created"
  4. Format the created_at column:

    • Click the column
    • Display formatting: Date/Time
    • Format: Relative time (e.g., "2 hours ago")

Step 4: Add Search and Filters

Let's add filtering capabilities above the table.

Add Search Input

  1. Drag a "Text Input" component above the table
  2. Rename to searchInput
  3. Set Placeholder: "Search tickets..."

Add Status Filter

  1. Drag a "Select" component next to the search input
  2. Rename to statusFilter
  3. Set Options:
[
  { "label": "All Statuses", "value": "all" },
  { "label": "Open", "value": "open" },
  { "label": "In Progress", "value": "in_progress" },
  { "label": "Pending", "value": "pending" },
  { "label": "Closed", "value": "closed" }
]
  1. Set Default value: "all"

Add Priority Filter

  1. Drag another "Select" component
  2. Rename to priorityFilter
  3. Set Options:
[
  { "label": "All Priorities", "value": "all" },
  { "label": "High", "value": "high" },
  { "label": "Medium", "value": "medium" },
  { "label": "Low", "value": "low" }
]
  1. Set Default value: "all"

Update Query with Filters

Edit your getTickets query:

SELECT
  t.id,
  t.subject,
  t.description,
  t.status,
  t.priority,
  t.created_at,
  t.updated_at,
  c.name as customer_name,
  c.email as customer_email
FROM tickets t
LEFT JOIN customers c ON t.customer_id = c.id
WHERE
  ({{ statusFilter.value }} = 'all' OR t.status = {{ statusFilter.value }})
  AND ({{ priorityFilter.value }} = 'all' OR t.priority = {{ priorityFilter.value }})
  AND (
    {{ searchInput.value }} = ''
    OR t.subject ILIKE {{ '%' + searchInput.value + '%' }}
    OR t.description ILIKE {{ '%' + searchInput.value + '%' }}
    OR c.name ILIKE {{ '%' + searchInput.value + '%' }}
  )
ORDER BY t.created_at DESC

Auto-refresh on Filter Change

For each filter component:

  1. Select the component
  2. Click "Interaction" tab
  3. Event handlerOn change
  4. ActionRun querygetTickets

Now filtering is instant!

Step 5: Create Ticket Details Panel

When you click a row, show detailed ticket information in a side panel.

Add Container Component

  1. Drag a "Container" component to the right side
  2. Rename to detailsPanel
  3. Set Hidden: {{ !ticketsTable.selectedRow }} (Only show when a row is selected)

Add Details Inside Container

Inside the container, add:

  1. Text component:

    • Value: {{ ticketsTable.selectedRow.data.subject }}
    • Font size: 24px
    • Font weight: Bold
  2. Text component for customer:

    • Value: "Customer: " + {{ ticketsTable.selectedRow.data.customer_name }}
  3. Text component for email:

    • Value: {{ ticketsTable.selectedRow.data.customer_email }}
  4. Text component for description:

    • Value: {{ ticketsTable.selectedRow.data.description }}
    • Multiline: Yes
  5. Select component for status:

    • Rename: statusSelect
    • Options: ["open", "in_progress", "pending", "closed"]
    • Value: {{ ticketsTable.selectedRow.data.status }}
  6. Select component for priority:

    • Rename: prioritySelect
    • Options: ["low", "medium", "high"]
    • Value: {{ ticketsTable.selectedRow.data.priority }}
  7. Button component:

    • Text: "Save Changes"
    • Rename: saveButton

Step 6: Update Ticket Status

Create a query to update tickets:

  1. Create new query: updateTicket
  2. Type: SQL
  3. Query:
UPDATE tickets
SET
  status = {{ statusSelect.value }},
  priority = {{ prioritySelect.value }},
  updated_at = NOW()
WHERE id = {{ ticketsTable.selectedRow.data.id }}

Add Save Button Workflow

  1. Select saveButton
  2. Event handlerOn click
  3. Action 1Run queryupdateTicket
  4. Action 2Run querygetTickets (refresh table)
  5. Action 3Show notification
    • Message: "Ticket updated successfully!"
    • Type: Success

Test it! Click a row, change status, and save.

Step 7: Add Customer Information Panel

Create a modal to view customer details:

  1. Drag a "Modal" component
  2. Rename to customerModal
  3. Inside the modal, add:
// Title
{
  {
    ticketsTable.selectedRow.data.customer_name;
  }
}

// Email
{
  {
    ticketsTable.selectedRow.data.customer_email;
  }
}

Create Customer Tickets Query

SELECT
  id,
  subject,
  status,
  priority,
  created_at
FROM tickets
WHERE customer_id = {{
  (SELECT customer_id
   FROM tickets
   WHERE id = {{ ticketsTable.selectedRow.data.id }})
}}
ORDER BY created_at DESC

Add a table inside the modal showing this customer's tickets.

Add Button to Open Modal

In the detailsPanel:

  1. Add Button: "View Customer Details"
  2. On clickShow modalcustomerModal

Step 8: Add Analytics Dashboard

Create a new page for analytics:

  1. Top left: PagesAdd page
  2. Name: "Analytics"

Create Metrics Queries

Query 1: Total Tickets

SELECT COUNT(*) as total FROM tickets

Query 2: Open Tickets

SELECT COUNT(*) as total FROM tickets WHERE status = 'open'

Query 3: Tickets by Status

SELECT status, COUNT(*) as count
FROM tickets
GROUP BY status

Query 4: Tickets by Priority

SELECT priority, COUNT(*) as count
FROM tickets
GROUP BY priority

Add Statistic Components

Drag 4 "Statistic" components in a row:

  1. Statistic 1:

    • Value: {{ getTotalTickets.data[0].total }}
    • Label: "Total Tickets"
  2. Statistic 2:

    • Value: {{ getOpenTickets.data[0].total }}
    • Label: "Open Tickets"
  3. Statistic 3:

    • Calculate: {{ (getOpenTickets.data[0].total / getTotalTickets.data[0].total * 100).toFixed(1) + '%' }}
    • Label: "Open Rate"

Add Charts

  1. Drag a "Chart" component
  2. Chart type: Pie
  3. Data: {{ getTicketsByStatus.data }}
  4. Label key: status
  5. Value key: count
  6. Title: "Tickets by Status"

Add another chart for priority breakdown.

Step 9: Add Permissions

Control who can see what:

  1. Click "Settings" (top right)
  2. Permissions tab
  3. Create groups:
    • Admins: Full access
    • Support: Can view and update
    • Read-only: Can only view

Add Conditional Visibility

For the save button:

  1. Select component
  2. Hidden: {{ !current_user.groups.includes('Admin') && !current_user.groups.includes('Support') }}

Step 10: Add Automation

Let's send an email when ticket status changes to "closed".

Create SendGrid Resource

  1. ResourcesCreate newREST API
  2. Name: "SendGrid"
  3. Base URL: https://api.sendgrid.com/v3
  4. Headers:
    • Authorization: Bearer YOUR_SENDGRID_API_KEY
    • Content-Type: application/json

Create Email Query

// Query type: REST API
// Method: POST
// Endpoint: /mail/send
// Body:
{
  "personalizations": [{
    "to": [{"email": "{{ ticketsTable.selectedRow.data.customer_email }}"}],
    "subject": "Your ticket has been resolved"
  }],
  "from": {"email": "support@yourcompany.com"},
  "content": [{
    "type": "text/html",
    "value": `
      <h1>Ticket Resolved</h1>
      <p>Hi {{ ticketsTable.selectedRow.data.customer_name }},</p>
      <p>Your ticket "${​{ ticketsTable.selectedRow.data.subject }}" has been resolved.</p>
      <p>Thanks for your patience!</p>
    `
  }]
}

Trigger Email on Status Change

Update the save button workflow:

  1. Action 1: Update ticket
  2. Action 2: Run JS Code:
if (statusSelect.value === 'closed') {
  sendCloseEmail.trigger();
}
  1. Action 3: Refresh tickets
  2. Action 4: Show notification

Advanced Retool Features

JavaScript Transformers

Transform data before displaying:

// In a component's value field
{
  {
    ticketsTable.data.map(ticket => ({
      ...ticket,
      statusBadge: ticket.status === 'open' ? '🔴 Open' : '✅ Closed',
    }));
  }
}

Custom Components

Build reusable components:

  1. ComponentsCustom component
  2. Build once, use everywhere

Mobile Apps

Retool Mobile lets you build native iOS/Android apps using the same components.

Workflows

Create backend automation:

  1. Workflows tab
  2. Build serverless functions
  3. Trigger via webhooks or schedule

Version Control

Retool has built-in Git integration:

  1. SettingsVersion control
  2. Connect to GitHub/GitLab
  3. Commit changes with descriptions
  4. Create branches for features

Best Practices

1. Query Optimization

Don't fetch unnecessary data:

-- Bad: Fetches everything
SELECT * FROM large_table

-- Good: Only needed columns
SELECT id, name, email FROM large_table LIMIT 100

2. Use Loading States

Show users when data is loading:

  • Set component Loading: {{ query.isFetching }}

3. Error Handling

Handle errors gracefully:

{
  {
    query.data ? query.data : 'No data available';
  }
}

4. Organize with Folders

Use folders in the component tree:

  • Header components
  • Table section
  • Details panel
  • Modals

5. Reusable Queries

Create global queries in Modules for reuse across apps.

Retool Pricing

  • Free: 5 users, unlimited apps
  • Team: $10/user/month
  • Business: $50/user/month (SSO, audit logs)
  • Enterprise: Custom pricing (self-hosted option)

Most companies start on Team plan.

When to Use Retool

Perfect for:

  • Internal dashboards
  • Admin panels
  • Database GUIs
  • Approval workflows
  • Operations tools
  • Customer support tools

Not ideal for:

  • Customer-facing apps (use Bubble)
  • Public websites (use Webflow)
  • Mobile-first apps (consider FlutterFlow)
  • Real-time collaborative tools

Retool Alternatives

  • Bubble: Better for external apps
  • Appsmith: Open-source alternative
  • Budibase: Self-hosted option
  • Internal.io: Similar to Retool
  • Custom code: If you need 100% flexibility

Common Use Cases

Customer Success Dashboard

View customer health scores, usage, support tickets, and engagement.

Order Management System

Process orders, update inventory, manage fulfillment.

Content Moderation

Review user-generated content, approve/reject, flag issues.

Data Migration Tool

Migrate data between databases with validation.

Approval Workflows

Route requests through approval chains with notifications.

Integrations

Retool connects to everything:

Databases:

  • PostgreSQL, MySQL, MongoDB
  • Supabase, Firebase
  • Snowflake, BigQuery, Redshift

APIs:

  • Any REST or GraphQL API
  • Stripe, Twilio, SendGrid
  • Internal microservices

Automation:

Join the House of Loops Community

Want to master Retool and build better internal tools? Join House of Loops for:

  • 30+ Retool templates for common use cases
  • $100K+ in startup credits (including Retool credits)
  • Weekly workshops on internal tools
  • Community of 1,000+ developers
  • Expert code reviews for your Retool apps

Join House of Loops Today →

Retool eliminates the tedious work of building internal tools, letting your team focus on what matters: shipping features customers love. Stop building CRUD interfaces from scratch – build once in Retool and move on to more important work. Happy building!

H

House of Loops Team

House of Loops is a technology-focused community for learning and implementing advanced automation workflows using n8n, Strapi, AI/LLM, and DevSecOps tools.

Join Our Community

Join 1,000+ automation enthusiasts