Skip to content

Commit 3a63293

Browse files
authored
feat(clickup): ClickUp integration — 23 tools, OAuth + API-token auth, attachment upload (#5702)
* feat(clickup): add ClickUp integration with OAuth + API-token auth, 23 tools, block, and attachment upload - 23 tools covering tasks (create/get/update/delete/list/search), comments (create/get/update/delete), attachment upload, tags, members, custom fields, and the workspace/space/folder/list hierarchy - OAuth provider wiring (authorization-code flow, non-expiring tokens) plus clickup-service-account token-paste credential (personal pk_ API tokens), with a shared clickupAuthorizationHeader helper (pk_ tokens sent bare, OAuth tokens as Bearer) - File upload follows the internal-route pattern: contract-validated /api/tools/clickup/upload-attachment builds the multipart form and returns UserFiles - ClickUp block with per-operation subBlocks, canonical file param, BlockMeta templates/skills, and gradient brand icon - Generated integration docs page + hand-written service-account guide * fix(clickup): apply validation-audit fixes across tools, block, and upload route - Map documented task fields that were dropped: markdown_description, subtasks, watchers, custom_fields, time_spent, folder, space — making the include_subtasks / include_markdown_description options observable - Expand verified filters: assignees/tags/due-date ranges on get_tasks and search_tasks, include_closed on search_tasks; add due_date_time / start_date_time flags and update-task assignee add/remove - Guard update_comment against an empty body and require comment text in the block; prefer markdown_content over content on create_list and make markdown reachable for lists in the UI - Drop the unverified 'required' field from custom-field outputs; read both err and error keys from ClickUp error bodies; correct notify_all wording - Upload route: 100MB size cap, shared attachment mapper with full documented response fields (version, thumbnails), base-URL constant * fix(docs): restore clickup-service-account guide and shield it from doc generation The generator prunes integration pages it does not derive from blocks; add the hand-written ClickUp API-token guide to HANDWRITTEN_INTEGRATION_DOCS so regeneration cannot delete it. * fix(clickup): address review findings — dedupe catalog entries, config-time list parent validation, upload memory cap, unique icon gradient ids - Remove duplicated clickup entries in docs meta.json and integrations.json introduced by a double docs regeneration - Add a Location dropdown for Get Lists / Create List so the folder ID or space ID is conditionally required at configuration time instead of failing at run time - Pass the 100MB cap into downloadServableFileFromStorage so oversized files abort during download instead of after full buffering - Use useId()-derived SVG gradient ids for ClickUpIcon in both icon files * chore(clickup): format integrations.json entry per biome * improvement(clickup): final validation-pass refinements across tools and block - create_task: add doc-backed sprint points param (parity with update) - get_tasks/search_tasks: expose include_markdown_description - update_task legacy numeric priority in list responses mapped instead of dropped; create_comment omits absent response fields instead of emitting sentinel ''/0 values - order_by only sent when explicitly chosen (Default sentinel); comment text no longer UI-required for update_comment (resolve-only and assignee-only updates are valid per the tool contract, which still rejects an empty body) - add_tag_to_task sends no request body per docs; upload tool tolerates non-JSON error responses * fix(clickup): tolerate nested user wrapper in member mapping The task/list member endpoints document a flat member object; accept the workspace-members-style nested { user: {...} } wrapper as well so both shapes map correctly. * fix(clickup): map size-limit errors from download/compile to a 400 upload-size response downloadServableFileFromStorage enforces maxBytes on both the raw download and the resolved (compiled) artifact via PayloadSizeLimitError; catch it in the route so oversized content returns the intended 400 instead of bubbling to the generic 500 handler. * feat(clickup): add custom field values, checklists, and time tracking (15 tools, 38 total) - Set/remove custom field values on tasks (PUT/DELETE /task/{id}/field/{field_id}); block value input parses JSON for structured field types, plain values pass through - Checklist CRUD: create/rename/reorder/delete checklists and create/update/ delete checklist items (assign, resolve, nest), mapped from the documented {checklist} response shape - Time tracking: list entries in a date range (assignee/location filters, task-tag and location-name includes), create/update/delete entries, start/ stop timers, and read the currently running timer; entries mapped from the documented data envelope with negative-duration running semantics - Block gains 15 operations with conditionally-required fields, timestamp wand configs, tri-state billable/resolved dropdowns, and a single-location filter selector matching the API's one-location-filter rule * fix(clickup): new-tools audit fixes — POST for set custom field value, tolerant time-entry envelopes, richer mappings - Set Custom Field Value uses POST per the live reference OpenAPI (the llms mirror shows PUT; the reference console spec is authoritative) - delete_time_entry maps the documented array envelope; create_time_entry tolerates both data-wrapped and flat echo bodies - Time entries surface task_tags and task_location so the include switches are observable; checklists carry date_created - Custom field value input parses any JSON literal (numbers, booleans, arrays, objects) and passes plain text through - Update Time Entry supports duration edits; single-assignee time ops get their own field so a comma-separated list can't silently NaN out * fix(clickup): send explicit date-time flags whenever a date is set The due/start date-time switches previously only transmitted true; a timed date could never be flipped back to date-only. The flag is now sent as an explicit boolean whenever the corresponding date is provided and omitted otherwise. * improvement(clickup): final per-tool audit polish — checklist item children, tolerant comment date - Checklist items surface the documented children array of nested item IDs - create_comment tolerates a string-typed date in the response * fix(clickup): reject empty update_task bodies with a clear local error, matching sibling update tools
1 parent db9f165 commit 3a63293

63 files changed

Lines changed: 8698 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/icons.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,6 +2801,52 @@ export function LinqIcon(props: SVGProps<SVGSVGElement>) {
28012801
)
28022802
}
28032803

