Retool
Build internal tools with Retool using queries, components, transformers, and workflows.
You are an expert in Retool, building internal tools with SQL/API queries, drag-and-drop components, JavaScript transformers, and Retool Workflows. You design maintainable admin panels, dashboards, and operational tools.
## Key Points
1. **Writing raw SQL with unparameterized user input** - Always use `{{ }}` template bindings which Retool parameterizes automatically. Never concatenate strings in SQL queries.
2. **Triggering all queries on page load** - Mark queries as "manual" if they depend on user selection. Only auto-run queries that populate initial page data.
3. **Using temporary state for everything** - Prefer query results and component values as the source of truth. Temporary state should only hold UI-specific state not derivable from queries.
4. **Building one monolithic app** - Split complex tools into modules or separate apps linked via URL params. Large apps become slow and hard to maintain.
- Building admin panels for customer support, order management, or user operations
- Creating dashboards that combine data from multiple databases and REST APIs
- Rapid prototyping of internal tools that would take weeks to build from scratch
- Non-frontend engineers need to build data-driven tools without learning React
- Operational workflows requiring human-in-the-loop approval steps with audit trails
## Quick Example
```typescript
// Table1 settings:
// - Data source: {{ getUsers.data }}
// - Editable columns: name, email, role
// - Save button onClick: updateUser.trigger().then(() => getUsers.trigger())
// - Pagination: server-side, total: {{ countUsers.data[0].total }}
```skilldb get automation-workflow-services-skills/RetoolFull skill: 212 linesRetool Internal Tools
You are an expert in Retool, building internal tools with SQL/API queries, drag-and-drop components, JavaScript transformers, and Retool Workflows. You design maintainable admin panels, dashboards, and operational tools.
Core Philosophy
Query-First Design
Start every Retool app by defining the data queries. Components bind to query results. Design your SQL queries and API calls first, then build the UI around the data shape. This prevents rework when data requirements change.
Reactive Binding Over Imperative Code
Retool components update reactively when their bound queries or state change. Use {{ }} bindings in component properties instead of writing imperative event handlers to set values. Reserve JavaScript for transformations and complex logic.
Least Privilege Resource Access
Each Retool resource connection should use database credentials scoped to the minimum required permissions. Read-only dashboards should connect via read-only database users. Write operations should use separate resources with audit logging.
Setup
// Retool API for programmatic management
const RETOOL_API = "https://your-org.retool.com/api/v2";
const headers = {
Authorization: `Bearer ${process.env.RETOOL_API_KEY}`,
"Content-Type": "application/json",
};
// List all apps
const response = await fetch(`${RETOOL_API}/apps`, { headers });
const { data: apps } = await response.json();
// Create a new folder
await fetch(`${RETOOL_API}/folders`, {
method: "POST",
headers,
body: JSON.stringify({ name: "Admin Tools", parentFolderId: null }),
});
-- Resource configuration: PostgreSQL example
-- Host: db.internal.example.com
-- Port: 5432
-- Database: operations
-- User: retool_readonly (for read queries)
-- SSL: Required
Key Patterns
Bind Components to Query Data
// Do: Use reactive bindings in component properties
// Table component data source: {{ getOrders.data }}
// Text component value: {{ `Total: ${getOrders.data.length} orders` }}
// Select component options: {{ getStatuses.data.map(s => ({ label: s.name, value: s.id })) }}
// Don't: Use event handlers to manually set table data after query runs
// Avoid: query1.trigger() then table1.setData(query1.data) in a script
Use Transformers for Data Shaping
// Do: Create a transformer for complex data manipulation
// Transformer: enrichedOrders
const orders = {{ getOrders.data }};
const users = {{ getUsers.data }};
const userMap = new Map(users.map(u => [u.id, u]));
return orders.map(order => ({
...order,
customerName: userMap.get(order.userId)?.name ?? "Unknown",
totalFormatted: `$${order.total.toFixed(2)}`,
isOverdue: new Date(order.dueDate) < new Date(),
}));
// Bind table to: {{ enrichedOrders.value }}
// Don't: Put heavy transformation logic directly in component bindings
Implement Confirmation for Destructive Actions
// Do: Use modal + confirmation for write operations
// Button "Delete Order" onClick:
// 1. modal1.open()
// Modal "Confirm Delete" confirm button onClick:
// 1. deleteOrder.trigger({ additionalScope: { orderId: table1.selectedRow.id } })
// 2. modal1.close()
// 3. getOrders.trigger() // refresh data
// Don't: Wire delete buttons directly to destructive queries without confirmation
Common Patterns
CRUD Table with Inline Editing
-- Query: getUsers (SQL)
SELECT id, name, email, role, created_at
FROM users
WHERE name ILIKE {{ '%' + searchInput.value + '%' }}
ORDER BY created_at DESC
LIMIT {{ pagination1.pageSize }}
OFFSET {{ (pagination1.currentPage - 1) * pagination1.pageSize }};
-- Query: updateUser (SQL, triggered manually)
UPDATE users
SET name = {{ table1.changesetObject.name }},
email = {{ table1.changesetObject.email }},
role = {{ table1.changesetObject.role }}
WHERE id = {{ table1.selectedRow.id }};
-- Query: countUsers (SQL, for pagination)
SELECT COUNT(*) as total FROM users
WHERE name ILIKE {{ '%' + searchInput.value + '%' }};
// Table1 settings:
// - Data source: {{ getUsers.data }}
// - Editable columns: name, email, role
// - Save button onClick: updateUser.trigger().then(() => getUsers.trigger())
// - Pagination: server-side, total: {{ countUsers.data[0].total }}
REST API Query with Error Handling
// Query: createTicket (REST API resource)
// Method: POST
// URL: /api/tickets
// Body:
{
"title": {{ titleInput.value }},
"priority": {{ prioritySelect.value }},
"assignee": {{ assigneeSelect.value }},
"description": {{ descriptionEditor.value }}
}
// Event handlers:
// onSuccess:
// utils.showNotification({ title: "Success", description: "Ticket created" });
// getTickets.trigger();
// titleInput.setValue("");
// onFailure:
// utils.showNotification({ title: "Error", description: createTicket.error, notificationType: "error" });
Retool Workflow for Background Processing
// Retool Workflow: processRefunds
// Trigger: Webhook or scheduled
// Step 1: Query - getPendingRefunds
// SELECT * FROM refunds WHERE status = 'pending' LIMIT 50;
// Step 2: Loop block over {{ getPendingRefunds.data }}
// Step 2a: REST API - processRefund
// POST https://payments.example.com/refunds
// { "chargeId": {{ value.charge_id }}, "amount": {{ value.amount }} }
//
// Step 2b: Query - markProcessed
// UPDATE refunds SET status = 'processed' WHERE id = {{ value.id }};
// Step 3: Code block - summary
return {
processed: {{ loop1.data.length }},
timestamp: new Date().toISOString(),
};
// Step 4: Slack notification with summary
Dynamic Form with Conditional Fields
// Form component settings:
// - typeSelect: options ["bug", "feature", "question"]
//
// Conditional visibility using component properties:
// severitySelect.hidden: {{ typeSelect.value !== "bug" }}
// timelineInput.hidden: {{ typeSelect.value !== "feature" }}
//
// Form submit handler:
// submitTicket.trigger({
// additionalScope: {
// type: typeSelect.value,
// severity: typeSelect.value === "bug" ? severitySelect.value : null,
// timeline: typeSelect.value === "feature" ? timelineInput.value : null,
// }
// });
Anti-Patterns
- Writing raw SQL with unparameterized user input - Always use
{{ }}template bindings which Retool parameterizes automatically. Never concatenate strings in SQL queries. - Triggering all queries on page load - Mark queries as "manual" if they depend on user selection. Only auto-run queries that populate initial page data.
- Using temporary state for everything - Prefer query results and component values as the source of truth. Temporary state should only hold UI-specific state not derivable from queries.
- Building one monolithic app - Split complex tools into modules or separate apps linked via URL params. Large apps become slow and hard to maintain.
When to Use
- Building admin panels for customer support, order management, or user operations
- Creating dashboards that combine data from multiple databases and REST APIs
- Rapid prototyping of internal tools that would take weeks to build from scratch
- Non-frontend engineers need to build data-driven tools without learning React
- Operational workflows requiring human-in-the-loop approval steps with audit trails
Install this skill directly: skilldb add automation-workflow-services-skills
Related Skills
Make Integromat
Build and manage Make (formerly Integromat) scenarios using modules, routers, webhooks, and data stores.
N8n
Build self-hosted and cloud workflow automations with n8n using nodes, expressions, webhooks, and code nodes.
Pipedream
Build serverless event-driven workflows with Pipedream using triggers, Node.js/Python steps, and data stores.
Superblocks
Build internal tools and workflows with Superblocks using API integrations, UI components, scheduled jobs, and permissions.
Val Town
Create serverless functions with Val Town using vals for HTTP handlers, cron jobs, email handlers, and SQLite storage.
Windmill
Build scripts, flows, and apps with Windmill using TypeScript and Python runtimes.