Retool Tutorial: Build Internal Tools Fast [2025]
![Retool Tutorial: Build Internal Tools Fast [2025]](/blog/images/retool-tutorial.jpg)
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
- Log into your Retool dashboard
- Click "Create new" → "App"
- Name it: "Customer Support Dashboard"
- 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)
- Create a free Supabase project
- 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
-
In Retool, click "Resources" (bottom left)
-
Click "Create new" → "PostgreSQL"
-
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
-
Click "Test connection" → "Save"
You're now connected!
Step 3: Create Tickets Table Component
Let's display all tickets in a table:
- Drag a "Table" component onto the canvas
- Rename it to
ticketsTable(bottom panel) - Resize to take up most of the screen
Create Tickets Query
- Click "+ New" → "Resource query"
- Select your Supabase resource
- Name:
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
ORDER BY t.created_at DESC
- Click "Run" to test
- Click "Save"
Bind Data to Table
- Select the
ticketsTablecomponent - In the Data section (right panel), set:
- Data source:
{{ getTickets.data }}
- Data source:
Your table now shows all tickets with customer information!
Customize Table Columns
In the table properties:
-
Click "Columns" tab
-
Hide columns you don't want (like customer_id)
-
Customize column headers:
id→ "ID"subject→ "Subject"customer_name→ "Customer"status→ "Status"priority→ "Priority"created_at→ "Created"
-
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
- Drag a "Text Input" component above the table
- Rename to
searchInput - Set Placeholder: "Search tickets..."
Add Status Filter
- Drag a "Select" component next to the search input
- Rename to
statusFilter - 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" }
]
- Set Default value: "all"
Add Priority Filter
- Drag another "Select" component
- Rename to
priorityFilter - Set Options:
[
{ "label": "All Priorities", "value": "all" },
{ "label": "High", "value": "high" },
{ "label": "Medium", "value": "medium" },
{ "label": "Low", "value": "low" }
]
- 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:
- Select the component
- Click "Interaction" tab
- Event handler → On change
- Action → Run query →
getTickets
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
- Drag a "Container" component to the right side
- Rename to
detailsPanel - Set Hidden:
{{ !ticketsTable.selectedRow }}(Only show when a row is selected)
Add Details Inside Container
Inside the container, add:
-
Text component:
- Value:
{{ ticketsTable.selectedRow.data.subject }} - Font size: 24px
- Font weight: Bold
- Value:
-
Text component for customer:
- Value:
"Customer: " + {{ ticketsTable.selectedRow.data.customer_name }}
- Value:
-
Text component for email:
- Value:
{{ ticketsTable.selectedRow.data.customer_email }}
- Value:
-
Text component for description:
- Value:
{{ ticketsTable.selectedRow.data.description }} - Multiline: Yes
- Value:
-
Select component for status:
- Rename:
statusSelect - Options: ["open", "in_progress", "pending", "closed"]
- Value:
{{ ticketsTable.selectedRow.data.status }}
- Rename:
-
Select component for priority:
- Rename:
prioritySelect - Options: ["low", "medium", "high"]
- Value:
{{ ticketsTable.selectedRow.data.priority }}
- Rename:
-
Button component:
- Text: "Save Changes"
- Rename:
saveButton
Step 6: Update Ticket Status
Create a query to update tickets:
- Create new query:
updateTicket - Type: SQL
- Query:
UPDATE tickets
SET
status = {{ statusSelect.value }},
priority = {{ prioritySelect.value }},
updated_at = NOW()
WHERE id = {{ ticketsTable.selectedRow.data.id }}
Add Save Button Workflow
- Select
saveButton - Event handler → On click
- Action 1 → Run query →
updateTicket - Action 2 → Run query →
getTickets(refresh table) - Action 3 → Show 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:
- Drag a "Modal" component
- Rename to
customerModal - 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:
- Add Button: "View Customer Details"
- On click → Show modal →
customerModal
Step 8: Add Analytics Dashboard
Create a new page for analytics:
- Top left: Pages → Add page
- 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:
-
Statistic 1:
- Value:
{{ getTotalTickets.data[0].total }} - Label: "Total Tickets"
- Value:
-
Statistic 2:
- Value:
{{ getOpenTickets.data[0].total }} - Label: "Open Tickets"
- Value:
-
Statistic 3:
- Calculate:
{{ (getOpenTickets.data[0].total / getTotalTickets.data[0].total * 100).toFixed(1) + '%' }} - Label: "Open Rate"
- Calculate:
Add Charts
- Drag a "Chart" component
- Chart type: Pie
- Data:
{{ getTicketsByStatus.data }} - Label key: status
- Value key: count
- Title: "Tickets by Status"
Add another chart for priority breakdown.
Step 9: Add Permissions
Control who can see what:
- Click "Settings" (top right)
- Permissions tab
- Create groups:
- Admins: Full access
- Support: Can view and update
- Read-only: Can only view
Add Conditional Visibility
For the save button:
- Select component
- 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
- Resources → Create new → REST API
- Name: "SendGrid"
- Base URL:
https://api.sendgrid.com/v3 - Headers:
Authorization:Bearer YOUR_SENDGRID_API_KEYContent-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:
- Action 1: Update ticket
- Action 2: Run JS Code:
if (statusSelect.value === 'closed') {
sendCloseEmail.trigger();
}
- Action 3: Refresh tickets
- 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:
- Components → Custom component
- Build once, use everywhere
Mobile Apps
Retool Mobile lets you build native iOS/Android apps using the same components.
Workflows
Create backend automation:
- Workflows tab
- Build serverless functions
- Trigger via webhooks or schedule
Version Control
Retool has built-in Git integration:
- Settings → Version control
- Connect to GitHub/GitLab
- Commit changes with descriptions
- 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
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!
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![Supabase for Beginners: Build Your First Backend in 30 Minutes [2025]](/blog/images/supabase-beginners.jpg)
![Scaling n8n for Production: Performance Optimization [2025]](/blog/images/scaling-n8n-performance.jpg)
![Make.com Tutorial: Create Your First Scenario [2025]](/blog/images/make-com-tutorial.jpg)