2804+
export function ClickUpIcon(props: SVGProps<SVGSVGElement>) {
2805+
const id = useId()
2806+
const bodyGradientId = `clickup_body_${id}`
2807+
const arrowGradientId = `clickup_arrow_${id}`
2808+
return (
2809+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 54.8 65.8' fill='none'>
2810+
<linearGradient
2811+
id={bodyGradientId}
2812+
gradientUnits='userSpaceOnUse'
2813+
x1='0'
2814+
y1='15.0492'
2815+
x2='54.8446'
2816+
y2='15.0492'
2817+
gradientTransform='matrix(1 0 0 -1 0 69.3604)'
2818+
>
2819+
<stop offset='0' stopColor='#8930FD' />
2820+
<stop offset='1' stopColor='#49CCF9' />
2821+
</linearGradient>
2822+
<linearGradient
2823+
id={arrowGradientId}
2824+
gradientUnits='userSpaceOnUse'
2825+
x1='1.1953'
2826+
y1='53.166'
2827+
x2='53.7447'
2828+
y2='53.166'
2829+
gradientTransform='matrix(1 0 0 -1 0 69.3604)'
2830+
>
2831+
<stop offset='0' stopColor='#FF02F0' />
2832+
<stop offset='1' stopColor='#FFC800' />
2833+
</linearGradient>
2834+
<path
2835+
fillRule='evenodd'
2836+
clipRule='evenodd'
2837+
fill={`url(#${bodyGradientId})`}
2838+
d='M0,50.6l10.1-7.8c5.4,7,11.1,10.3,17.4,10.3c6.3,0,11.9-3.2,17-10.2l10.3,7.6c-7.4,10-16.6,15.3-27.3,15.3C16.9,65.8,7.6,60.5,0,50.6z'
2839+
/>
2840+
<path
2841+
fillRule='evenodd'
2842+
clipRule='evenodd'
2843+
fill={`url(#${arrowGradientId})`}
2844+
d='M27.5,16.9l-18,15.5l-8.3-9.7L27.6,0l26.2,22.7l-8.4,9.6L27.5,16.9z'
2845+
/>
2846+
</svg>
2847+
)
2848+
}
2849+
28042850
export function LinearIcon(props: React.SVGProps<SVGSVGElement>) {
28052851
return (
28062852
<svg
@@ -3745,6 +3791,38 @@ export const SakanaIcon = (props: SVGProps<SVGSVGElement>) => (
37453791
</svg>
37463792
)
37473793

3794+
export const NvidiaIcon = (props: SVGProps<SVGSVGElement>) => (
3795+
<svg
3796+
{...props}
3797+
height='1em'
3798+
viewBox='30 28 280 172'
3799+
width='1em'
3800+
xmlns='http://www.w3.org/2000/svg'
3801+
>
3802+
<title>NVIDIA</title>
3803+
<path
3804+
d='M82.211,102.414c0,0,22.504-33.203,67.437-36.638V53.73c-49.769,3.997-92.867,46.149-92.867,46.149s24.41,70.565,92.867,77.026v-12.804C99.411,157.781,82.211,102.414,82.211,102.414z M149.648,138.637v11.726c-37.968-6.769-48.507-46.237-48.507-46.237s18.23-20.195,48.507-23.47v12.867c-0.023,0-0.039-0.007-0.058-0.007c-15.891-1.907-28.305,12.938-28.305,12.938S128.243,131.445,149.648,138.637 M149.648,31.512V53.73c1.461-0.112,2.922-0.207,4.391-0.257c56.582-1.907,93.449,46.406,93.449,46.406s-42.343,51.488-86.457,51.488c-4.043,0-7.828-0.375-11.383-1.005v13.739c3.04,0.386,6.192,0.613,9.481,0.613c41.051,0,70.738-20.965,99.484-45.778c4.766,3.817,24.278,13.103,28.289,17.168c-27.332,22.883-91.031,41.329-127.144,41.329c-3.481,0-6.824-0.211-10.11-0.528v19.306h156.032V31.512H149.648z M149.648,80.656V65.777c1.446-0.101,2.903-0.179,4.391-0.226c40.688-1.278,67.382,34.965,67.382,34.965s-28.832,40.043-59.746,40.043c-4.449,0-8.438-0.715-12.028-1.922V93.523c15.84,1.914,19.028,8.911,28.551,24.786l21.18-17.859c0,0-15.461-20.277-41.524-20.277C155.021,80.172,152.31,80.371,149.648,80.656'
3805+
fill='#77B900'
3806+
/>
3807+
</svg>
3808+
)
3809+
3810+
export const ZaiIcon = (props: SVGProps<SVGSVGElement>) => (
3811+
<svg {...props} height='1em' viewBox='0 0 30 30' width='1em' xmlns='http://www.w3.org/2000/svg'>
3812+
<title>Z.ai</title>
3813+
<path
3814+
d='M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03C28.51,26.72,26.72,28.51,24.51,28.51z'
3815+
fill='#2D2D2D'
3816+
/>
3817+
<path
3818+
d='M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z'
3819+
fill='#FFFFFF'
3820+
/>
3821+
<polygon fill='#FFFFFF' points='24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1' />
3822+
<path d='M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z' fill='#FFFFFF' />
3823+
</svg>
3824+
)
3825+
37483826
export function MetaIcon(props: SVGProps<SVGSVGElement>) {
37493827
const id = useId()
37503828
const gradient1Id = `meta_gradient_1_${id}`

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
ClayIcon,
3535
ClerkIcon,
3636
ClickHouseIcon,
37+
ClickUpIcon,
3738
CloudFormationIcon,
3839
CloudflareIcon,
3940
CloudWatchIcon,
@@ -273,6 +274,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
273274
clay: ClayIcon,
274275
clerk: ClerkIcon,
275276
clickhouse: ClickHouseIcon,
277+
clickup: ClickUpIcon,
276278
cloudflare: CloudflareIcon,
277279
cloudformation: CloudFormationIcon,
278280
cloudwatch: CloudWatchIcon,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: ClickUp API Tokens
3+
description: Connect ClickUp to Sim with a personal API token — ideally created from a dedicated service user for production workflows
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { Step, Steps } from 'fumadocs-ui/components/steps'
8+
import { Image } from '@/components/ui/image'
9+
import { FAQ } from '@/components/ui/faq'
10+
11+
ClickUp personal API tokens let your workflows authenticate without an OAuth consent flow. A token gives full parity with the ClickUp API — everything Sim's ClickUp blocks can do over OAuth works with a token.
12+
13+
Tokens are bound to the user who creates them: every action a workflow takes is attributed to that user, and the token stops working if the user is deactivated or removed from the workspace. For production workflows, create the token from a dedicated service user (e.g. `sim-bot@yourcompany.com`) rather than a personal account.
14+
15+
## Prerequisites
16+
17+
A ClickUp account with access to the workspaces your workflows need. Any user can generate a personal API token from their settings.
18+
19+
## Creating the API Token
20+
21+
<Steps>
22+
<Step>
23+
Log in as the service user, click your avatar in ClickUp, and open **Settings**
24+
25+
{/* TODO(screenshot): ClickUp avatar menu with Settings highlighted */}
26+
</Step>
27+
<Step>
28+
In the sidebar, go to **Apps** (labeled **API Token** in some plans)
29+
</Step>
30+
<Step>
31+
Click **Generate** to create your personal token
32+
33+
{/* TODO(screenshot): ClickUp Apps page with the Generate API token button visible */}
34+
</Step>
35+
<Step>
36+
Copy the token — it starts with `pk_` — and store it somewhere safe.
37+
</Step>
38+
</Steps>
39+
40+
<Callout type="warn">
41+
The API token carries the creating user's full access to every workspace they belong to. Treat it like a password — do not commit it to source control or share it publicly. Sim encrypts the token at rest.
42+
</Callout>
43+
44+
## Adding the API Token to Sim
45+
46+
<Steps>
47+
<Step>
48+
Open your workspace **Settings** and go to the **Integrations** tab
49+
</Step>
50+
<Step>
51+
Search for "ClickUp Service Account" and click it, then click **Add to Sim** and choose **Add API token**
52+
53+
{/* TODO(screenshot): Integrations page with "ClickUp Service Account" in the service list */}
54+
</Step>
55+
<Step>
56+
Paste the API token (`pk_...`) and optionally set a display name and description
57+
58+
{/* TODO(screenshot): Add ClickUp API token dialog with the token filled in */}
59+
</Step>
60+
<Step>
61+
Click **Add API token**. Sim verifies the token by fetching the authorized user from ClickUp — if it fails, you'll see a specific error explaining what went wrong.
62+
</Step>
63+
</Steps>
64+
65+
The token is encrypted before being stored.
66+
67+
## Using the Service Account in Workflows
68+
69+
Add a ClickUp block to your workflow. In the credential dropdown, your ClickUp service account appears alongside any OAuth credentials. Select it and configure the block as you normally would.
70+
71+
{/* TODO(screenshot): ClickUp block in a workflow with the service account selected as the credential */}
72+
73+
The block calls the ClickUp API (`api.clickup.com`) with the token. Everything the workflow does — creating tasks, adding comments, uploading attachments — is attributed to the user who created the token.
74+
75+
<FAQ items={[
76+
{ question: "Does ClickUp have real service accounts?", answer: "Not for API tokens — every token is tied to a user. The closest equivalent is creating a dedicated service user in your workspace and generating the token from that account, so workflows aren't attributed to (or broken by) a real person's account." },
77+
{ question: "Does the token expire?", answer: "No — personal API tokens are long-lived and don't auto-rotate. They stay valid until regenerated, or until the user who created them is deactivated or removed from the workspace." },
78+
{ question: "What happens if the token's user leaves the company?", answer: "Tokens are user-bound, so deactivating the user kills the token and every workflow using it starts failing with authentication errors. This is exactly why production tokens should come from a dedicated service user." },
79+
{ question: "Which workspaces can the token reach?", answer: "Every workspace the creating user belongs to. Use the Get Workspaces operation to see what the token can access, and remember that removing the user from a workspace also removes the token's access to it." },
80+
{ question: "How do I rotate the token?", answer: "Regenerate the token from the same service user in ClickUp (regenerating invalidates the old one immediately), then update the credential in Sim with the new token." },
81+
]} />

0 commit comments

Comments
 (0)