Export ClickUp tasks to Google Sheets, including the closed tasks and subtasks left out by default and the timestamps that arrive as millisecond strings.
ClickUp's API is capable, and two defaults quietly ruin the first report people build with it: closed tasks and subtasks are both excluded unless you explicitly ask for them, so completed-work reporting comes back near empty. Add timestamps that arrive as millisecond strings and a four-level hierarchy you have to walk to find a list ID, and there is enough here to be worth spelling out.
The two-minute option: CSV export
Open a list or board view, use the view's ••• menu, and choose Export with CSV. The export reflects the current view, so its filters, grouping and visible columns all apply. Import into Sheets via File → Import.
ClickUp to Google Sheets for free (Apps Script + API v2)
Generate a personal API token under Settings → Apps → API Token. It begins pk_ and goes in the Authorization header directly, with no Bearer prefix, which trips up people used to other APIs.
Get the list ID from the list's URL. Then the important part: pass include_closed=true and subtasks=true, or your export silently omits most of the work you are trying to measure.
function importClickUpTasks() { const TOKEN = "pk_XXXXXXXXXXXX"; const LIST_ID = "901234567"; // No "Bearer" prefix; the token goes in the header as-is. const headers = { Authorization: TOKEN }; const rows = []; let page = 0; while (true) { const url = "https://api.clickup.com/api/v2/list/" + LIST_ID + "/task" + "?page=" + page + "&include_closed=true" // closed tasks are excluded by default + "&subtasks=true"; // subtasks are excluded by default too const res = JSON.parse( UrlFetchApp.fetch(url, { headers, muteHttpExceptions: true }) .getContentText() ); if (res.err) throw new Error(res.err); if (!res.tasks || !res.tasks.length) break; res.tasks.forEach((t) => { // Custom field values are typed; dropdowns give an index into // type_config.options rather than the label itself. const custom = (t.custom_fields || []) .filter((f) => f.value !== undefined && f.value !== null) .map((f) => { let v = f.value; if (f.type === "drop_down" && f.type_config && f.type_config.options) { const opt = f.type_config.options[v]; v = opt ? opt.name : v; } return f.name + ": " + v; }) .join("; "); rows.push([ t.name, t.status ? t.status.status : "", (t.assignees || []).map((a) => a.username).join(", "), t.priority ? t.priority.priority : "", msToDate(t.due_date), // epoch milliseconds, as a string msToDate(t.date_created), msToDate(t.date_closed), t.time_estimate ? t.time_estimate / 3600000 : "", // ms → hours custom, t.url, ]); }); // last_page tells you when to stop; pages are 100 tasks each. if (res.last_page) break; page++; } const header = ["Task","Status","Assignees","Priority","Due","Created", "Closed","Est. hours","Custom fields","URL"]; const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName("ClickUp") || ss.insertSheet("ClickUp"); sheet.clearContents(); sheet.getRange(1, 1, 1, header.length).setValues([header]); if (rows.length) { sheet.getRange(2, 1, rows.length, header.length).setValues(rows); }} // ClickUp sends epoch milliseconds as strings: "1721476800000".function msToDate(value) { if (!value) return ""; return new Date(Number(value));}Note time_estimate and the time-tracking fields are also in milliseconds, so dividing by 3,600,000 gives hours. Treating them as minutes or seconds is a common source of estimates that look absurd.
Limits: around 100 requests a minute per token on lower plans, and Apps Script's six-minute cap. Covering a whole workspace means walking spaces, folders and lists before you fetch any tasks, so target specific lists rather than crawling everything on each run.
The Brooked method
Brooked walks the hierarchy for you so you pick spaces and lists from a menu rather than hunting for IDs, includes closed tasks and subtasks by default, converts millisecond timestamps into real dates, and resolves dropdown custom fields to their labels. Several lists can import into one spreadsheet on a single schedule.
Step 1: Install Brooked
Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.
Step 2: Connect ClickUp
Choose ClickUp in Brooked and paste a personal API token from Settings → Apps. Pick the space and lists you want, decide whether to include closed tasks and subtasks, and import.
Step 3: Schedule the refresh
Set the import to re-run daily or weekly so sprint and throughput reports rebuild before the review rather than during it.
Method comparison
| Method | Cost | Setup | Refresh | Two-way | Best for |
|---|---|---|---|---|---|
| ClickUp CSV export | Free | 2 min | Manual | No | A one-off export of a single view |
| Apps Script + API v2 | Free | 25 to 35 min | Time-driven trigger | DIY | Several lists, custom fields |
| Zapier / Make | Free tier limited · paid for volume | 10 to 15 min | On task created or updated | DIY (second workflow) | Logging individual task events |
| Brooked | Free tier · Pro $29/user/mo | ~3 min | Scheduled (hourly, daily, weekly) | Yes | Scheduled reporting across spaces and lists |
Common use cases
- Sprint throughput: tasks closed per person per sprint across lists
- Estimate accuracy: compare time_estimate against tracked time to calibrate planning
- Portfolio status: roll several spaces into one leadership table
- Client reporting: filter to client-facing lists and share only that sheet
- Workload: open tasks per assignee to spot who is overloaded
Troubleshooting common issues
Related Articles
- How to Connect Monday.com to Google Sheets
- How to Connect Clockify to Google Sheets
- How to Connect Asana to Google Sheets
Frequently asked questions
Why are closed ClickUp tasks missing from my export?
The task endpoint excludes closed tasks by default, so any report about completed work comes back looking empty. Add include_closed=true to the request. Subtasks are excluded by default too and need subtasks=true, which together account for most reports where the numbers look far too low.
Why do ClickUp dates show as huge numbers?
ClickUp returns timestamps as Unix epoch milliseconds, and as strings rather than numbers. Convert with new Date(Number(value)) before writing to a sheet. Because they are strings, arithmetic on them silently concatenates rather than adding, which produces nonsense durations.
Where do I find my ClickUp list ID?
ClickUp nests everything as team, then space, then folder, then list, and the task endpoint needs the list ID specifically. The quickest route is the list URL, which contains it. Programmatically you walk the hierarchy: team, then spaces, then folders and folderless lists, which is several calls before you can request a single task.
Can I export ClickUp to Google Sheets for free?
Yes. ClickUp exports a view to CSV from the view menu on all plans, though export options are richer on paid tiers. The Apps Script method here uses a free personal API token and a free time-driven trigger. Brooked's free tier covers 100 imports a month.
How do custom fields come back?
As an array on each task, with each entry carrying the field name, its type, and a value whose shape depends on that type. Dropdowns return an option index or ID rather than the label, so you need the field's type_config options list to resolve the readable value. Labels and users come back as arrays of objects.
What are ClickUp's rate limits?
Roughly 100 requests per minute per token on Free and Unlimited plans, with higher ceilings on Business and Enterprise. Because the hierarchy walk plus per-list task requests adds up quickly on a large workspace, prefer filtered requests over pulling every list, and space out scheduled runs.
Connect ClickUp to Google Sheets today.
Free tier: 100 imports per month, no credit card required. Or see the full ClickUp integration details.
Install Brooked free →

