Connect Retool to ClickUp by creating a REST API Resource with ClickUp's base URL and personal API token. Once configured, build queries to fetch tasks across spaces, lists, and folders, then bind results to Retool Table and Form components for a fully custom task management panel that goes beyond ClickUp's native views.
| Fact | Value |
|---|---|
| Tool | ClickUp |
| Category | Productivity |
| Method | REST API Resource |
| Difficulty | Intermediate |
| Time required | 25 minutes |
| Last updated | April 2026 |
Build a Custom ClickUp Task Dashboard in Retool
ClickUp's native interface is powerful but opinionated. For operations teams managing multiple projects, the default views often require too many clicks to surface cross-list insights — which tasks are overdue across all spaces, which team members are overloaded, or how many tasks are blocked across the entire workspace. Retool lets you build a single dashboard that aggregates this data exactly the way your team needs it.
ClickUp exposes a comprehensive REST API v2 covering workspaces, spaces, folders, lists, tasks, time tracking, comments, and custom fields. Authentication uses a personal API token (for individual use) or OAuth 2.0 (for apps shared across accounts). For most Retool use cases — internal tools used by a fixed team — a personal token is the simplest and most reliable approach. The token goes into an Authorization header, and Retool's server-side proxy handles all requests.
The main architectural challenge with ClickUp is its deep hierarchy. Before you can query tasks, you often need to traverse workspace → space → folder → list to get the list IDs you want to filter on. Retool handles this elegantly through chained queries and dropdown selectors — build one query for spaces, bind it to a Select component, then use the selected space ID to populate a list query, and finally use the selected list ID to fetch tasks.
Integration method
ClickUp connects to Retool through a REST API Resource using ClickUp's personal API token for authentication. All requests are proxied server-side through Retool, so your API token never reaches the browser. You configure the base URL and auth header once in the Resource, then build individual queries per endpoint for tasks, lists, spaces, and assignees.
Prerequisites
- A ClickUp account with at least one workspace, space, and list containing tasks
- A ClickUp personal API token (Settings → Apps → API Token in ClickUp)
- Your ClickUp Team ID (visible in the ClickUp URL or via the /team endpoint)
- A Retool account (Cloud or self-hosted) with permission to create Resources
- Basic familiarity with Retool's query editor and component bindings
Step-by-step guide
Generate a ClickUp personal API token
Before configuring Retool, you need a ClickUp personal API token. Log in to ClickUp and click your avatar in the bottom-left corner to open User Settings. In the Settings page, navigate to the Apps section in the left sidebar. Under the API Token section, click Generate to create a new token. Copy the token immediately — it is only shown once. If you already have a token, you can use the existing one or regenerate it (note that regenerating invalidates the old token). For team-wide Retool tools where multiple users will share the same ClickUp connection, you may want to generate the token under a dedicated service account (a ClickUp user created specifically for integrations) rather than a personal account. This way the token remains valid even if an individual user leaves the organization. Store the token securely — you will add it to Retool's Configuration Variables in the next steps. Note: Personal API tokens provide access scoped to the user who generated them. If you need access limited to specific workspaces or scopes, consider ClickUp's OAuth 2.0 flow instead, which allows specifying permission scopes. For most internal Retool tools, the personal token is sufficient.
Pro tip: Keep your team ID handy. You can find it by navigating to any ClickUp list and looking at the URL: app.clickup.com/{teamId}/...
Expected result: You have a ClickUp personal API token copied to your clipboard, ready to add to Retool.
Create the ClickUp REST API Resource in Retool
In your Retool dashboard, click the Resources tab in the left navigation. Click the Add Resource button (top-right), then search for or select REST API from the resource type list. This opens the REST API resource configuration form. In the Name field, enter something descriptive like ClickUp API. In the Base URL field, enter: https://api.clickup.com/api/v2 This is ClickUp's API v2 base URL — all query paths you write later will be relative to this base. Now configure authentication: scroll down to the Headers section and click Add Header. In the key field, enter Authorization. In the value field, enter your personal API token directly (e.g., pk_12345678_ABCDEFGHIJKLMNOPQRSTUVWXYZ). ClickUp's v2 API uses the token directly as a Bearer value without a prefix. If you prefer to store the token as a configuration variable (recommended for security), first go to Settings → Configuration Variables, create a variable named CLICKUP_API_TOKEN with your token as the value, and mark it as a secret. Then in the header value field, use the expression {{ retoolContext.configVars.CLICKUP_API_TOKEN }}. Leave all other settings at their defaults. Click Create Resource to save. Retool will test the connection and confirm the resource was created successfully.
Pro tip: If your ClickUp token starts with 'pk_', you're using a personal token. OAuth tokens look different and require a 'Bearer' prefix. Personal tokens work directly without any prefix in the Authorization header.
Expected result: The ClickUp API resource appears in your Resources list with a green Connected status.
Fetch your workspace spaces and lists
Before querying tasks, you need to navigate ClickUp's hierarchy to get the IDs of the spaces and lists you want to work with. In your Retool app, open the query editor (Code panel) and create a new query named getSpaces. Select the ClickUp API resource. Set the method to GET and the path to /team/YOUR_TEAM_ID/space. Replace YOUR_TEAM_ID with your actual ClickUp team ID, or make it dynamic: /team/{{ textInput_teamId.value }}/space. Add a query parameter archived=false to exclude archived spaces. Run the query — you should see a JSON response with an array of spaces, each containing an id, name, and statuses array. Note the space IDs for the ones you want to include in your dashboard. Next, create a second query named getLists. Set the path to /space/{{ select_space.value }}/list?archived=false. This fetches all lists directly under a space (not nested in folders). If you also need lists inside folders, create a third query for /space/{{ select_space.value }}/folder?archived=false to get folders, then /folder/{{ select_folder.value }}/list for folder-based lists. Bind the getSpaces query to a Select component (select_space) by setting its data to {{ getSpaces.data.spaces }} and its mapped value/label to id and name respectively. When the user picks a space, getLists automatically re-runs because it references {{ select_space.value }}.
1// Transformer to flatten spaces for a Select component2const spaces = data.spaces || [];3return spaces.map(space => ({4 label: space.name,5 value: space.id,6 color: space.color || '#cccccc',7 memberCount: space.members ? space.members.length : 08}));Pro tip: Run the /team endpoint first to discover your team ID: GET /team will return all teams your token has access to.
Expected result: The getSpaces query returns your ClickUp spaces. The Select component populates with space names and selecting one triggers getLists automatically.
Query tasks from a selected list
With your list ID available from the previous step, create the main task query. Name it getTasks. Select the ClickUp API resource, set method to GET, and path to /list/{{ select_list.value }}/task. ClickUp's task list endpoint supports rich filtering via query parameters. Add the following query params to your Retool query: - page: {{ pagination.offset / pagination.pageSize }} (for pagination, ClickUp returns 100 tasks per page) - order_by: due_date - reverse: true (newest due dates first) - include_closed: {{ toggle_showClosed.value }} - assignees[]: {{ multiselect_assignees.value?.join(',') }} (filter by assignee) - statuses[]: {{ multiselect_status.value?.join(',') }} (filter by status) - due_date_gt: {{ dateRange.start ? new Date(dateRange.start).getTime() : '' }} - due_date_lt: {{ dateRange.end ? new Date(dateRange.end).getTime() : '' }} Note that ClickUp timestamps are in milliseconds since epoch. The date range picker returns ISO strings, so the transformer above converts them. Once the query runs, add a transformer to flatten the nested task data for Retool's Table component. The raw response has a tasks array, each task containing nested assignees (as an array of user objects), status (as an object with status and color), and custom fields (as an array). The transformer below reshapes this into flat rows.
1// Transformer to flatten ClickUp task data for a Retool Table2const tasks = data.tasks || [];3return tasks.map(task => ({4 id: task.id,5 name: task.name,6 status: task.status?.status || 'unknown',7 status_color: task.status?.color || '#cccccc',8 priority: task.priority?.priority || 'none',9 assignees: task.assignees?.map(a => a.username).join(', ') || 'Unassigned',10 due_date: task.due_date ? new Date(parseInt(task.due_date)).toLocaleDateString() : 'No due date',11 date_created: new Date(parseInt(task.date_created)).toLocaleDateString(),12 url: task.url,13 list_name: task.list?.name || '',14 space_name: task.space?.name || '',15 time_estimate: task.time_estimate ? `${Math.round(task.time_estimate / 3600000)}h` : ''16}));Pro tip: ClickUp's API uses millisecond epoch timestamps (13 digits). Always use parseInt() before passing to new Date().
Expected result: The getTasks query returns a flat array of task objects ready to display in a Retool Table component.
Build the task management UI and wire up actions
Now build the Retool app UI to display and interact with tasks. Drag a Table component onto the canvas and set its data source to {{ getTasks.data }}. The columns will auto-populate from the transformer output. Configure the Table to show: task name, status (with a color indicator), priority, assignees, due date, and a link to the ClickUp task URL. For task updates, add a second panel (use a Container component) to the right of the Table. Inside it, place: a TextInput for task name, a Select for status (options sourced from the list's available statuses), a Select for priority (Urgent/High/Normal/Low), a DatePicker for due date, and a MultiSelect for assignees. Pre-populate these fields with {{ table1.selectedRow.data }} so they show the currently selected task's values. Create an updateTask query: method PUT, path /task/{{ table1.selectedRow.data.id }}, body type JSON. The JSON body should be: { "name": "{{ textInput_name.value }}", "status": "{{ select_status.value }}", "priority": {{ select_priority.value }}, "due_date": "{{ datePicker_due.value ? new Date(datePicker_due.value).getTime() : null }}" } Set updateTask's On Success event handler to trigger getTasks (refresh the table). Add a Save button that triggers updateTask. For reassigning tasks, use the assignees endpoint: PUT /task/{taskId}/member with the new user ID. For team-wide Retool tools, RapidDev's team can help architect multi-resource apps that combine ClickUp data with your internal database for full project tracking.
1{2 "name": "{{ textInput_name.value }}",3 "status": "{{ select_status.value }}",4 "priority": {{ [null, 'urgent', 'high', 'normal', 'low'].indexOf(select_priority.value) }},5 "due_date": "{{ datePicker_due.value ? new Date(datePicker_due.value).getTime().toString() : null }}",6 "assignees": {7 "add": {{ JSON.stringify(multiselect_assignees.value || []) }},8 "rem": []9 }10}Pro tip: ClickUp priority is a number 1-4 (1=urgent, 2=high, 3=normal, 4=low) in the PUT request, but a string in the GET response. Keep a mapping in a transformer.
Expected result: A functional task dashboard showing ClickUp tasks in a Table. Clicking a row populates the edit panel; clicking Save updates the task in ClickUp and refreshes the Table.
Add pagination and a task creation form
ClickUp's task list endpoint returns a maximum of 100 tasks per page. For lists with many tasks, implement pagination. In the getTasks query, set the page parameter to {{ Math.floor(table1.pagination.offset / 100) }}. In the Table component settings, enable server-side pagination, set the page size to 100, and configure the pagination to use table1.pagination. For task creation, add a Modal component triggered by a New Task button. Inside the modal, place: a TextInput for task name (required), a TextArea for description, a Select for status, a DatePicker for due date, a MultiSelect for assignees (data: list of team members from a getTeamMembers query), and a Number Input for estimated time in hours. Create a createTask query: method POST, path /list/{{ select_list.value }}/task, body type JSON with the task fields. The description field accepts markdown. On success, close the modal (use a Close Modal event handler action), trigger getTasks to refresh, and show a success notification. For time estimates, ClickUp expects milliseconds: multiply hours × 3600000. For custom fields, the body should include a custom_fields array with id/value pairs — you can fetch custom field definitions via GET /list/{listId}/field.
1// createTask query body2{3 "name": "{{ textInput_newName.value }}",4 "description": "{{ textArea_desc.value }}",5 "status": "{{ select_newStatus.value || 'Open' }}",6 "priority": {{ select_newPriority.value || 3 }},7 "due_date": {{ datePicker_newDue.value ? new Date(datePicker_newDue.value).getTime() : 'null' }},8 "time_estimate": {{ numberInput_hours.value ? numberInput_hours.value * 3600000 : 'null' }},9 "assignees": {{ JSON.stringify(multiselect_newAssignees.value || []) }}10}Pro tip: Set the createTask query to run on manual trigger only (disable Run query automatically) so it only fires when the user explicitly clicks Create.
Expected result: The app supports creating new ClickUp tasks from Retool. Submitting the form creates the task in ClickUp, closes the modal, and the new task appears in the Table.
Common use cases
Build a cross-workspace task status dashboard
Aggregate tasks from multiple ClickUp lists and spaces into a single Retool Table with filters for status, assignee, priority, and due date. Operations managers can see all overdue or blocked tasks across the entire workspace without switching between ClickUp views.
Build a Retool dashboard with a Table showing all tasks from a selected ClickUp list. Include filter dropdowns for status (Open, In Progress, Done), priority (Urgent, High, Normal), and assignee. Add a Date Range picker to filter by due date. Include a Refresh button and show task count in a stat component.
Copy this prompt to try it in Retool
Build a task bulk-update panel for sprint management
Build a Retool panel where engineering managers can select multiple tasks from a Table and bulk-update their status, assignee, or due date. This replaces tedious one-by-one updates in ClickUp when re-assigning work or closing out a sprint.
Build a Retool tool with a multiselect Table of ClickUp tasks. Add a form panel on the right with a Status dropdown, Assignee dropdown, and Due Date picker. When the user selects rows and clicks Apply, update all selected tasks via PUT /task/{taskId} using a loop query. Show a success notification when done.
Copy this prompt to try it in Retool
Build a time tracking summary report
Pull ClickUp time tracking data for a date range and aggregate it by team member and project. Display total hours tracked, billable vs non-billable breakdown, and a bar chart comparing time across spaces — all updated in real time from Retool.
Build a Retool time tracking report that queries ClickUp's /team/{teamId}/time_entries endpoint filtered by date range. Show a Table with assignee, task name, time logged, and billable flag. Add a Chart component showing total hours per space. Include a CSV export button.
Copy this prompt to try it in Retool
Troubleshooting
401 Unauthorized error when running any ClickUp query
Cause: The Authorization header value is incorrect. ClickUp's v2 API uses the personal token directly — no 'Bearer' prefix is needed for personal tokens. OAuth tokens do require 'Bearer'.
Solution: Check the Resource configuration. Open Resources → ClickUp API → Edit. Verify the Authorization header value matches your token exactly. Personal tokens start with 'pk_' and are used directly. If you're using a config variable, verify Settings → Configuration Variables shows the correct token value without extra spaces.
Tasks query returns empty array even though the list has tasks
Cause: The list ID being passed to the query is incorrect, or the query is running before the list selector has a value. ClickUp also hides closed tasks by default.
Solution: Add a console log to verify the list ID: in the query editor, check the query preview to see the actual URL being called. If using include_closed: false and all your tasks are in a closed state, toggle include_closed to true. Also check that the list selector component has a value before the query runs by setting the getTasks query to run only when {{ select_list.value }} is truthy.
1// Add this condition in query settings → Run this query automatically when inputs change2// Only run when list is selected:3{{ !!select_list.value }}Task update fails with 400 Bad Request: 'Invalid due date'
Cause: ClickUp expects due_date as a millisecond epoch timestamp string, but the DatePicker component returns an ISO 8601 string or a Date object.
Solution: Convert the DatePicker value to a millisecond timestamp before sending. Wrap the value in new Date().getTime() and convert to a string. Also ensure you are not sending null as a string 'null' — use the ternary to omit the field entirely if no date is selected.
1// In the updateTask body, use:2"due_date": "{{ datePicker_due.value ? new Date(datePicker_due.value).getTime().toString() : '' }}"Rate limit errors (429 Too Many Requests) when loading large workspaces
Cause: ClickUp's API enforces rate limits of 100 requests per minute per token for personal tokens. Loading tasks across many lists with multiple queries can quickly hit this limit.
Solution: Implement lazy loading — only fetch tasks for the currently selected list rather than all lists at once. Add debounce to filters (500ms) so rapid filter changes don't flood requests. For bulk operations across many lists, use Retool Workflows with built-in retry and throttle settings instead of chained app queries.
Best practices
- Use a dedicated ClickUp service account for the API token instead of a personal account, so the integration remains active even when individual team members leave.
- Store the ClickUp API token in Retool Configuration Variables (Settings → Configuration Variables) marked as a secret, not hardcoded in the resource header value.
- Implement lazy loading: only fetch tasks for the selected list/space rather than trying to load all workspace tasks at once. This avoids rate limits and improves performance.
- Add the archived=false query parameter to all space, folder, and list queries to avoid cluttering your dropdowns with archived projects.
- Use ClickUp's include_closed=false parameter by default, and let users toggle it on with a checkbox when they need to see completed tasks.
- For bulk task updates (changing status across 10+ tasks), use Retool Workflows with Loop blocks and configurable retry settings instead of in-app Promise.all() patterns.
- Cache the spaces and lists queries with a 5-minute cache duration since project hierarchy changes infrequently — this reduces API calls significantly.
- When displaying task URLs from ClickUp, use the url field returned by the API as a direct link — don't try to construct ClickUp URLs manually as the format varies.
Alternatives
Monday.com uses a GraphQL API with a flat board structure, while ClickUp uses REST with a multi-level hierarchy — choose Monday if your team prefers boards over nested lists.
Asana has a more straightforward REST API with tasks and projects, making it easier to query and update than ClickUp's deeper hierarchy.
Jira is better suited for software engineering teams using sprints, epics, and story points, while ClickUp is more flexible for cross-functional operations teams.
Frequently asked questions
Does Retool have a native ClickUp connector?
No, Retool does not have a dedicated native connector for ClickUp. You connect via a REST API Resource using ClickUp's v2 API. This gives you full access to all ClickUp endpoints — tasks, spaces, folders, lists, time tracking, comments, and custom fields — with the same server-side proxy security as native connectors.
Can I use ClickUp's OAuth 2.0 instead of a personal API token?
Yes. ClickUp supports OAuth 2.0 for applications where multiple users authenticate with their own ClickUp accounts. Configure Retool's OAuth 2.0 authentication in the Resource settings with ClickUp's authorization URL (https://app.clickup.com/api), token URL (https://api.clickup.com/api/v2/oauth/token), your app's client ID and secret, and the required scopes. The OAuth flow is required if you're building a multi-tenant tool where each user accesses their own ClickUp workspace.
How do I handle ClickUp's custom fields in Retool?
Custom fields are returned as an array in each task response with id, name, type, and value properties. Fetch the list's custom field definitions via GET /list/{listId}/field to get field names and types. In your transformer, map each custom field by ID to its value. For updating custom fields, include a custom_fields array in the PUT /task/{taskId} request body with objects containing the field id and value.
Can I query tasks across multiple ClickUp lists at once?
ClickUp's standard API fetches tasks per-list. To aggregate across multiple lists, run parallel queries using Retool's JavaScript query with Promise.all(), then merge the results with a transformer. Alternatively, use ClickUp's 'View' API endpoint (GET /view/{viewId}/task) if you have saved Everything views in ClickUp that span multiple lists. For large-scale aggregation, Retool Workflows are more robust than in-app parallel queries.
Why do my date filters not return any tasks?
ClickUp's date filtering parameters (due_date_gt, due_date_lt, date_created_gt) require millisecond epoch timestamps, not ISO date strings. Ensure you convert your DatePicker values using new Date(isoString).getTime() before passing them as query parameters. Also verify the timestamp has 13 digits — ClickUp uses milliseconds, not seconds.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation