Retool builds internal tools on any database or API — drag-and-drop UI with SQL and REST query wiring. Retool Workflows: triggered by schedule or webhook, execute sequences of code blocks, queries, and loops — POST https://api.retool.com/api/v2/workflows/{id}/startTrigger starts a workflow. Retool API: POST /api/v2/resources creates database/API connections, GET /api/v2/folders lists app collections. Custom components: npm create @retool/custom-component@latest, export a React component with Retool.useRetoolState({ name, initialValue }) for two-way state binding — build in React, register in the Custom Component Editor. Retool.useEventCallback("buttonClick", handler) triggers Retool queries from custom component events. REST API queries in Retool: method, URL, headers with {{ env.API_KEY }}, body with {{ JSON.stringify(form.data) }}, then use transformers for JSON.parse(data) cleanup. SQL queries: write standard SQL against connected databases — {{ table.selectedRow.id }} for dynamic params. Environment variables: Settings → Environments — add API_URL and API_KEY per env, reference as {{ env.API_URL }}. Retool DB: a built-in PostgreSQL instance at Settings → Retool DB — no separate provisioning. Retool.triggerQuery("my_query") from custom components. Claude Code generates Retool Workflow payloads, custom component builds, and REST API backends designed for Retool consumption.
CLAUDE.md for Retool
## Retool Integration
- Retool Workflows: REST trigger or schedule; POST to /api/v2/workflows/{id}/startTrigger with Bearer token
- Custom components: npm create @retool/custom-component — React component with Retool.useRetoolState hook
- State binding: const [value, setValue] = Retool.useRetoolState({ name: "selectedId", initialValue: null })
- Event: Retool.useEventCallback("submit", (payload) => /* triggers Retool query */)
- API from Claude Code: expose REST endpoints Retool can query — simple JSON in/out is easiest
- Auth: pass API keys via Retool environment variables {{ env.MY_API_KEY }} — never hardcode
- Transformers: standard JS in transformer blocks — return transformed data array for tables
Retool Workflow Trigger API
// lib/retool/workflows.ts — trigger Retool Workflows from Node.js
const RETOOL_API_TOKEN = process.env.RETOOL_API_TOKEN!
const RETOOL_BASE_URL = "https://api.retool.com/api/v2"
export type WorkflowTriggerPayload = Record<string, unknown>
export async function triggerWorkflow(
workflowId: string,
payload: WorkflowTriggerPayload = {},
): Promise<{ runId: string; status: string }> {
const res = await fetch(
`${RETOOL_BASE_URL}/workflows/${workflowId}/startTrigger`,
{
method: "POST",
headers: {
Authorization: `Bearer ${RETOOL_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ payload }),
},
)
if (!res.ok) {
throw new Error(`Retool workflow error ${res.status}: ${await res.text()}`)
}
return res.json()
}
/** Trigger a workflow and wait for completion (polling) */
export async function triggerAndWait(
workflowId: string,
payload: WorkflowTriggerPayload = {},
timeoutMs = 30_000,
): Promise<Record<string, unknown>> {
const { runId } = await triggerWorkflow(workflowId, payload)
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const statusRes = await fetch(
`${RETOOL_BASE_URL}/workflows/runs/${runId}`,
{ headers: { Authorization: `Bearer ${RETOOL_API_TOKEN}` } },
)
const run = await statusRes.json() as { status: string; output?: unknown }
if (run.status === "success") return run.output as Record<string, unknown>
if (run.status === "failed") throw new Error(`Workflow ${workflowId} failed`)
await new Promise((r) => setTimeout(r, 1500))
}
throw new Error(`Workflow ${workflowId} timed out after ${timeoutMs}ms`)
}
Retool-Friendly API Endpoints
// app/api/admin/users/route.ts — API designed for Retool consumption
// Retool expects: { data: [...], totalCount: number } for tables
import { NextResponse } from "next/server"
import { z } from "zod"
import { db } from "@/lib/db"
import { verifyRetoolToken } from "@/lib/auth/retool"
export async function GET(req: Request) {
if (!verifyRetoolToken(req)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const url = new URL(req.url)
const page = parseInt(url.searchParams.get("page") ?? "1")
const limit = parseInt(url.searchParams.get("limit") ?? "50")
const search = url.searchParams.get("search") ?? ""
const plan = url.searchParams.get("plan") ?? undefined
const where = {
...(search ? { OR: [{ email: { contains: search } }, { name: { contains: search } }] } : {}),
...(plan ? { plan } : {}),
}
const [users, total] = await Promise.all([
db.user.findMany({
where,
select: {
id: true,
email: true,
name: true,
plan: true,
createdAt: true,
_count: { select: { orders: true } },
},
orderBy: { createdAt: "desc" },
take: limit,
skip: (page - 1) * limit,
}),
db.user.count({ where }),
])
// Retool-friendly flat response for table components
return NextResponse.json({
data: users.map((u) => ({ ...u, orderCount: u._count.orders, createdAt: u.createdAt.toISOString() })),
totalCount: total,
page,
pageCount: Math.ceil(total / limit),
})
}
const UpdateSchema = z.object({ userId: z.string(), plan: z.enum(["free", "pro", "enterprise"]) })
export async function PATCH(req: Request) {
if (!verifyRetoolToken(req)) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { userId, plan } = UpdateSchema.parse(await req.json())
const user = await db.user.update({ where: { id: userId }, data: { plan } })
return NextResponse.json({ success: true, user })
}
Retool Auth Helper
// lib/auth/retool.ts — verify requests from Retool
const RETOOL_WEBHOOK_SECRET = process.env.RETOOL_WEBHOOK_SECRET!
export function verifyRetoolToken(req: Request): boolean {
const token = req.headers.get("x-retool-workflow-key")
?? req.headers.get("authorization")?.replace("Bearer ", "")
if (!token) return false
return token === RETOOL_WEBHOOK_SECRET
}
Custom Retool Component
// custom-components/src/UserBadge.tsx — custom React component for Retool
import Retool from "@tryretool/custom-component-support"
export const UserBadge: React.FC = () => {
const [userId, setUserId] = Retool.useRetoolState({ name: "userId", initialValue: "" })
const [userName, setName] = Retool.useRetoolState({ name: "userName", initialValue: "Unknown" })
const [plan, ] = Retool.useRetoolState({ name: "plan", initialValue: "free" })
const onUpgrade = Retool.useEventCallback("upgrade")
const planColors: Record<string, string> = {
free: "bg-gray-100 text-gray-600",
pro: "bg-indigo-100 text-indigo-700",
enterprise: "bg-amber-100 text-amber-700",
}
return (
<div className="flex items-center gap-3 p-3 rounded-lg border bg-white shadow-sm">
<div className="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center font-semibold text-indigo-700">
{userName.charAt(0).toUpperCase()}
</div>
<div className="flex-1">
<div className="font-medium text-gray-800">{userName}</div>
<div className="text-xs text-gray-400">{userId}</div>
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${planColors[plan] ?? planColors.free}`}>
{plan}
</span>
{plan === "free" && (
<button
onClick={onUpgrade}
className="px-3 py-1 text-xs bg-indigo-500 text-white rounded-lg hover:bg-indigo-600"
>
Upgrade
</button>
)}
</div>
)
}
For the Appsmith alternative when needing an open-source, self-hostable internal tool builder with a similar drag-and-drop interface, Appsmith’s broader widget library, Git-based version control of applications, and on-premise deployment — Appsmith is fully open-source and self-hostable while Retool is commercial with a free tier, and Retool generally has a more polished UI and larger component ecosystem, see the Appsmith guide. For the Metabase alternative when primarily needing self-service BI and analytics dashboards where business analysts explore data without writing code or building forms — Metabase is purpose-built for data visualization and exploration while Retool is built for internal operational tools where CRUD operations, forms, and workflow automation are the core features, see the Metabase guide. The Claude Skills 360 bundle includes Retool skill sets covering Workflow triggers, custom components, and Retool-friendly APIs. Start with the free tier to try internal tool generation.