From 0f1822b36e499090bd6ce277bcbb8cd18d45d526 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Jul 2026 19:49:21 -0700 Subject: [PATCH 01/13] 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 --- apps/docs/components/icons.tsx | 75 ++ apps/docs/components/ui/icon-mapping.ts | 2 + .../integrations/clickup-service-account.mdx | 81 ++ .../content/docs/en/integrations/clickup.mdx | 479 +++++++++ .../content/docs/en/integrations/meta.json | 2 + .../tools/clickup/upload-attachment/route.ts | 110 ++ apps/sim/blocks/blocks/clickup.ts | 986 ++++++++++++++++++ apps/sim/blocks/registry-maps.ts | 3 + apps/sim/components/icons.tsx | 43 + apps/sim/lib/api/contracts/tools/clickup.ts | 49 + apps/sim/lib/api/contracts/tools/index.ts | 1 + apps/sim/lib/auth/auth.ts | 49 + apps/sim/lib/core/config/env.ts | 2 + .../token-service-accounts/descriptors.ts | 19 + .../token-service-accounts/server.ts | 3 + .../validators/clickup.ts | 73 ++ apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 116 +++ apps/sim/lib/oauth/oauth.ts | 30 + apps/sim/lib/oauth/types.ts | 2 + apps/sim/tools/clickup/add_tag_to_task.ts | 69 ++ apps/sim/tools/clickup/create_comment.ts | 106 ++ apps/sim/tools/clickup/create_folder.ts | 77 ++ apps/sim/tools/clickup/create_list.ts | 111 ++ apps/sim/tools/clickup/create_task.ts | 167 +++ apps/sim/tools/clickup/delete_comment.ts | 64 ++ apps/sim/tools/clickup/delete_task.ts | 61 ++ apps/sim/tools/clickup/get_comments.ts | 98 ++ apps/sim/tools/clickup/get_custom_fields.ts | 106 ++ apps/sim/tools/clickup/get_folders.ts | 88 ++ apps/sim/tools/clickup/get_list_members.ts | 76 ++ apps/sim/tools/clickup/get_lists.ts | 100 ++ apps/sim/tools/clickup/get_space_tags.ts | 82 ++ apps/sim/tools/clickup/get_spaces.ts | 105 ++ apps/sim/tools/clickup/get_task.ts | 92 ++ apps/sim/tools/clickup/get_task_members.ts | 76 ++ apps/sim/tools/clickup/get_tasks.ts | 133 +++ apps/sim/tools/clickup/get_workspaces.ts | 74 ++ apps/sim/tools/clickup/index.ts | 47 + .../sim/tools/clickup/remove_tag_from_task.ts | 69 ++ apps/sim/tools/clickup/search_tasks.ts | 149 +++ apps/sim/tools/clickup/shared.ts | 441 ++++++++ apps/sim/tools/clickup/types.ts | 430 ++++++++ apps/sim/tools/clickup/update_comment.ts | 91 ++ apps/sim/tools/clickup/update_task.ts | 151 +++ apps/sim/tools/clickup/upload_attachment.ts | 86 ++ apps/sim/tools/registry.ts | 48 + 47 files changed, 5324 insertions(+) create mode 100644 apps/docs/content/docs/en/integrations/clickup-service-account.mdx create mode 100644 apps/docs/content/docs/en/integrations/clickup.mdx create mode 100644 apps/sim/app/api/tools/clickup/upload-attachment/route.ts create mode 100644 apps/sim/blocks/blocks/clickup.ts create mode 100644 apps/sim/lib/api/contracts/tools/clickup.ts create mode 100644 apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts create mode 100644 apps/sim/tools/clickup/add_tag_to_task.ts create mode 100644 apps/sim/tools/clickup/create_comment.ts create mode 100644 apps/sim/tools/clickup/create_folder.ts create mode 100644 apps/sim/tools/clickup/create_list.ts create mode 100644 apps/sim/tools/clickup/create_task.ts create mode 100644 apps/sim/tools/clickup/delete_comment.ts create mode 100644 apps/sim/tools/clickup/delete_task.ts create mode 100644 apps/sim/tools/clickup/get_comments.ts create mode 100644 apps/sim/tools/clickup/get_custom_fields.ts create mode 100644 apps/sim/tools/clickup/get_folders.ts create mode 100644 apps/sim/tools/clickup/get_list_members.ts create mode 100644 apps/sim/tools/clickup/get_lists.ts create mode 100644 apps/sim/tools/clickup/get_space_tags.ts create mode 100644 apps/sim/tools/clickup/get_spaces.ts create mode 100644 apps/sim/tools/clickup/get_task.ts create mode 100644 apps/sim/tools/clickup/get_task_members.ts create mode 100644 apps/sim/tools/clickup/get_tasks.ts create mode 100644 apps/sim/tools/clickup/get_workspaces.ts create mode 100644 apps/sim/tools/clickup/index.ts create mode 100644 apps/sim/tools/clickup/remove_tag_from_task.ts create mode 100644 apps/sim/tools/clickup/search_tasks.ts create mode 100644 apps/sim/tools/clickup/shared.ts create mode 100644 apps/sim/tools/clickup/types.ts create mode 100644 apps/sim/tools/clickup/update_comment.ts create mode 100644 apps/sim/tools/clickup/update_task.ts create mode 100644 apps/sim/tools/clickup/upload_attachment.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index d18862c51e0..f0f336f9083 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2801,6 +2801,49 @@ export function LinqIcon(props: SVGProps) { ) } +export function ClickUpIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + ) +} + export function LinearIcon(props: React.SVGProps) { return ( ) => ( ) +export const NvidiaIcon = (props: SVGProps) => ( + + NVIDIA + + +) + +export const ZaiIcon = (props: SVGProps) => ( + + Z.ai + + + + + +) + export function MetaIcon(props: SVGProps) { const id = useId() const gradient1Id = `meta_gradient_1_${id}` diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index a8e939b23c1..65a7684ef0a 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -34,6 +34,7 @@ import { ClayIcon, ClerkIcon, ClickHouseIcon, + ClickUpIcon, CloudFormationIcon, CloudflareIcon, CloudWatchIcon, @@ -273,6 +274,7 @@ export const blockTypeToIconMap: Record = { clay: ClayIcon, clerk: ClerkIcon, clickhouse: ClickHouseIcon, + clickup: ClickUpIcon, cloudflare: CloudflareIcon, cloudformation: CloudFormationIcon, cloudwatch: CloudWatchIcon, diff --git a/apps/docs/content/docs/en/integrations/clickup-service-account.mdx b/apps/docs/content/docs/en/integrations/clickup-service-account.mdx new file mode 100644 index 00000000000..4d06900ecb4 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/clickup-service-account.mdx @@ -0,0 +1,81 @@ +--- +title: ClickUp API Tokens +description: Connect ClickUp to Sim with a personal API token — ideally created from a dedicated service user for production workflows +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Image } from '@/components/ui/image' +import { FAQ } from '@/components/ui/faq' + +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. + +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. + +## Prerequisites + +A ClickUp account with access to the workspaces your workflows need. Any user can generate a personal API token from their settings. + +## Creating the API Token + + + + Log in as the service user, click your avatar in ClickUp, and open **Settings** + + {/* TODO(screenshot): ClickUp avatar menu with Settings highlighted */} + + + In the sidebar, go to **Apps** (labeled **API Token** in some plans) + + + Click **Generate** to create your personal token + + {/* TODO(screenshot): ClickUp Apps page with the Generate API token button visible */} + + + Copy the token — it starts with `pk_` — and store it somewhere safe. + + + + +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. + + +## Adding the API Token to Sim + + + + Open your workspace **Settings** and go to the **Integrations** tab + + + Search for "ClickUp Service Account" and click it, then click **Add to Sim** and choose **Add API token** + + {/* TODO(screenshot): Integrations page with "ClickUp Service Account" in the service list */} + + + Paste the API token (`pk_...`) and optionally set a display name and description + + {/* TODO(screenshot): Add ClickUp API token dialog with the token filled in */} + + + 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. + + + +The token is encrypted before being stored. + +## Using the Service Account in Workflows + +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. + +{/* TODO(screenshot): ClickUp block in a workflow with the service account selected as the credential */} + +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. + + diff --git a/apps/docs/content/docs/en/integrations/clickup.mdx b/apps/docs/content/docs/en/integrations/clickup.mdx new file mode 100644 index 00000000000..5b75cbc33c2 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/clickup.mdx @@ -0,0 +1,479 @@ +--- +title: ClickUp +description: Interact with ClickUp +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate ClickUp into the workflow. Create, read, update, and delete tasks, manage comments, tags, folders, and lists, upload attachments, and look up workspaces, members, and custom fields. + + + +## Actions + +### `clickup_create_task` + +Create a new task in a ClickUp list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `listId` | string | Yes | ID of the list to create the task in | +| `name` | string | Yes | Name of the task | +| `description` | string | No | Plain text description of the task | +| `markdownContent` | string | No | Markdown description of the task \(overrides description\) | +| `status` | string | No | Status to create the task with \(must exist in the list\) | +| `priority` | number | No | Priority: 1 \(urgent\), 2 \(high\), 3 \(normal\), 4 \(low\) | +| `dueDate` | number | No | Due date as a Unix timestamp in milliseconds | +| `startDate` | number | No | Start date as a Unix timestamp in milliseconds | +| `assignees` | array | No | User IDs to assign to the task | +| `tags` | array | No | Tag names to apply to the task | +| `timeEstimate` | number | No | Time estimate in milliseconds | +| `parent` | string | No | Parent task ID to create this task as a subtask | +| `notifyAll` | boolean | No | Whether to notify the task creator on creation | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | The created task | + +### `clickup_get_task` + +Retrieve a task from ClickUp by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to retrieve | +| `includeSubtasks` | boolean | No | Include subtasks in the response | +| `includeMarkdownDescription` | boolean | No | Return the task description in Markdown format | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | The requested task | + +### `clickup_update_task` + +Update an existing task in ClickUp + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to update | +| `name` | string | No | New name for the task | +| `description` | string | No | New plain text description \(use a single space to clear\) | +| `markdownContent` | string | No | New Markdown description \(takes precedence over description\) | +| `status` | string | No | New status for the task \(must exist in the list\) | +| `priority` | number | No | Priority: 1 \(urgent\), 2 \(high\), 3 \(normal\), 4 \(low\) | +| `dueDate` | number | No | New due date as a Unix timestamp in milliseconds | +| `startDate` | number | No | New start date as a Unix timestamp in milliseconds | +| `timeEstimate` | number | No | New time estimate in milliseconds | +| `points` | number | No | New sprint points value | +| `parent` | string | No | Parent task ID to move this task under \(cannot be cleared\) | +| `archived` | boolean | No | Set to true to archive the task, false to unarchive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | The updated task | + +### `clickup_delete_task` + +Delete a task from ClickUp + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the deleted task | +| `deleted` | boolean | Whether the task was deleted | + +### `clickup_get_tasks` + +List the tasks in a ClickUp list (100 tasks per page) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `listId` | string | Yes | ID of the list to fetch tasks from | +| `page` | number | No | Page to fetch \(starts at 0\) | +| `orderBy` | string | No | Order by field: id, created, updated, or due_date | +| `reverse` | boolean | No | Return tasks in reverse order | +| `subtasks` | boolean | No | Include subtasks \(excluded by default\) | +| `includeClosed` | boolean | No | Include closed tasks \(excluded by default\) | +| `archived` | boolean | No | Return archived tasks | +| `statuses` | array | No | Filter tasks by status names | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tasks` | array | Tasks in the list | + +### `clickup_search_tasks` + +Search tasks across a ClickUp workspace, filtered by lists, folders, or spaces (100 tasks per page) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceId` | string | Yes | ID of the workspace \(team\) to search tasks in | +| `page` | number | No | Page to fetch \(starts at 0\) | +| `orderBy` | string | No | Order by field: id, created, updated, or due_date | +| `reverse` | boolean | No | Return tasks in reverse order | +| `subtasks` | boolean | No | Include subtasks \(excluded by default\) | +| `listIds` | array | No | Filter by list IDs | +| `spaceIds` | array | No | Filter by space IDs | +| `folderIds` | array | No | Filter by folder IDs | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tasks` | array | Tasks matching the filters | + +### `clickup_create_comment` + +Add a comment to a ClickUp task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to comment on | +| `commentText` | string | Yes | Content of the comment | +| `assignee` | number | No | User ID to assign the comment to | +| `notifyAll` | boolean | No | Whether to also notify the comment creator \(assignees and watchers are always notified\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the created comment | +| `histId` | string | History ID of the created comment | +| `date` | number | Creation timestamp of the comment \(Unix ms\) | + +### `clickup_get_comments` + +Retrieve comments on a ClickUp task, newest first (25 per page; paginate with start and startId) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to fetch comments from | +| `start` | number | No | Unix timestamp \(ms\) of the reference comment for pagination \(use the date of the last comment from the previous page, together with startId\) | +| `startId` | string | No | ID of the reference comment for pagination \(use the id of the last comment from the previous page, together with start\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comments` | array | Comments on the task, newest first | + +### `clickup_update_comment` + +Update the content, assignee, or resolved state of a ClickUp task comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `commentId` | string | Yes | ID of the comment to update | +| `commentText` | string | No | New content for the comment | +| `assignee` | number | No | User ID to assign the comment to | +| `resolved` | boolean | No | Whether the comment is resolved | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the updated comment | +| `updated` | boolean | Whether the comment was updated | + +### `clickup_delete_comment` + +Delete a comment from a ClickUp task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `commentId` | string | Yes | ID of the comment to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the deleted comment | +| `deleted` | boolean | Whether the comment was deleted | + +### `clickup_upload_attachment` + +Upload a file to a ClickUp task as an attachment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to attach the file to | +| `file` | file | Yes | File to attach to the task | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachment` | json | The created attachment | +| ↳ `id` | string | Attachment ID | +| ↳ `title` | string | Attachment title | +| ↳ `extension` | string | File extension | +| ↳ `url` | string | URL of the uploaded attachment | +| ↳ `date` | number | Upload timestamp \(Unix ms\) | +| `files` | file[] | The uploaded attachment file | + +### `clickup_add_tag_to_task` + +Add an existing space tag to a ClickUp task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to tag | +| `tagName` | string | Yes | Name of the tag to add \(must exist in the space\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskId` | string | ID of the tagged task | +| `tagName` | string | Name of the tag that was added | + +### `clickup_remove_tag_from_task` + +Remove a tag from a ClickUp task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to remove the tag from | +| `tagName` | string | Yes | Name of the tag to remove | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskId` | string | ID of the task | +| `tagName` | string | Name of the tag that was removed | + +### `clickup_get_space_tags` + +List the task tags available in a ClickUp space + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spaceId` | string | Yes | ID of the space to list tags from | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tags` | array | Tags available in the space | + +### `clickup_get_task_members` + +List the workspace members who have explicit access to a ClickUp task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | ID of the task to list members for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `members` | array | Members with explicit access to the task | + +### `clickup_get_list_members` + +List the workspace members who have explicit access to a ClickUp list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `listId` | string | Yes | ID of the list to list members for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `members` | array | Members with explicit access to the list | + +### `clickup_get_custom_fields` + +List the custom fields accessible in a ClickUp list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `listId` | string | Yes | ID of the list to fetch custom fields from | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fields` | array | Custom fields accessible in the list | +| ↳ `id` | string | Custom field ID | +| ↳ `name` | string | Custom field name | +| ↳ `type` | string | Custom field type \(e.g. text, number, drop_down\) | +| ↳ `typeConfig` | json | Type-specific configuration \(e.g. dropdown options\) | +| ↳ `dateCreated` | string | Creation timestamp \(Unix ms\) | +| ↳ `hideFromGuests` | boolean | Whether the field is hidden from guests | +| ↳ `required` | boolean | Whether the field is required | + +### `clickup_get_workspaces` + +List the ClickUp workspaces (teams) available to the connected account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workspaces` | array | Workspaces available to the connected account | +| ↳ `id` | string | Workspace ID | +| ↳ `name` | string | Workspace name | +| ↳ `color` | string | Workspace color | +| ↳ `avatar` | string | Workspace avatar URL | + +### `clickup_get_spaces` + +List the spaces in a ClickUp workspace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceId` | string | Yes | ID of the workspace \(team\) to list spaces from | +| `archived` | boolean | No | Return archived spaces | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spaces` | array | Spaces in the workspace | +| ↳ `id` | string | Space ID | +| ↳ `name` | string | Space name | +| ↳ `private` | boolean | Whether the space is private | +| ↳ `archived` | boolean | Whether the space is archived | +| ↳ `statuses` | array | Task statuses available in the space | +| ↳ `status` | string | Status name | +| ↳ `color` | string | Status color | +| ↳ `type` | string | Status type | + +### `clickup_get_folders` + +List the folders in a ClickUp space + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spaceId` | string | Yes | ID of the space to list folders from | +| `archived` | boolean | No | Return archived folders | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `folders` | array | Folders in the space | + +### `clickup_get_lists` + +List the lists in a ClickUp folder, or the folderless lists in a space when a space ID is provided instead + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `folderId` | string | No | ID of the folder to list lists from \(provide this or spaceId\) | +| `spaceId` | string | No | ID of the space to list folderless lists from \(provide this or folderId\) | +| `archived` | boolean | No | Return archived lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `lists` | array | Lists in the folder or space | + +### `clickup_create_folder` + +Create a new folder in a ClickUp space + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spaceId` | string | Yes | ID of the space to create the folder in | +| `name` | string | Yes | Name of the folder | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `folder` | json | The created folder | + +### `clickup_create_list` + +Create a new list in a ClickUp folder, or a folderless list in a space when a space ID is provided instead + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `folderId` | string | No | ID of the folder to create the list in \(provide this or spaceId\) | +| `spaceId` | string | No | ID of the space to create a folderless list in \(provide this or folderId\) | +| `name` | string | Yes | Name of the list | +| `content` | string | No | Plain text description of the list | +| `markdownContent` | string | No | Markdown description of the list \(use instead of content\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `list` | json | The created list | + + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 248715d9577..28522766552 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -36,6 +36,8 @@ "clay", "clerk", "clickhouse", + "clickup", + "clickup-service-account", "cloudflare", "cloudformation", "cloudwatch", diff --git a/apps/sim/app/api/tools/clickup/upload-attachment/route.ts b/apps/sim/app/api/tools/clickup/upload-attachment/route.ts new file mode 100644 index 00000000000..5e599857d07 --- /dev/null +++ b/apps/sim/app/api/tools/clickup/upload-attachment/route.ts @@ -0,0 +1,110 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { clickupUploadAttachmentContract } from '@/lib/api/contracts/tools/clickup' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' +import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' +import { assertToolFileAccess } from '@/app/api/files/authorization' +import { clickupAuthorizationHeader, extractClickUpErrorMessage } from '@/tools/clickup/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('ClickUpUploadAttachmentAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json( + { success: false, error: authResult.error || 'Unauthorized' }, + { status: 401 } + ) + } + + const parsed = await parseRequest(clickupUploadAttachmentContract, request, {}) + if (!parsed.success) return parsed.response + const params = parsed.data.body + + const userFiles = processFilesToUserFiles([params.file as RawFileInput], requestId, logger) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'No valid file provided for upload' }, + { status: 400 } + ) + } + + const userFile = userFiles[0] + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + + let buffer: Buffer + let downloadedContentType = '' + try { + const result = await downloadServableFileFromStorage(userFile, requestId, logger) + buffer = result.buffer + downloadedContentType = result.contentType + } catch (error) { + const notReady = docNotReadyResponse(error) + if (notReady) return notReady + throw error + } + + const formData = new FormData() + const blob = new Blob([new Uint8Array(buffer)], { + type: downloadedContentType || userFile.type || 'application/octet-stream', + }) + formData.append('attachment', blob, userFile.name) + + const url = `https://api.clickup.com/api/v2/task/${encodeURIComponent(params.taskId)}/attachment` + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: clickupAuthorizationHeader(params.accessToken), + }, + body: formData, + }) + + const data: unknown = await response.json().catch(() => null) + if (!response.ok) { + const message = extractClickUpErrorMessage( + response, + data, + 'Failed to upload ClickUp attachment' + ) + logger.error(`[${requestId}] ClickUp attachment upload failed`, { + status: response.status, + message, + }) + return NextResponse.json({ success: false, error: message }, { status: response.status }) + } + + const record = (data ?? {}) as Record + + return NextResponse.json({ + success: true, + output: { + attachment: { + id: typeof record.id === 'string' ? record.id : '', + title: typeof record.title === 'string' ? record.title : null, + extension: typeof record.extension === 'string' ? record.extension : null, + url: typeof record.url === 'string' ? record.url : null, + date: typeof record.date === 'number' ? record.date : null, + }, + files: userFiles, + }, + }) + } catch (error) { + logger.error(`[${requestId}] ClickUp attachment upload error`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Internal server error') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/clickup.ts b/apps/sim/blocks/blocks/clickup.ts new file mode 100644 index 00000000000..b88877988d9 --- /dev/null +++ b/apps/sim/blocks/blocks/clickup.ts @@ -0,0 +1,986 @@ +import { ClickUpIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { ClickUpResponse } from '@/tools/clickup/types' + +const TIMESTAMP_WAND_PROMPT = `Generate a Unix timestamp in milliseconds based on the user's description. +Examples: +- "tomorrow at noon" -> Calculate tomorrow at 12:00 and return its Unix ms timestamp +- "next Friday" -> Calculate next Friday at 00:00 and return its Unix ms timestamp +- "in 3 days" -> Calculate 3 days from now and return its Unix ms timestamp + +Return ONLY the numeric timestamp in milliseconds - no explanations, no quotes, no extra text.` + +function splitCommaSeparated(value: unknown): string[] | undefined { + if (typeof value !== 'string') return undefined + const parts = value + .split(',') + .map((part) => part.trim()) + .filter((part) => part.length > 0) + return parts.length > 0 ? parts : undefined +} + +function splitCommaSeparatedNumbers(value: unknown): number[] | undefined { + const parts = splitCommaSeparated(value) + if (!parts) return undefined + const numbers = parts.map((part) => Number(part)).filter((num) => Number.isFinite(num)) + return numbers.length > 0 ? numbers : undefined +} + +function optionalNumber(value: unknown): number | undefined { + if (value === undefined || value === null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +export const ClickUpBlock: BlockConfig = { + type: 'clickup', + name: 'ClickUp', + description: 'Interact with ClickUp', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate ClickUp into the workflow. Create, read, update, and delete tasks, manage comments, tags, folders, and lists, upload attachments, and look up workspaces, members, and custom fields.', + docsLink: 'https://docs.sim.ai/integrations/clickup', + category: 'tools', + integrationType: IntegrationType.Productivity, + bgColor: '#FFFFFF', + icon: ClickUpIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Task', id: 'create_task' }, + { label: 'Get Task', id: 'get_task' }, + { label: 'Update Task', id: 'update_task' }, + { label: 'Delete Task', id: 'delete_task' }, + { label: 'Get Tasks', id: 'get_tasks' }, + { label: 'Search Tasks', id: 'search_tasks' }, + { label: 'Create Comment', id: 'create_comment' }, + { label: 'Get Comments', id: 'get_comments' }, + { label: 'Update Comment', id: 'update_comment' }, + { label: 'Delete Comment', id: 'delete_comment' }, + { label: 'Upload Attachment', id: 'upload_attachment' }, + { label: 'Add Tag to Task', id: 'add_tag_to_task' }, + { label: 'Remove Tag from Task', id: 'remove_tag_from_task' }, + { label: 'Get Space Tags', id: 'get_space_tags' }, + { label: 'Get Task Members', id: 'get_task_members' }, + { label: 'Get List Members', id: 'get_list_members' }, + { label: 'Get Custom Fields', id: 'get_custom_fields' }, + { label: 'Get Workspaces', id: 'get_workspaces' }, + { label: 'Get Spaces', id: 'get_spaces' }, + { label: 'Get Folders', id: 'get_folders' }, + { label: 'Get Lists', id: 'get_lists' }, + { label: 'Create Folder', id: 'create_folder' }, + { label: 'Create List', id: 'create_list' }, + ], + value: () => 'create_task', + }, + { + id: 'credential', + title: 'ClickUp Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'clickup', + requiredScopes: getScopesForService('clickup'), + placeholder: 'Select ClickUp account', + }, + { + id: 'manualCredential', + title: 'ClickUp Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + { + id: 'workspaceId', + title: 'Workspace ID', + type: 'short-input', + required: true, + placeholder: 'Enter workspace (team) ID', + condition: { + field: 'operation', + value: ['search_tasks', 'get_spaces'], + }, + }, + { + id: 'spaceId', + title: 'Space ID', + type: 'short-input', + required: { field: 'operation', value: ['get_folders', 'create_folder', 'get_space_tags'] }, + placeholder: 'Enter space ID', + condition: { + field: 'operation', + value: ['get_folders', 'create_folder', 'get_space_tags', 'get_lists', 'create_list'], + }, + }, + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'Enter folder ID (or use a space ID for folderless lists)', + condition: { + field: 'operation', + value: ['get_lists', 'create_list'], + }, + }, + { + id: 'listId', + title: 'List ID', + type: 'short-input', + required: true, + placeholder: 'Enter list ID', + condition: { + field: 'operation', + value: ['create_task', 'get_tasks', 'get_list_members', 'get_custom_fields'], + }, + }, + { + id: 'taskId', + title: 'Task ID', + type: 'short-input', + required: true, + placeholder: 'Enter task ID', + condition: { + field: 'operation', + value: [ + 'get_task', + 'update_task', + 'delete_task', + 'create_comment', + 'get_comments', + 'upload_attachment', + 'add_tag_to_task', + 'remove_tag_from_task', + 'get_task_members', + ], + }, + }, + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + required: true, + placeholder: 'Enter comment ID', + condition: { + field: 'operation', + value: ['update_comment', 'delete_comment'], + }, + }, + { + id: 'name', + title: 'Name', + type: 'short-input', + required: { field: 'operation', value: ['create_task', 'create_folder', 'create_list'] }, + placeholder: 'Enter a name', + condition: { + field: 'operation', + value: ['create_task', 'update_task', 'create_folder', 'create_list'], + }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Enter task description', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'markdownContent', + title: 'Markdown Description', + type: 'long-input', + mode: 'advanced', + placeholder: 'Markdown description (overrides Description)', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'status', + title: 'Status', + type: 'short-input', + placeholder: 'Enter a status that exists in the list (e.g. "in progress")', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'priority', + title: 'Priority', + type: 'dropdown', + options: [ + { label: 'None', id: 'none' }, + { label: 'Urgent', id: '1' }, + { label: 'High', id: '2' }, + { label: 'Normal', id: '3' }, + { label: 'Low', id: '4' }, + ], + value: () => 'none', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'dueDate', + title: 'Due Date', + type: 'short-input', + placeholder: 'Unix timestamp in milliseconds', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + wandConfig: { + enabled: true, + prompt: TIMESTAMP_WAND_PROMPT, + placeholder: 'Describe the due date (e.g., "tomorrow", "next Friday")...', + generationType: 'timestamp', + }, + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + mode: 'advanced', + placeholder: 'Unix timestamp in milliseconds', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + wandConfig: { + enabled: true, + prompt: TIMESTAMP_WAND_PROMPT, + placeholder: 'Describe the start date (e.g., "today", "next Monday")...', + generationType: 'timestamp', + }, + }, + { + id: 'assignees', + title: 'Assignees', + type: 'short-input', + placeholder: 'Comma-separated user IDs (e.g. 183, 184)', + condition: { + field: 'operation', + value: ['create_task'], + }, + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'Comma-separated tag names', + condition: { + field: 'operation', + value: ['create_task'], + }, + }, + { + id: 'timeEstimate', + title: 'Time Estimate', + type: 'short-input', + mode: 'advanced', + placeholder: 'Time estimate in milliseconds', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'points', + title: 'Sprint Points', + type: 'short-input', + mode: 'advanced', + placeholder: 'Sprint points (number)', + condition: { + field: 'operation', + value: ['update_task'], + }, + }, + { + id: 'parent', + title: 'Parent Task ID', + type: 'short-input', + mode: 'advanced', + placeholder: 'Parent task ID (creates a subtask)', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'notifyAll', + title: 'Notify Creator', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_task', 'create_comment'], + }, + }, + { + id: 'archiveAction', + title: 'Archive', + type: 'dropdown', + mode: 'advanced', + options: [ + { label: 'No change', id: 'none' }, + { label: 'Archive', id: 'archive' }, + { label: 'Unarchive', id: 'unarchive' }, + ], + value: () => 'none', + condition: { + field: 'operation', + value: ['update_task'], + }, + }, + { + id: 'includeSubtasks', + title: 'Include Subtasks', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_task'], + }, + }, + { + id: 'includeMarkdownDescription', + title: 'Markdown Description', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_task'], + }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + mode: 'advanced', + placeholder: 'Page to fetch (starts at 0)', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + }, + { + id: 'orderBy', + title: 'Order By', + type: 'dropdown', + mode: 'advanced', + options: [ + { label: 'Created', id: 'created' }, + { label: 'Updated', id: 'updated' }, + { label: 'Due date', id: 'due_date' }, + { label: 'ID', id: 'id' }, + ], + value: () => 'created', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + }, + { + id: 'reverse', + title: 'Reverse Order', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + }, + { + id: 'subtasks', + title: 'Include Subtasks', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + }, + { + id: 'includeClosed', + title: 'Include Closed Tasks', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_tasks'], + }, + }, + { + id: 'archived', + title: 'Archived Only', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_tasks', 'get_spaces', 'get_folders', 'get_lists'], + }, + }, + { + id: 'statuses', + title: 'Statuses', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated status names to filter by', + condition: { + field: 'operation', + value: ['get_tasks'], + }, + }, + { + id: 'listIds', + title: 'List IDs', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated list IDs to filter by', + condition: { + field: 'operation', + value: ['search_tasks'], + }, + }, + { + id: 'spaceIds', + title: 'Space IDs', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated space IDs to filter by', + condition: { + field: 'operation', + value: ['search_tasks'], + }, + }, + { + id: 'folderIds', + title: 'Folder IDs', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated folder IDs to filter by', + condition: { + field: 'operation', + value: ['search_tasks'], + }, + }, + { + id: 'commentText', + title: 'Comment Text', + type: 'long-input', + required: { field: 'operation', value: ['create_comment'] }, + placeholder: 'Enter comment text', + condition: { + field: 'operation', + value: ['create_comment', 'update_comment'], + }, + }, + { + id: 'commentAssignee', + title: 'Comment Assignee', + type: 'short-input', + mode: 'advanced', + placeholder: 'User ID to assign the comment to', + condition: { + field: 'operation', + value: ['create_comment', 'update_comment'], + }, + }, + { + id: 'resolvedAction', + title: 'Resolved', + type: 'dropdown', + mode: 'advanced', + options: [ + { label: 'No change', id: 'none' }, + { label: 'Resolve', id: 'resolve' }, + { label: 'Unresolve', id: 'unresolve' }, + ], + value: () => 'none', + condition: { + field: 'operation', + value: ['update_comment'], + }, + }, + { + id: 'start', + title: 'Start Timestamp', + type: 'short-input', + mode: 'advanced', + placeholder: 'Unix ms of the last comment from the previous page', + condition: { + field: 'operation', + value: ['get_comments'], + }, + }, + { + id: 'startId', + title: 'Start Comment ID', + type: 'short-input', + mode: 'advanced', + placeholder: 'ID of the last comment from the previous page', + condition: { + field: 'operation', + value: ['get_comments'], + }, + }, + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + required: true, + placeholder: 'Enter tag name', + condition: { + field: 'operation', + value: ['add_tag_to_task', 'remove_tag_from_task'], + }, + }, + { + id: 'content', + title: 'List Description', + type: 'long-input', + placeholder: 'Enter list description', + condition: { + field: 'operation', + value: ['create_list'], + }, + }, + { + id: 'attachmentFile', + title: 'File', + type: 'file-upload', + canonicalParamId: 'file', + placeholder: 'Upload file', + mode: 'basic', + multiple: false, + required: true, + condition: { + field: 'operation', + value: ['upload_attachment'], + }, + }, + { + id: 'fileReference', + title: 'File', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'File reference from a previous block', + mode: 'advanced', + required: true, + condition: { + field: 'operation', + value: ['upload_attachment'], + }, + }, + ], + tools: { + access: [ + 'clickup_create_task', + 'clickup_get_task', + 'clickup_update_task', + 'clickup_delete_task', + 'clickup_get_tasks', + 'clickup_search_tasks', + 'clickup_create_comment', + 'clickup_get_comments', + 'clickup_update_comment', + 'clickup_delete_comment', + 'clickup_upload_attachment', + 'clickup_add_tag_to_task', + 'clickup_remove_tag_from_task', + 'clickup_get_space_tags', + 'clickup_get_task_members', + 'clickup_get_list_members', + 'clickup_get_custom_fields', + 'clickup_get_workspaces', + 'clickup_get_spaces', + 'clickup_get_folders', + 'clickup_get_lists', + 'clickup_create_folder', + 'clickup_create_list', + ], + config: { + tool: (params) => `clickup_${params.operation}`, + params: (params) => { + const { oauthCredential, operation } = params + + const baseParams = { + accessToken: oauthCredential?.accessToken, + } + + const priorityValue = + params.priority && params.priority !== 'none' ? Number(params.priority) : undefined + + switch (operation) { + case 'create_task': + return { + ...baseParams, + listId: params.listId, + name: params.name, + description: params.description || undefined, + markdownContent: params.markdownContent || undefined, + status: params.status || undefined, + priority: priorityValue, + dueDate: optionalNumber(params.dueDate), + startDate: optionalNumber(params.startDate), + assignees: splitCommaSeparatedNumbers(params.assignees), + tags: splitCommaSeparated(params.tags), + timeEstimate: optionalNumber(params.timeEstimate), + parent: params.parent || undefined, + notifyAll: params.notifyAll ? true : undefined, + } + case 'get_task': + return { + ...baseParams, + taskId: params.taskId, + includeSubtasks: params.includeSubtasks ? true : undefined, + includeMarkdownDescription: params.includeMarkdownDescription ? true : undefined, + } + case 'update_task': + return { + ...baseParams, + taskId: params.taskId, + name: params.name || undefined, + description: params.description || undefined, + markdownContent: params.markdownContent || undefined, + status: params.status || undefined, + priority: priorityValue, + dueDate: optionalNumber(params.dueDate), + startDate: optionalNumber(params.startDate), + timeEstimate: optionalNumber(params.timeEstimate), + points: optionalNumber(params.points), + parent: params.parent || undefined, + archived: + params.archiveAction === 'archive' + ? true + : params.archiveAction === 'unarchive' + ? false + : undefined, + } + case 'delete_task': + return { + ...baseParams, + taskId: params.taskId, + } + case 'get_tasks': + return { + ...baseParams, + listId: params.listId, + page: optionalNumber(params.page), + orderBy: params.orderBy || undefined, + reverse: params.reverse ? true : undefined, + subtasks: params.subtasks ? true : undefined, + includeClosed: params.includeClosed ? true : undefined, + archived: params.archived ? true : undefined, + statuses: splitCommaSeparated(params.statuses), + } + case 'search_tasks': + return { + ...baseParams, + workspaceId: params.workspaceId, + page: optionalNumber(params.page), + orderBy: params.orderBy || undefined, + reverse: params.reverse ? true : undefined, + subtasks: params.subtasks ? true : undefined, + listIds: splitCommaSeparated(params.listIds), + spaceIds: splitCommaSeparated(params.spaceIds), + folderIds: splitCommaSeparated(params.folderIds), + } + case 'create_comment': + return { + ...baseParams, + taskId: params.taskId, + commentText: params.commentText, + assignee: optionalNumber(params.commentAssignee), + notifyAll: params.notifyAll ? true : undefined, + } + case 'get_comments': + return { + ...baseParams, + taskId: params.taskId, + start: optionalNumber(params.start), + startId: params.startId || undefined, + } + case 'update_comment': + return { + ...baseParams, + commentId: params.commentId, + commentText: params.commentText || undefined, + assignee: optionalNumber(params.commentAssignee), + resolved: + params.resolvedAction === 'resolve' + ? true + : params.resolvedAction === 'unresolve' + ? false + : undefined, + } + case 'delete_comment': + return { + ...baseParams, + commentId: params.commentId, + } + case 'upload_attachment': { + const normalizedFile = normalizeFileInput(params.file, { single: true }) + if (!normalizedFile) { + throw new Error('An attachment file is required.') + } + return { + ...baseParams, + taskId: params.taskId, + file: normalizedFile, + } + } + case 'add_tag_to_task': + case 'remove_tag_from_task': + return { + ...baseParams, + taskId: params.taskId, + tagName: params.tagName, + } + case 'get_space_tags': + return { + ...baseParams, + spaceId: params.spaceId, + } + case 'get_task_members': + return { + ...baseParams, + taskId: params.taskId, + } + case 'get_list_members': + return { + ...baseParams, + listId: params.listId, + } + case 'get_custom_fields': + return { + ...baseParams, + listId: params.listId, + } + case 'get_workspaces': + return { + ...baseParams, + } + case 'get_spaces': + return { + ...baseParams, + workspaceId: params.workspaceId, + archived: params.archived ? true : undefined, + } + case 'get_folders': + return { + ...baseParams, + spaceId: params.spaceId, + archived: params.archived ? true : undefined, + } + case 'get_lists': + return { + ...baseParams, + folderId: params.folderId || undefined, + spaceId: params.spaceId || undefined, + archived: params.archived ? true : undefined, + } + case 'create_folder': + return { + ...baseParams, + spaceId: params.spaceId, + name: params.name, + } + case 'create_list': + return { + ...baseParams, + folderId: params.folderId || undefined, + spaceId: params.spaceId || undefined, + name: params.name, + content: params.content || undefined, + markdownContent: params.markdownContent || undefined, + } + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'ClickUp OAuth credential' }, + workspaceId: { type: 'string', description: 'Workspace (team) ID' }, + spaceId: { type: 'string', description: 'Space ID' }, + folderId: { type: 'string', description: 'Folder ID' }, + listId: { type: 'string', description: 'List ID' }, + taskId: { type: 'string', description: 'Task ID' }, + commentId: { type: 'string', description: 'Comment ID' }, + name: { type: 'string', description: 'Name for the task, folder, or list' }, + description: { type: 'string', description: 'Task description' }, + markdownContent: { type: 'string', description: 'Markdown description' }, + status: { type: 'string', description: 'Task status' }, + priority: { type: 'string', description: 'Task priority (1-4)' }, + dueDate: { type: 'string', description: 'Due date (Unix ms)' }, + startDate: { type: 'string', description: 'Start date (Unix ms)' }, + assignees: { type: 'string', description: 'Comma-separated assignee user IDs' }, + tags: { type: 'string', description: 'Comma-separated tag names' }, + timeEstimate: { type: 'string', description: 'Time estimate in milliseconds' }, + points: { type: 'string', description: 'Sprint points' }, + parent: { type: 'string', description: 'Parent task ID' }, + notifyAll: { type: 'boolean', description: 'Notify the creator' }, + archiveAction: { type: 'string', description: 'Archive action (none, archive, unarchive)' }, + includeSubtasks: { type: 'boolean', description: 'Include subtasks' }, + includeMarkdownDescription: { + type: 'boolean', + description: 'Return the description in Markdown', + }, + page: { type: 'string', description: 'Page to fetch (starts at 0)' }, + orderBy: { type: 'string', description: 'Order-by field' }, + reverse: { type: 'boolean', description: 'Reverse order' }, + subtasks: { type: 'boolean', description: 'Include subtasks in results' }, + includeClosed: { type: 'boolean', description: 'Include closed tasks' }, + archived: { type: 'boolean', description: 'Return archived items' }, + statuses: { type: 'string', description: 'Comma-separated status filters' }, + listIds: { type: 'string', description: 'Comma-separated list ID filters' }, + spaceIds: { type: 'string', description: 'Comma-separated space ID filters' }, + folderIds: { type: 'string', description: 'Comma-separated folder ID filters' }, + commentText: { type: 'string', description: 'Comment text' }, + commentAssignee: { type: 'string', description: 'User ID to assign the comment to' }, + resolvedAction: { type: 'string', description: 'Resolved action (none, resolve, unresolve)' }, + start: { type: 'string', description: 'Pagination start timestamp (Unix ms)' }, + startId: { type: 'string', description: 'Pagination start comment ID' }, + tagName: { type: 'string', description: 'Tag name' }, + content: { type: 'string', description: 'List description' }, + file: { type: 'json', description: 'File to upload as an attachment' }, + }, + outputs: { + task: { type: 'json', description: 'Task details' }, + tasks: { type: 'json', description: 'Array of tasks' }, + comments: { type: 'json', description: 'Array of comments' }, + id: { type: 'string', description: 'ID of the affected resource' }, + histId: { type: 'string', description: 'History ID of the created comment' }, + date: { type: 'number', description: 'Timestamp of the created comment (Unix ms)' }, + updated: { type: 'boolean', description: 'Whether the comment was updated' }, + deleted: { type: 'boolean', description: 'Whether the resource was deleted' }, + attachment: { type: 'json', description: 'Uploaded attachment details' }, + files: { type: 'json', description: 'Uploaded attachment files' }, + taskId: { type: 'string', description: 'Task ID for tag operations' }, + tagName: { type: 'string', description: 'Tag name for tag operations' }, + tags: { type: 'json', description: 'Array of space tags' }, + members: { type: 'json', description: 'Array of members' }, + fields: { type: 'json', description: 'Array of custom fields' }, + workspaces: { type: 'json', description: 'Array of workspaces' }, + spaces: { type: 'json', description: 'Array of spaces' }, + folders: { type: 'json', description: 'Array of folders' }, + folder: { type: 'json', description: 'Created folder details' }, + lists: { type: 'json', description: 'Array of lists' }, + list: { type: 'json', description: 'Created list details' }, + }, +} + +export const ClickUpBlockMeta = { + tags: ['project-management', 'ticketing', 'automation'], + url: 'https://clickup.com', + templates: [ + { + icon: ClickUpIcon, + title: 'ClickUp task intake', + prompt: + 'Build a workflow that turns incoming Slack requests into ClickUp tasks in the right list with a clear name, description, priority, and due date, then replies in the thread with the task link.', + modules: ['agent', 'workflows'], + category: 'productivity', + tags: ['team', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp standup digest', + prompt: + 'Create a scheduled weekday workflow that pulls open ClickUp tasks for each list, groups them by assignee and status, and posts a morning standup summary to the team Slack channel.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'productivity', + tags: ['team', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp overdue-task nudger', + prompt: + 'Build a scheduled workflow that searches ClickUp for tasks past their due date, adds a comment asking for a status update, and emails each assignee a digest of their overdue work.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'productivity', + tags: ['team', 'monitoring'], + alsoIntegrations: ['gmail'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp bug triager', + prompt: + 'Create a workflow that reads newly reported bugs from a ClickUp list, classifies each by severity and component with an agent, sets the priority and tags accordingly, and opens a matching GitHub issue for engineering.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['engineering', 'automation'], + alsoIntegrations: ['github'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp sprint retro writer', + prompt: + 'Build a workflow that pulls the ClickUp tasks completed during a sprint, summarizes wins, blockers, and recurring themes with an agent, and shares a retro document with the team in Slack.', + modules: ['agent', 'files', 'workflows'], + category: 'productivity', + tags: ['team', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp customer onboarding launcher', + prompt: + 'Create a workflow that on a closed-won Salesforce opportunity creates a ClickUp onboarding task with the right assignees and due date, attaches the signed order form, and writes the task link back to the opportunity.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['sales', 'crm'], + alsoIntegrations: ['salesforce'], + }, + { + icon: ClickUpIcon, + title: 'ClickUp meeting action items', + prompt: + 'Build a workflow that takes meeting notes, extracts action items with an agent, creates a ClickUp task for each with the right assignee and due date, and replies with a checklist of created tasks.', + modules: ['agent', 'workflows'], + category: 'productivity', + tags: ['team', 'automation'], + }, + ], + skills: [ + { + name: 'create-task-from-request', + description: + 'Turn an incoming request or message into a well-formed ClickUp task in the right list with priority, assignees, and due date. Use for intake and ticket creation.', + content: + '# Create Task from Request\n\nConvert an incoming request into a structured ClickUp task.\n\n## Steps\n1. Extract the work to be done, the target list, any assignees, a priority, and a due date.\n2. If the list is referenced by name, walk the hierarchy (workspaces, spaces, folders, lists) to resolve its ID.\n3. Create the task with a clear name, a description capturing the request details, and the extracted fields.\n4. Add a comment with any links or source context if helpful.\n\n## Output\nReport the created task name, its URL or ID, list, assignees, and due date.', + }, + { + name: 'summarize-list-tasks', + description: + 'Fetch the tasks in a ClickUp list and summarize status, overdue items, and who owns what. Use for standups and project status checks.', + content: + '# Summarize List Tasks\n\nProduce a status snapshot of a ClickUp list.\n\n## Steps\n1. Resolve the list, then fetch its tasks (include closed tasks when a full picture is needed).\n2. For each task capture name, assignees, status, priority, and due date.\n3. Group into completed, in progress, and overdue or due soon.\n4. Note any unassigned tasks or tasks with no due date.\n\n## Output\nA concise status summary: counts per group, overdue tasks called out by name and owner, and any gaps to address.', + }, + { + name: 'update-task-status', + description: + 'Find a ClickUp task and update its fields — status, priority, due date — or add a progress comment. Use to keep tasks current from other systems.', + content: + '# Update Task Status\n\nKeep a ClickUp task in sync with the latest state.\n\n## Steps\n1. Identify the target task by ID, or search the workspace to find it.\n2. Read the current task to confirm it is the right one.\n3. Update the relevant fields — status, priority, due date, or archive state.\n4. Add a comment summarizing what changed and why.\n\n## Output\nReport which fields changed and confirm the task ID. If no matching task was found, say so.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index bf6fe014b37..109bc55b652 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -32,6 +32,7 @@ import { CirclebackBlock, CirclebackBlockMeta } from '@/blocks/blocks/circleback import { ClayBlock, ClayBlockMeta } from '@/blocks/blocks/clay' import { ClerkBlock, ClerkBlockMeta } from '@/blocks/blocks/clerk' import { ClickHouseBlock, ClickHouseBlockMeta } from '@/blocks/blocks/clickhouse' +import { ClickUpBlock, ClickUpBlockMeta } from '@/blocks/blocks/clickup' import { CloudflareBlock, CloudflareBlockMeta } from '@/blocks/blocks/cloudflare' import { CloudFormationBlock, CloudFormationBlockMeta } from '@/blocks/blocks/cloudformation' import { CloudWatchBlock, CloudWatchBlockMeta } from '@/blocks/blocks/cloudwatch' @@ -373,6 +374,7 @@ export const BLOCK_REGISTRY: Record = { clay: ClayBlock, clerk: ClerkBlock, clickhouse: ClickHouseBlock, + clickup: ClickUpBlock, cloudflare: CloudflareBlock, cloudformation: CloudFormationBlock, cloudwatch: CloudWatchBlock, @@ -694,6 +696,7 @@ export const BLOCK_META_REGISTRY: Record = { clay: ClayBlockMeta, clerk: ClerkBlockMeta, clickhouse: ClickHouseBlockMeta, + clickup: ClickUpBlockMeta, cloudflare: CloudflareBlockMeta, cloudformation: CloudFormationBlockMeta, cloudwatch: CloudWatchBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index e7acb6322a2..f0f336f9083 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2801,6 +2801,49 @@ export function LinqIcon(props: SVGProps) { ) } +export function ClickUpIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + ) +} + export function LinearIcon(props: React.SVGProps) { return ( +export type ClickUpUploadAttachmentApiResponse = ContractJsonResponse< + typeof clickupUploadAttachmentContract +> diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts index 3270a34ef99..c1bfb0ccdbe 100644 --- a/apps/sim/lib/api/contracts/tools/index.ts +++ b/apps/sim/lib/api/contracts/tools/index.ts @@ -2,6 +2,7 @@ export * from './a2a' export * from './agiloft' export * from './asana' export * from './brex' +export * from './clickup' export * from './communication' export * from './crowdstrike' export * from './cursor' diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 61a104eccf2..a4aeafc2343 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -2348,6 +2348,55 @@ export const auth = betterAuth({ }, }, + { + providerId: 'clickup', + clientId: env.CLICKUP_CLIENT_ID as string, + clientSecret: env.CLICKUP_CLIENT_SECRET as string, + authorizationUrl: 'https://app.clickup.com/api', + tokenUrl: 'https://api.clickup.com/api/v2/oauth/token', + scopes: getCanonicalScopesForProvider('clickup'), + responseType: 'code', + pkce: false, + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/clickup`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://api.clickup.com/api/v2/user', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + await response.text().catch(() => {}) + logger.error('Error fetching ClickUp user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const data = await response.json() + const user = data.user + if (!user?.id) return null + + const now = new Date() + return { + id: `${user.id.toString()}-${generateId()}`, + name: user.username || 'ClickUp User', + email: user.email || `${user.id}@clickup.user`, + emailVerified: !!user.email, + createdAt: now, + updatedAt: now, + image: user.profilePicture || undefined, + } + } catch (error) { + logger.error('Error in ClickUp getUserInfo:', { error }) + return null + } + }, + }, + { providerId: 'linear', clientId: env.LINEAR_CLIENT_ID as string, diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index cbb95efd5fb..d3c6f4ba61c 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -391,6 +391,8 @@ export const env = createEnv({ PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret + CLICKUP_CLIENT_ID: z.string().optional(), // ClickUp OAuth client ID + CLICKUP_CLIENT_SECRET: z.string().optional(), // ClickUp OAuth client secret BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret DROPBOX_CLIENT_ID: z.string().optional(), // Dropbox OAuth client ID diff --git a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts index eac00def403..f3661a6ff93 100644 --- a/apps/sim/lib/credentials/token-service-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/token-service-accounts/descriptors.ts @@ -53,6 +53,7 @@ export const AIRTABLE_SERVICE_ACCOUNT_PROVIDER_ID = 'airtable-service-account' a export const NOTION_SERVICE_ACCOUNT_PROVIDER_ID = 'notion-service-account' as const export const ASANA_SERVICE_ACCOUNT_PROVIDER_ID = 'asana-service-account' as const export const ATTIO_SERVICE_ACCOUNT_PROVIDER_ID = 'attio-service-account' as const +export const CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID = 'clickup-service-account' as const export const LINEAR_SERVICE_ACCOUNT_PROVIDER_ID = 'linear-service-account' as const export const MONDAY_SERVICE_ACCOUNT_PROVIDER_ID = 'monday-service-account' as const export const SHOPIFY_SERVICE_ACCOUNT_PROVIDER_ID = 'shopify-service-account' as const @@ -69,6 +70,7 @@ export type TokenServiceAccountProviderId = | typeof NOTION_SERVICE_ACCOUNT_PROVIDER_ID | typeof ASANA_SERVICE_ACCOUNT_PROVIDER_ID | typeof ATTIO_SERVICE_ACCOUNT_PROVIDER_ID + | typeof CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID | typeof LINEAR_SERVICE_ACCOUNT_PROVIDER_ID | typeof MONDAY_SERVICE_ACCOUNT_PROVIDER_ID | typeof SHOPIFY_SERVICE_ACCOUNT_PROVIDER_ID @@ -172,6 +174,23 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record< helpText: 'Check the scopes granted to the key in Attio — tools whose scopes are missing will fail at run time.', }, + [CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'ClickUp', + tokenNoun: 'personal API token', + connectNoun: 'API token', + fields: [ + { + id: 'apiToken', + label: 'API token', + placeholder: 'pk_...', + secret: true, + hintPattern: /^pk_/, + hintMessage: 'ClickUp personal API tokens start with pk_.', + }, + ], + docsUrl: 'https://docs.sim.ai/integrations/clickup-service-account', + }, [LINEAR_SERVICE_ACCOUNT_PROVIDER_ID]: { providerId: LINEAR_SERVICE_ACCOUNT_PROVIDER_ID, serviceLabel: 'Linear', diff --git a/apps/sim/lib/credentials/token-service-accounts/server.ts b/apps/sim/lib/credentials/token-service-accounts/server.ts index c0a2ed16d1d..0d232c414cc 100644 --- a/apps/sim/lib/credentials/token-service-accounts/server.ts +++ b/apps/sim/lib/credentials/token-service-accounts/server.ts @@ -3,6 +3,7 @@ import { ASANA_SERVICE_ACCOUNT_PROVIDER_ID, ATTIO_SERVICE_ACCOUNT_PROVIDER_ID, CALCOM_SERVICE_ACCOUNT_PROVIDER_ID, + CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID, HUBSPOT_SERVICE_ACCOUNT_PROVIDER_ID, isTokenServiceAccountProviderId, LINEAR_SERVICE_ACCOUNT_PROVIDER_ID, @@ -19,6 +20,7 @@ import { validateAirtableServiceAccount } from '@/lib/credentials/token-service- import { validateAsanaServiceAccount } from '@/lib/credentials/token-service-accounts/validators/asana' import { validateAttioServiceAccount } from '@/lib/credentials/token-service-accounts/validators/attio' import { validateCalcomServiceAccount } from '@/lib/credentials/token-service-accounts/validators/calcom' +import { validateClickupServiceAccount } from '@/lib/credentials/token-service-accounts/validators/clickup' import { validateHubspotServiceAccount } from '@/lib/credentials/token-service-accounts/validators/hubspot' import { validateLinearServiceAccount } from '@/lib/credentials/token-service-accounts/validators/linear' import { validateMondayServiceAccount } from '@/lib/credentials/token-service-accounts/validators/monday' @@ -67,6 +69,7 @@ const TOKEN_SERVICE_ACCOUNT_VALIDATORS: Record< [NOTION_SERVICE_ACCOUNT_PROVIDER_ID]: validateNotionServiceAccount, [ASANA_SERVICE_ACCOUNT_PROVIDER_ID]: validateAsanaServiceAccount, [ATTIO_SERVICE_ACCOUNT_PROVIDER_ID]: validateAttioServiceAccount, + [CLICKUP_SERVICE_ACCOUNT_PROVIDER_ID]: validateClickupServiceAccount, [LINEAR_SERVICE_ACCOUNT_PROVIDER_ID]: validateLinearServiceAccount, [MONDAY_SERVICE_ACCOUNT_PROVIDER_ID]: validateMondayServiceAccount, [SHOPIFY_SERVICE_ACCOUNT_PROVIDER_ID]: validateShopifyServiceAccount, diff --git a/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts b/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts new file mode 100644 index 00000000000..129fbb1fb02 --- /dev/null +++ b/apps/sim/lib/credentials/token-service-accounts/validators/clickup.ts @@ -0,0 +1,73 @@ +import { + fetchProvider, + parseProviderJson, + readProviderErrorSnippet, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' +import type { + TokenServiceAccountFields, + TokenServiceAccountValidationResult, +} from '@/lib/credentials/token-service-accounts/server' +import { clickupAuthorizationHeader } from '@/tools/clickup/shared' + +const CLICKUP_USER_URL = 'https://api.clickup.com/api/v2/user' + +interface ClickUpUserResponse { + user?: { + id?: number + username?: string | null + email?: string | null + } +} + +/** + * Validates a ClickUp personal API token by fetching the authorized user. The + * Authorization header is built with the same helper the runtime tools use + * (`clickupAuthorizationHeader`): personal `pk_` tokens go bare, anything else + * gets the `Bearer` prefix — so validation exercises the exact header shape + * tools will send. + */ +export async function validateClickupServiceAccount( + fields: TokenServiceAccountFields +): Promise { + const res = await fetchProvider( + CLICKUP_USER_URL, + { + method: 'GET', + headers: { + Authorization: clickupAuthorizationHeader(fields.apiToken), + 'Content-Type': 'application/json', + }, + }, + 'user' + ) + + if (!res.ok) { + const body = await readProviderErrorSnippet(res) + if (res.status === 401 || res.status === 403) { + throw new TokenServiceAccountValidationError('invalid_credentials', res.status, { + step: 'user', + body, + }) + } + throw new TokenServiceAccountValidationError('provider_unavailable', res.status, { + step: 'user', + body, + }) + } + + const payload = await parseProviderJson(res, 'user') + const user = payload.user + if (!user?.id) { + throw new TokenServiceAccountValidationError('provider_unavailable', 502, { + step: 'user', + reason: 'missing user in response', + }) + } + + return { + displayName: user.username || user.email || 'ClickUp account', + auditMetadata: { clickupUserId: String(user.id) }, + storedMetadata: { userId: String(user.id) }, + } +} diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 621aea2f729..185e321db5c 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -34,6 +34,7 @@ import { ClayIcon, ClerkIcon, ClickHouseIcon, + ClickUpIcon, CloudFormationIcon, CloudflareIcon, CloudWatchIcon, @@ -271,6 +272,7 @@ export const blockTypeToIconMap: Record = { clay: ClayIcon, clerk: ClerkIcon, clickhouse: ClickHouseIcon, + clickup: ClickUpIcon, cloudflare: CloudflareIcon, cloudformation: CloudFormationIcon, cloudwatch: CloudWatchIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 6d2d8a661c9..5f0a54db88e 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -3239,6 +3239,122 @@ "integrationType": "databases", "tags": ["data-warehouse", "data-analytics"] }, + { + "type": "clickup", + "slug": "clickup", + "name": "ClickUp", + "description": "Interact with ClickUp", + "longDescription": "Integrate ClickUp into the workflow. Create, read, update, and delete tasks, manage comments, tags, folders, and lists, upload attachments, and look up workspaces, members, and custom fields.", + "bgColor": "#FFFFFF", + "iconName": "ClickUpIcon", + "docsUrl": "https://docs.sim.ai/integrations/clickup", + "operations": [ + { + "name": "Create Task", + "description": "Create a new task in a ClickUp list" + }, + { + "name": "Get Task", + "description": "Retrieve a task from ClickUp by ID" + }, + { + "name": "Update Task", + "description": "Update an existing task in ClickUp" + }, + { + "name": "Delete Task", + "description": "Delete a task from ClickUp" + }, + { + "name": "Get Tasks", + "description": "List the tasks in a ClickUp list (100 tasks per page)" + }, + { + "name": "Search Tasks", + "description": "Search tasks across a ClickUp workspace, filtered by lists, folders, or spaces (100 tasks per page)" + }, + { + "name": "Create Comment", + "description": "Add a comment to a ClickUp task" + }, + { + "name": "Get Comments", + "description": "Retrieve comments on a ClickUp task, newest first (25 per page; paginate with start and startId)" + }, + { + "name": "Update Comment", + "description": "Update the content, assignee, or resolved state of a ClickUp task comment" + }, + { + "name": "Delete Comment", + "description": "Delete a comment from a ClickUp task" + }, + { + "name": "Upload Attachment", + "description": "Upload a file to a ClickUp task as an attachment" + }, + { + "name": "Add Tag to Task", + "description": "Add an existing space tag to a ClickUp task" + }, + { + "name": "Remove Tag from Task", + "description": "Remove a tag from a ClickUp task" + }, + { + "name": "Get Space Tags", + "description": "List the task tags available in a ClickUp space" + }, + { + "name": "Get Task Members", + "description": "List the workspace members who have explicit access to a ClickUp task" + }, + { + "name": "Get List Members", + "description": "List the workspace members who have explicit access to a ClickUp list" + }, + { + "name": "Get Custom Fields", + "description": "List the custom fields accessible in a ClickUp list" + }, + { + "name": "Get Workspaces", + "description": "List the ClickUp workspaces (teams) available to the connected account" + }, + { + "name": "Get Spaces", + "description": "List the spaces in a ClickUp workspace" + }, + { + "name": "Get Folders", + "description": "List the folders in a ClickUp space" + }, + { + "name": "Get Lists", + "description": "List the lists in a ClickUp folder, or the folderless lists in a space when a space ID is provided instead" + }, + { + "name": "Create Folder", + "description": "Create a new folder in a ClickUp space" + }, + { + "name": "Create List", + "description": "Create a new list in a ClickUp folder, or a folderless list in a space when a space ID is provided instead" + } + ], + "operationCount": 23, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "oauthServiceId": "clickup", + "category": "tools", + "integrationType": "productivity", + "tags": [ + "project-management", + "ticketing", + "automation" + ] + }, { "type": "cloudflare", "slug": "cloudflare", diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index d1a0bb00dbe..33d810f1f48 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -8,6 +8,7 @@ import { AzureIcon, BoxCompanyIcon, CalComIcon, + ClickUpIcon, ConfluenceIcon, DocuSignIcon, DropboxIcon, @@ -626,6 +627,22 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'notion', }, + clickup: { + name: 'ClickUp', + icon: ClickUpIcon, + services: { + clickup: { + name: 'ClickUp', + description: 'Manage tasks, lists, and comments in ClickUp.', + providerId: 'clickup', + serviceAccountProviderId: 'clickup-service-account', + icon: ClickUpIcon, + baseProviderIcon: ClickUpIcon, + scopes: [], + }, + }, + defaultService: 'clickup', + }, linear: { name: 'Linear', icon: LinearIcon, @@ -1270,6 +1287,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: true, } } + case 'clickup': { + const { clientId, clientSecret } = getCredentials( + env.CLICKUP_CLIENT_ID, + env.CLICKUP_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://api.clickup.com/api/v2/oauth/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: false, + } + } case 'linear': { const { clientId, clientSecret } = getCredentials( env.LINEAR_CLIENT_ID, diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 96b7f1a06eb..40c24fe8547 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -65,6 +65,7 @@ export type OAuthProvider = | 'outlook' | 'onedrive' | 'sharepoint' + | 'clickup' | 'linear' | 'slack' | 'reddit' @@ -117,6 +118,7 @@ export type OAuthService = | 'microsoft-planner' | 'sharepoint' | 'outlook' + | 'clickup' | 'linear' | 'slack' | 'reddit' diff --git a/apps/sim/tools/clickup/add_tag_to_task.ts b/apps/sim/tools/clickup/add_tag_to_task.ts new file mode 100644 index 00000000000..e1576fec619 --- /dev/null +++ b/apps/sim/tools/clickup/add_tag_to_task.ts @@ -0,0 +1,69 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpTaskTagParams, ClickUpTaskTagResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupAddTagToTaskTool: ToolConfig = { + id: 'clickup_add_tag_to_task', + name: 'ClickUp Add Tag To Task', + description: 'Add an existing space tag to a ClickUp task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to tag', + }, + tagName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the tag to add (must exist in the space)', + }, + }, + + request: { + url: (params) => + `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/tag/${encodeURIComponent(params.tagName)}`, + method: 'POST', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const data = await response.json().catch(() => null) + const error = extractClickUpErrorMessage(response, data, 'Failed to add tag to task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { taskId: params?.taskId, tagName: params?.tagName }, + } + }, + + outputs: { + taskId: { type: 'string', description: 'ID of the tagged task', optional: true }, + tagName: { type: 'string', description: 'Name of the tag that was added', optional: true }, + }, +} diff --git a/apps/sim/tools/clickup/create_comment.ts b/apps/sim/tools/clickup/create_comment.ts new file mode 100644 index 00000000000..0f54beb37bd --- /dev/null +++ b/apps/sim/tools/clickup/create_comment.ts @@ -0,0 +1,106 @@ +import { isRecordLike } from '@sim/utils/object' +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpCreateCommentParams, ClickUpCreateCommentResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupCreateCommentTool: ToolConfig< + ClickUpCreateCommentParams, + ClickUpCreateCommentResponse +> = { + id: 'clickup_create_comment', + name: 'ClickUp Create Comment', + description: 'Add a comment to a ClickUp task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to comment on', + }, + commentText: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Content of the comment', + }, + assignee: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'User ID to assign the comment to', + }, + notifyAll: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether to also notify the comment creator (assignees and watchers are always notified)', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/comment`, + method: 'POST', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + comment_text: params.commentText, + notify_all: params.notifyAll ?? false, + } + + if (params.assignee !== undefined) body.assignee = params.assignee + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to create comment') + return { success: false, output: { error }, error } + } + + const record = isRecordLike(data) ? data : {} + + return { + success: true, + output: { + id: typeof record.id === 'string' ? record.id : String(record.id ?? ''), + histId: typeof record.hist_id === 'string' ? record.hist_id : String(record.hist_id ?? ''), + date: typeof record.date === 'number' ? record.date : Number(record.date ?? 0), + }, + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the created comment', optional: true }, + histId: { type: 'string', description: 'History ID of the created comment', optional: true }, + date: { + type: 'number', + description: 'Creation timestamp of the comment (Unix ms)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/clickup/create_folder.ts b/apps/sim/tools/clickup/create_folder.ts new file mode 100644 index 00000000000..fa67c709610 --- /dev/null +++ b/apps/sim/tools/clickup/create_folder.ts @@ -0,0 +1,77 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_FOLDER_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpFolder, +} from '@/tools/clickup/shared' +import type { ClickUpCreateFolderParams, ClickUpFolderResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupCreateFolderTool: ToolConfig = + { + id: 'clickup_create_folder', + name: 'ClickUp Create Folder', + description: 'Create a new folder in a ClickUp space', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to create the folder in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the folder', + }, + }, + + request: { + url: (params) => + `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/folder`, + method: 'POST', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => ({ name: params.name }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to create folder') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { folder: mapClickUpFolder(data) }, + } + }, + + outputs: { + folder: { + type: 'json', + description: 'The created folder', + optional: true, + properties: CLICKUP_FOLDER_OUTPUT_PROPERTIES, + }, + }, + } diff --git a/apps/sim/tools/clickup/create_list.ts b/apps/sim/tools/clickup/create_list.ts new file mode 100644 index 00000000000..b7f462c3239 --- /dev/null +++ b/apps/sim/tools/clickup/create_list.ts @@ -0,0 +1,111 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_LIST_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpList, +} from '@/tools/clickup/shared' +import type { ClickUpCreateListParams, ClickUpListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupCreateListTool: ToolConfig = { + id: 'clickup_create_list', + name: 'ClickUp Create List', + description: + 'Create a new list in a ClickUp folder, or a folderless list in a space when a space ID is provided instead', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the folder to create the list in (provide this or spaceId)', + }, + spaceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the space to create a folderless list in (provide this or folderId)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the list', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Plain text description of the list', + }, + markdownContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Markdown description of the list (use instead of content)', + }, + }, + + request: { + url: (params) => { + if (params.folderId) { + return `${CLICKUP_API_BASE_URL}/folder/${encodeURIComponent(params.folderId)}/list` + } + if (params.spaceId) { + return `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/list` + } + throw new Error('Either a folder ID or a space ID is required to create a list') + }, + method: 'POST', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + name: params.name, + } + + if (params.content) body.content = params.content + if (params.markdownContent) body.markdown_content = params.markdownContent + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to create list') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { list: mapClickUpList(data) }, + } + }, + + outputs: { + list: { + type: 'json', + description: 'The created list', + optional: true, + properties: CLICKUP_LIST_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/clickup/create_task.ts b/apps/sim/tools/clickup/create_task.ts new file mode 100644 index 00000000000..b2d2b9bb75a --- /dev/null +++ b/apps/sim/tools/clickup/create_task.ts @@ -0,0 +1,167 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TASK_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTask, +} from '@/tools/clickup/shared' +import type { ClickUpCreateTaskParams, ClickUpTaskResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupCreateTaskTool: ToolConfig = { + id: 'clickup_create_task', + name: 'ClickUp Create Task', + description: 'Create a new task in a ClickUp list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the list to create the task in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the task', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Plain text description of the task', + }, + markdownContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Markdown description of the task (overrides description)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Status to create the task with (must exist in the list)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Priority: 1 (urgent), 2 (high), 3 (normal), 4 (low)', + }, + dueDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Due date as a Unix timestamp in milliseconds', + }, + startDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start date as a Unix timestamp in milliseconds', + }, + assignees: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'User IDs to assign to the task', + items: { + type: 'number', + description: 'A ClickUp user ID', + }, + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Tag names to apply to the task', + items: { + type: 'string', + description: 'A tag name', + }, + }, + timeEstimate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Time estimate in milliseconds', + }, + parent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Parent task ID to create this task as a subtask', + }, + notifyAll: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to notify the task creator on creation', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/list/${encodeURIComponent(params.listId)}/task`, + method: 'POST', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + name: params.name, + } + + if (params.description) body.description = params.description + if (params.markdownContent) body.markdown_content = params.markdownContent + if (params.status) body.status = params.status + if (params.priority !== undefined) body.priority = params.priority + if (params.dueDate !== undefined) body.due_date = params.dueDate + if (params.startDate !== undefined) body.start_date = params.startDate + if (params.assignees?.length) body.assignees = params.assignees + if (params.tags?.length) body.tags = params.tags + if (params.timeEstimate !== undefined) body.time_estimate = params.timeEstimate + if (params.parent) body.parent = params.parent + if (params.notifyAll !== undefined) body.notify_all = params.notifyAll + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to create task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { task: mapClickUpTask(data) }, + } + }, + + outputs: { + task: { + type: 'json', + description: 'The created task', + optional: true, + properties: CLICKUP_TASK_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/clickup/delete_comment.ts b/apps/sim/tools/clickup/delete_comment.ts new file mode 100644 index 00000000000..2ad8005f55f --- /dev/null +++ b/apps/sim/tools/clickup/delete_comment.ts @@ -0,0 +1,64 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpDeleteCommentParams, ClickUpDeleteResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupDeleteCommentTool: ToolConfig< + ClickUpDeleteCommentParams, + ClickUpDeleteResponse +> = { + id: 'clickup_delete_comment', + name: 'ClickUp Delete Comment', + description: 'Delete a comment from a ClickUp task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the comment to delete', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/comment/${encodeURIComponent(params.commentId)}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const data = await response.json().catch(() => null) + const error = extractClickUpErrorMessage(response, data, 'Failed to delete comment') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { id: params?.commentId, deleted: true }, + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the deleted comment', optional: true }, + deleted: { type: 'boolean', description: 'Whether the comment was deleted', optional: true }, + }, +} diff --git a/apps/sim/tools/clickup/delete_task.ts b/apps/sim/tools/clickup/delete_task.ts new file mode 100644 index 00000000000..76f41ade6ef --- /dev/null +++ b/apps/sim/tools/clickup/delete_task.ts @@ -0,0 +1,61 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpDeleteResponse, ClickUpDeleteTaskParams } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupDeleteTaskTool: ToolConfig = { + id: 'clickup_delete_task', + name: 'ClickUp Delete Task', + description: 'Delete a task from ClickUp', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to delete', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const data = await response.json().catch(() => null) + const error = extractClickUpErrorMessage(response, data, 'Failed to delete task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { id: params?.taskId, deleted: true }, + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the deleted task', optional: true }, + deleted: { type: 'boolean', description: 'Whether the task was deleted', optional: true }, + }, +} diff --git a/apps/sim/tools/clickup/get_comments.ts b/apps/sim/tools/clickup/get_comments.ts new file mode 100644 index 00000000000..bd1643e35c8 --- /dev/null +++ b/apps/sim/tools/clickup/get_comments.ts @@ -0,0 +1,98 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_COMMENT_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpComment, +} from '@/tools/clickup/shared' +import type { ClickUpCommentListResponse, ClickUpGetCommentsParams } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetCommentsTool: ToolConfig< + ClickUpGetCommentsParams, + ClickUpCommentListResponse +> = { + id: 'clickup_get_comments', + name: 'ClickUp Get Comments', + description: + 'Retrieve comments on a ClickUp task, newest first (25 per page; paginate with start and startId)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to fetch comments from', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Unix timestamp (ms) of the reference comment for pagination (use the date of the last comment from the previous page, together with startId)', + }, + startId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'ID of the reference comment for pagination (use the id of the last comment from the previous page, together with start)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/comment` + ) + if (params.start !== undefined) url.searchParams.set('start', String(params.start)) + if (params.startId) url.searchParams.set('start_id', params.startId) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get comments') + return { success: false, output: { error }, error } + } + + const rawComments = Array.isArray(data?.comments) ? data.comments : [] + + return { + success: true, + output: { comments: rawComments.map((comment: unknown) => mapClickUpComment(comment)) }, + } + }, + + outputs: { + comments: { + type: 'array', + description: 'Comments on the task, newest first', + optional: true, + items: { + type: 'object', + properties: CLICKUP_COMMENT_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_custom_fields.ts b/apps/sim/tools/clickup/get_custom_fields.ts new file mode 100644 index 00000000000..e7976efa59f --- /dev/null +++ b/apps/sim/tools/clickup/get_custom_fields.ts @@ -0,0 +1,106 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpCustomField, +} from '@/tools/clickup/shared' +import type { + ClickUpCustomFieldListResponse, + ClickUpGetCustomFieldsParams, +} from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetCustomFieldsTool: ToolConfig< + ClickUpGetCustomFieldsParams, + ClickUpCustomFieldListResponse +> = { + id: 'clickup_get_custom_fields', + name: 'ClickUp Get Custom Fields', + description: 'List the custom fields accessible in a ClickUp list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the list to fetch custom fields from', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/list/${encodeURIComponent(params.listId)}/field`, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get custom fields') + return { success: false, output: { error }, error } + } + + const rawFields = Array.isArray(data?.fields) ? data.fields : [] + + return { + success: true, + output: { fields: rawFields.map((field: unknown) => mapClickUpCustomField(field)) }, + } + }, + + outputs: { + fields: { + type: 'array', + description: 'Custom fields accessible in the list', + optional: true, + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Custom field ID' }, + name: { type: 'string', description: 'Custom field name', nullable: true }, + type: { + type: 'string', + description: 'Custom field type (e.g. text, number, drop_down)', + nullable: true, + }, + typeConfig: { + type: 'json', + description: 'Type-specific configuration (e.g. dropdown options)', + nullable: true, + }, + dateCreated: { + type: 'string', + description: 'Creation timestamp (Unix ms)', + nullable: true, + }, + hideFromGuests: { + type: 'boolean', + description: 'Whether the field is hidden from guests', + nullable: true, + }, + required: { + type: 'boolean', + description: 'Whether the field is required', + nullable: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_folders.ts b/apps/sim/tools/clickup/get_folders.ts new file mode 100644 index 00000000000..bb100b96b54 --- /dev/null +++ b/apps/sim/tools/clickup/get_folders.ts @@ -0,0 +1,88 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_FOLDER_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpFolder, +} from '@/tools/clickup/shared' +import type { ClickUpFolderListResponse, ClickUpGetFoldersParams } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetFoldersTool: ToolConfig = + { + id: 'clickup_get_folders', + name: 'ClickUp Get Folders', + description: 'List the folders in a ClickUp space', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to list folders from', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return archived folders', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/folder` + ) + if (params.archived !== undefined) { + url.searchParams.set('archived', String(params.archived)) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get folders') + return { success: false, output: { error }, error } + } + + const rawFolders = Array.isArray(data?.folders) ? data.folders : [] + + return { + success: true, + output: { folders: rawFolders.map((folder: unknown) => mapClickUpFolder(folder)) }, + } + }, + + outputs: { + folders: { + type: 'array', + description: 'Folders in the space', + optional: true, + items: { + type: 'object', + properties: CLICKUP_FOLDER_OUTPUT_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/clickup/get_list_members.ts b/apps/sim/tools/clickup/get_list_members.ts new file mode 100644 index 00000000000..22723efe5a1 --- /dev/null +++ b/apps/sim/tools/clickup/get_list_members.ts @@ -0,0 +1,76 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_MEMBER_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpMember, +} from '@/tools/clickup/shared' +import type { ClickUpGetListMembersParams, ClickUpMemberListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetListMembersTool: ToolConfig< + ClickUpGetListMembersParams, + ClickUpMemberListResponse +> = { + id: 'clickup_get_list_members', + name: 'ClickUp Get List Members', + description: 'List the workspace members who have explicit access to a ClickUp list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the list to list members for', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/list/${encodeURIComponent(params.listId)}/member`, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get list members') + return { success: false, output: { error }, error } + } + + const rawMembers = Array.isArray(data?.members) ? data.members : [] + + return { + success: true, + output: { members: rawMembers.map((member: unknown) => mapClickUpMember(member)) }, + } + }, + + outputs: { + members: { + type: 'array', + description: 'Members with explicit access to the list', + optional: true, + items: { + type: 'object', + properties: CLICKUP_MEMBER_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_lists.ts b/apps/sim/tools/clickup/get_lists.ts new file mode 100644 index 00000000000..6d1ae50d6ed --- /dev/null +++ b/apps/sim/tools/clickup/get_lists.ts @@ -0,0 +1,100 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_LIST_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpList, +} from '@/tools/clickup/shared' +import type { ClickUpGetListsParams, ClickUpListListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetListsTool: ToolConfig = { + id: 'clickup_get_lists', + name: 'ClickUp Get Lists', + description: + 'List the lists in a ClickUp folder, or the folderless lists in a space when a space ID is provided instead', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the folder to list lists from (provide this or spaceId)', + }, + spaceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the space to list folderless lists from (provide this or folderId)', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return archived lists', + }, + }, + + request: { + url: (params) => { + const base = params.folderId + ? `${CLICKUP_API_BASE_URL}/folder/${encodeURIComponent(params.folderId)}/list` + : params.spaceId + ? `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/list` + : null + + if (!base) { + throw new Error('Either a folder ID or a space ID is required to get lists') + } + + const url = new URL(base) + if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get lists') + return { success: false, output: { error }, error } + } + + const rawLists = Array.isArray(data?.lists) ? data.lists : [] + + return { + success: true, + output: { lists: rawLists.map((list: unknown) => mapClickUpList(list)) }, + } + }, + + outputs: { + lists: { + type: 'array', + description: 'Lists in the folder or space', + optional: true, + items: { + type: 'object', + properties: CLICKUP_LIST_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_space_tags.ts b/apps/sim/tools/clickup/get_space_tags.ts new file mode 100644 index 00000000000..b3b1fe6c617 --- /dev/null +++ b/apps/sim/tools/clickup/get_space_tags.ts @@ -0,0 +1,82 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TAG_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTag, +} from '@/tools/clickup/shared' +import type { + ClickUpGetSpaceTagsParams, + ClickUpTag, + ClickUpTagListResponse, +} from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetSpaceTagsTool: ToolConfig = + { + id: 'clickup_get_space_tags', + name: 'ClickUp Get Space Tags', + description: 'List the task tags available in a ClickUp space', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to list tags from', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/tag`, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get space tags') + return { success: false, output: { error }, error } + } + + const rawTags = Array.isArray(data?.tags) ? data.tags : [] + + return { + success: true, + output: { + tags: rawTags + .map((tag: unknown) => mapClickUpTag(tag)) + .filter((tag: ClickUpTag | null): tag is ClickUpTag => tag !== null), + }, + } + }, + + outputs: { + tags: { + type: 'array', + description: 'Tags available in the space', + optional: true, + items: { + type: 'object', + properties: CLICKUP_TAG_OUTPUT_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/clickup/get_spaces.ts b/apps/sim/tools/clickup/get_spaces.ts new file mode 100644 index 00000000000..b2ebd414f2d --- /dev/null +++ b/apps/sim/tools/clickup/get_spaces.ts @@ -0,0 +1,105 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpSpace, +} from '@/tools/clickup/shared' +import type { ClickUpGetSpacesParams, ClickUpSpaceListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetSpacesTool: ToolConfig = { + id: 'clickup_get_spaces', + name: 'ClickUp Get Spaces', + description: 'List the spaces in a ClickUp workspace', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + workspaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the workspace (team) to list spaces from', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return archived spaces', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `${CLICKUP_API_BASE_URL}/team/${encodeURIComponent(params.workspaceId)}/space` + ) + if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get spaces') + return { success: false, output: { error }, error } + } + + const rawSpaces = Array.isArray(data?.spaces) ? data.spaces : [] + + return { + success: true, + output: { spaces: rawSpaces.map((space: unknown) => mapClickUpSpace(space)) }, + } + }, + + outputs: { + spaces: { + type: 'array', + description: 'Spaces in the workspace', + optional: true, + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Space ID' }, + name: { type: 'string', description: 'Space name', nullable: true }, + private: { type: 'boolean', description: 'Whether the space is private', nullable: true }, + archived: { + type: 'boolean', + description: 'Whether the space is archived', + nullable: true, + }, + statuses: { + type: 'array', + description: 'Task statuses available in the space', + items: { + type: 'object', + properties: { + status: { type: 'string', description: 'Status name', nullable: true }, + color: { type: 'string', description: 'Status color', nullable: true }, + type: { type: 'string', description: 'Status type', nullable: true }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_task.ts b/apps/sim/tools/clickup/get_task.ts new file mode 100644 index 00000000000..658cb0e08c7 --- /dev/null +++ b/apps/sim/tools/clickup/get_task.ts @@ -0,0 +1,92 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TASK_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTask, +} from '@/tools/clickup/shared' +import type { ClickUpGetTaskParams, ClickUpTaskResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetTaskTool: ToolConfig = { + id: 'clickup_get_task', + name: 'ClickUp Get Task', + description: 'Retrieve a task from ClickUp by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to retrieve', + }, + includeSubtasks: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include subtasks in the response', + }, + includeMarkdownDescription: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return the task description in Markdown format', + }, + }, + + request: { + url: (params) => { + const url = new URL(`${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}`) + if (params.includeSubtasks !== undefined) { + url.searchParams.set('include_subtasks', String(params.includeSubtasks)) + } + if (params.includeMarkdownDescription !== undefined) { + url.searchParams.set( + 'include_markdown_description', + String(params.includeMarkdownDescription) + ) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { task: mapClickUpTask(data) }, + } + }, + + outputs: { + task: { + type: 'json', + description: 'The requested task', + optional: true, + properties: CLICKUP_TASK_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_task_members.ts b/apps/sim/tools/clickup/get_task_members.ts new file mode 100644 index 00000000000..57112c5767f --- /dev/null +++ b/apps/sim/tools/clickup/get_task_members.ts @@ -0,0 +1,76 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_MEMBER_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpMember, +} from '@/tools/clickup/shared' +import type { ClickUpGetTaskMembersParams, ClickUpMemberListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetTaskMembersTool: ToolConfig< + ClickUpGetTaskMembersParams, + ClickUpMemberListResponse +> = { + id: 'clickup_get_task_members', + name: 'ClickUp Get Task Members', + description: 'List the workspace members who have explicit access to a ClickUp task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to list members for', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/member`, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get task members') + return { success: false, output: { error }, error } + } + + const rawMembers = Array.isArray(data?.members) ? data.members : [] + + return { + success: true, + output: { members: rawMembers.map((member: unknown) => mapClickUpMember(member)) }, + } + }, + + outputs: { + members: { + type: 'array', + description: 'Members with explicit access to the task', + optional: true, + items: { + type: 'object', + properties: CLICKUP_MEMBER_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_tasks.ts b/apps/sim/tools/clickup/get_tasks.ts new file mode 100644 index 00000000000..da5842da8ff --- /dev/null +++ b/apps/sim/tools/clickup/get_tasks.ts @@ -0,0 +1,133 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TASK_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTask, +} from '@/tools/clickup/shared' +import type { ClickUpGetTasksParams, ClickUpTaskListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetTasksTool: ToolConfig = { + id: 'clickup_get_tasks', + name: 'ClickUp Get Tasks', + description: 'List the tasks in a ClickUp list (100 tasks per page)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the list to fetch tasks from', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page to fetch (starts at 0)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order by field: id, created, updated, or due_date', + }, + reverse: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return tasks in reverse order', + }, + subtasks: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include subtasks (excluded by default)', + }, + includeClosed: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include closed tasks (excluded by default)', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return archived tasks', + }, + statuses: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by status names', + items: { + type: 'string', + description: 'A status name', + }, + }, + }, + + request: { + url: (params) => { + const url = new URL(`${CLICKUP_API_BASE_URL}/list/${encodeURIComponent(params.listId)}/task`) + if (params.page !== undefined) url.searchParams.set('page', String(params.page)) + if (params.orderBy) url.searchParams.set('order_by', params.orderBy) + if (params.reverse !== undefined) url.searchParams.set('reverse', String(params.reverse)) + if (params.subtasks !== undefined) url.searchParams.set('subtasks', String(params.subtasks)) + if (params.includeClosed !== undefined) { + url.searchParams.set('include_closed', String(params.includeClosed)) + } + if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived)) + for (const status of params.statuses ?? []) { + url.searchParams.append('statuses[]', status) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get tasks') + return { success: false, output: { error }, error } + } + + const rawTasks = Array.isArray(data?.tasks) ? data.tasks : [] + + return { + success: true, + output: { tasks: rawTasks.map((task: unknown) => mapClickUpTask(task)) }, + } + }, + + outputs: { + tasks: { + type: 'array', + description: 'Tasks in the list', + optional: true, + items: { + type: 'object', + properties: CLICKUP_TASK_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/get_workspaces.ts b/apps/sim/tools/clickup/get_workspaces.ts new file mode 100644 index 00000000000..caa4f663d93 --- /dev/null +++ b/apps/sim/tools/clickup/get_workspaces.ts @@ -0,0 +1,74 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpWorkspace, +} from '@/tools/clickup/shared' +import type { ClickUpGetWorkspacesParams, ClickUpWorkspaceListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupGetWorkspacesTool: ToolConfig< + ClickUpGetWorkspacesParams, + ClickUpWorkspaceListResponse +> = { + id: 'clickup_get_workspaces', + name: 'ClickUp Get Workspaces', + description: 'List the ClickUp workspaces (teams) available to the connected account', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + }, + + request: { + url: `${CLICKUP_API_BASE_URL}/team`, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to get workspaces') + return { success: false, output: { error }, error } + } + + const rawTeams = Array.isArray(data?.teams) ? data.teams : [] + + return { + success: true, + output: { workspaces: rawTeams.map((team: unknown) => mapClickUpWorkspace(team)) }, + } + }, + + outputs: { + workspaces: { + type: 'array', + description: 'Workspaces available to the connected account', + optional: true, + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Workspace ID' }, + name: { type: 'string', description: 'Workspace name', nullable: true }, + color: { type: 'string', description: 'Workspace color', nullable: true }, + avatar: { type: 'string', description: 'Workspace avatar URL', nullable: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/clickup/index.ts b/apps/sim/tools/clickup/index.ts new file mode 100644 index 00000000000..9ff371474a6 --- /dev/null +++ b/apps/sim/tools/clickup/index.ts @@ -0,0 +1,47 @@ +import { clickupAddTagToTaskTool } from '@/tools/clickup/add_tag_to_task' +import { clickupCreateCommentTool } from '@/tools/clickup/create_comment' +import { clickupCreateFolderTool } from '@/tools/clickup/create_folder' +import { clickupCreateListTool } from '@/tools/clickup/create_list' +import { clickupCreateTaskTool } from '@/tools/clickup/create_task' +import { clickupDeleteCommentTool } from '@/tools/clickup/delete_comment' +import { clickupDeleteTaskTool } from '@/tools/clickup/delete_task' +import { clickupGetCommentsTool } from '@/tools/clickup/get_comments' +import { clickupGetCustomFieldsTool } from '@/tools/clickup/get_custom_fields' +import { clickupGetFoldersTool } from '@/tools/clickup/get_folders' +import { clickupGetListMembersTool } from '@/tools/clickup/get_list_members' +import { clickupGetListsTool } from '@/tools/clickup/get_lists' +import { clickupGetSpaceTagsTool } from '@/tools/clickup/get_space_tags' +import { clickupGetSpacesTool } from '@/tools/clickup/get_spaces' +import { clickupGetTaskTool } from '@/tools/clickup/get_task' +import { clickupGetTaskMembersTool } from '@/tools/clickup/get_task_members' +import { clickupGetTasksTool } from '@/tools/clickup/get_tasks' +import { clickupGetWorkspacesTool } from '@/tools/clickup/get_workspaces' +import { clickupRemoveTagFromTaskTool } from '@/tools/clickup/remove_tag_from_task' +import { clickupSearchTasksTool } from '@/tools/clickup/search_tasks' +import { clickupUpdateCommentTool } from '@/tools/clickup/update_comment' +import { clickupUpdateTaskTool } from '@/tools/clickup/update_task' +import { clickupUploadAttachmentTool } from '@/tools/clickup/upload_attachment' + +export { clickupAddTagToTaskTool } +export { clickupCreateCommentTool } +export { clickupCreateFolderTool } +export { clickupCreateListTool } +export { clickupCreateTaskTool } +export { clickupDeleteCommentTool } +export { clickupDeleteTaskTool } +export { clickupGetCommentsTool } +export { clickupGetCustomFieldsTool } +export { clickupGetFoldersTool } +export { clickupGetListMembersTool } +export { clickupGetListsTool } +export { clickupGetSpaceTagsTool } +export { clickupGetSpacesTool } +export { clickupGetTaskTool } +export { clickupGetTaskMembersTool } +export { clickupGetTasksTool } +export { clickupGetWorkspacesTool } +export { clickupRemoveTagFromTaskTool } +export { clickupSearchTasksTool } +export { clickupUpdateCommentTool } +export { clickupUpdateTaskTool } +export { clickupUploadAttachmentTool } diff --git a/apps/sim/tools/clickup/remove_tag_from_task.ts b/apps/sim/tools/clickup/remove_tag_from_task.ts new file mode 100644 index 00000000000..7b0f0b4e57c --- /dev/null +++ b/apps/sim/tools/clickup/remove_tag_from_task.ts @@ -0,0 +1,69 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpTaskTagParams, ClickUpTaskTagResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupRemoveTagFromTaskTool: ToolConfig = + { + id: 'clickup_remove_tag_from_task', + name: 'ClickUp Remove Tag From Task', + description: 'Remove a tag from a ClickUp task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to remove the tag from', + }, + tagName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the tag to remove', + }, + }, + + request: { + url: (params) => + `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/tag/${encodeURIComponent(params.tagName)}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const data = await response.json().catch(() => null) + const error = extractClickUpErrorMessage(response, data, 'Failed to remove tag from task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { taskId: params?.taskId, tagName: params?.tagName }, + } + }, + + outputs: { + taskId: { type: 'string', description: 'ID of the task', optional: true }, + tagName: { type: 'string', description: 'Name of the tag that was removed', optional: true }, + }, + } diff --git a/apps/sim/tools/clickup/search_tasks.ts b/apps/sim/tools/clickup/search_tasks.ts new file mode 100644 index 00000000000..ed2edc2198c --- /dev/null +++ b/apps/sim/tools/clickup/search_tasks.ts @@ -0,0 +1,149 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TASK_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTask, +} from '@/tools/clickup/shared' +import type { ClickUpSearchTasksParams, ClickUpTaskListResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupSearchTasksTool: ToolConfig = + { + id: 'clickup_search_tasks', + name: 'ClickUp Search Tasks', + description: + 'Search tasks across a ClickUp workspace, filtered by lists, folders, or spaces (100 tasks per page)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + workspaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the workspace (team) to search tasks in', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page to fetch (starts at 0)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order by field: id, created, updated, or due_date', + }, + reverse: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return tasks in reverse order', + }, + subtasks: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include subtasks (excluded by default)', + }, + listIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Filter by list IDs', + items: { + type: 'string', + description: 'A ClickUp list ID', + }, + }, + spaceIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Filter by space IDs', + items: { + type: 'string', + description: 'A ClickUp space ID', + }, + }, + folderIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Filter by folder IDs', + items: { + type: 'string', + description: 'A ClickUp folder ID', + }, + }, + }, + + request: { + url: (params) => { + const url = new URL( + `${CLICKUP_API_BASE_URL}/team/${encodeURIComponent(params.workspaceId)}/task` + ) + if (params.page !== undefined) url.searchParams.set('page', String(params.page)) + if (params.orderBy) url.searchParams.set('order_by', params.orderBy) + if (params.reverse !== undefined) url.searchParams.set('reverse', String(params.reverse)) + if (params.subtasks !== undefined) { + url.searchParams.set('subtasks', String(params.subtasks)) + } + for (const listId of params.listIds ?? []) { + url.searchParams.append('list_ids[]', listId) + } + for (const spaceId of params.spaceIds ?? []) { + url.searchParams.append('space_ids[]', spaceId) + } + for (const folderId of params.folderIds ?? []) { + url.searchParams.append('project_ids[]', folderId) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to search tasks') + return { success: false, output: { error }, error } + } + + const rawTasks = Array.isArray(data?.tasks) ? data.tasks : [] + + return { + success: true, + output: { tasks: rawTasks.map((task: unknown) => mapClickUpTask(task)) }, + } + }, + + outputs: { + tasks: { + type: 'array', + description: 'Tasks matching the filters', + optional: true, + items: { + type: 'object', + properties: CLICKUP_TASK_OUTPUT_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/clickup/shared.ts b/apps/sim/tools/clickup/shared.ts new file mode 100644 index 00000000000..47f50c83415 --- /dev/null +++ b/apps/sim/tools/clickup/shared.ts @@ -0,0 +1,441 @@ +import { isRecordLike } from '@sim/utils/object' +import type { OutputProperty } from '@/tools/types' +import type { + ClickUpComment, + ClickUpCustomField, + ClickUpFolder, + ClickUpList, + ClickUpMember, + ClickUpPriority, + ClickUpSpace, + ClickUpStatus, + ClickUpTag, + ClickUpTask, + ClickUpUser, + ClickUpWorkspace, +} from '@/tools/clickup/types' + +export const CLICKUP_API_BASE_URL = 'https://api.clickup.com/api/v2' + +/** + * Builds the Authorization header value for ClickUp API requests. + * + * ClickUp documents two credential shapes: OAuth access tokens are sent as + * `Authorization: Bearer `, while personal API tokens (prefixed with + * `pk_`) must be sent bare as `Authorization: ` with no scheme. + * This helper detects the personal-token prefix and returns the correct form + * so tools work with both OAuth connections and pasted API tokens. + * + * @param accessToken - OAuth access token or ClickUp personal API token + * @returns The value to use for the `Authorization` header + */ +export function clickupAuthorizationHeader(accessToken: string): string { + return accessToken.startsWith('pk_') ? accessToken : `Bearer ${accessToken}` +} + +function getRequiredString(value: unknown, field: string): string { + if (typeof value === 'string' && value.trim().length > 0) { + return value + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } + + throw new Error(`ClickUp response is missing required field: ${field}`) +} + +function getOptionalString(value: unknown): string | null { + if (typeof value === 'string') { + return value + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } + + return null +} + +function getOptionalBoolean(value: unknown): boolean | null { + return typeof value === 'boolean' ? value : null +} + +function getOptionalNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + + if (typeof value === 'string' && value.trim().length > 0) { + const parsed = Number(value) + if (Number.isFinite(parsed)) { + return parsed + } + } + + return null +} + +const CLICKUP_USER_OUTPUT_PROPERTIES: Record = { + id: { type: 'number', description: 'User ID', nullable: true }, + username: { type: 'string', description: 'Username', nullable: true }, + email: { type: 'string', description: 'User email', nullable: true }, + profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true }, +} + +export const CLICKUP_TASK_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Task ID' }, + customId: { type: 'string', description: 'Custom task ID', nullable: true }, + name: { type: 'string', description: 'Task name' }, + textContent: { type: 'string', description: 'Plain text content', nullable: true }, + description: { type: 'string', description: 'Task description', nullable: true }, + status: { + type: 'object', + description: 'Task status', + nullable: true, + properties: { + status: { type: 'string', description: 'Status name', nullable: true }, + color: { type: 'string', description: 'Status color', nullable: true }, + type: { type: 'string', description: 'Status type (open, closed, custom)', nullable: true }, + }, + }, + archived: { type: 'boolean', description: 'Whether the task is archived' }, + creator: { + type: 'object', + description: 'Task creator', + nullable: true, + properties: CLICKUP_USER_OUTPUT_PROPERTIES, + }, + assignees: { + type: 'array', + description: 'Users assigned to the task', + items: { type: 'object', properties: CLICKUP_USER_OUTPUT_PROPERTIES }, + }, + tags: { + type: 'array', + description: 'Tags applied to the task', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Tag name', nullable: true }, + tagFg: { type: 'string', description: 'Tag foreground color', nullable: true }, + tagBg: { type: 'string', description: 'Tag background color', nullable: true }, + }, + }, + }, + parent: { type: 'string', description: 'Parent task ID', nullable: true }, + priority: { + type: 'object', + description: 'Task priority', + nullable: true, + properties: { + id: { type: 'string', description: 'Priority ID', nullable: true }, + priority: { type: 'string', description: 'Priority name', nullable: true }, + color: { type: 'string', description: 'Priority color', nullable: true }, + }, + }, + dueDate: { type: 'string', description: 'Due date (Unix ms)', nullable: true }, + startDate: { type: 'string', description: 'Start date (Unix ms)', nullable: true }, + points: { type: 'number', description: 'Sprint points', nullable: true }, + timeEstimate: { type: 'number', description: 'Time estimate in milliseconds', nullable: true }, + dateCreated: { type: 'string', description: 'Creation timestamp (Unix ms)', nullable: true }, + dateUpdated: { type: 'string', description: 'Last update timestamp (Unix ms)', nullable: true }, + dateClosed: { type: 'string', description: 'Closed timestamp (Unix ms)', nullable: true }, + dateDone: { type: 'string', description: 'Done timestamp (Unix ms)', nullable: true }, + list: { + type: 'object', + description: 'List containing the task', + nullable: true, + properties: { + id: { type: 'string', description: 'List ID' }, + name: { type: 'string', description: 'List name', nullable: true }, + }, + }, + url: { type: 'string', description: 'URL to the task in ClickUp', nullable: true }, +} + +export const CLICKUP_MEMBER_OUTPUT_PROPERTIES: Record = { + id: { type: 'number', description: 'Member user ID', nullable: true }, + username: { type: 'string', description: 'Username', nullable: true }, + email: { type: 'string', description: 'User email', nullable: true }, + color: { type: 'string', description: 'Profile color', nullable: true }, + initials: { type: 'string', description: 'User initials', nullable: true }, + profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true }, +} + +export const CLICKUP_COMMENT_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Comment ID' }, + commentText: { type: 'string', description: 'Comment text content', nullable: true }, + resolved: { type: 'boolean', description: 'Whether the comment is resolved', nullable: true }, + user: { + type: 'object', + description: 'Comment author', + nullable: true, + properties: CLICKUP_USER_OUTPUT_PROPERTIES, + }, + assignee: { + type: 'object', + description: 'User the comment is assigned to', + nullable: true, + properties: CLICKUP_USER_OUTPUT_PROPERTIES, + }, + date: { type: 'string', description: 'Comment timestamp (Unix ms)', nullable: true }, + replyCount: { type: 'string', description: 'Number of replies', nullable: true }, +} + +export const CLICKUP_LIST_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'List ID' }, + name: { type: 'string', description: 'List name', nullable: true }, + taskCount: { type: 'string', description: 'Number of tasks in the list', nullable: true }, + archived: { type: 'boolean', description: 'Whether the list is archived', nullable: true }, +} + +export const CLICKUP_FOLDER_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Folder ID' }, + name: { type: 'string', description: 'Folder name', nullable: true }, + hidden: { type: 'boolean', description: 'Whether the folder is hidden', nullable: true }, + taskCount: { type: 'string', description: 'Number of tasks in the folder', nullable: true }, + space: { + type: 'object', + description: 'Space containing the folder', + nullable: true, + properties: { + id: { type: 'string', description: 'Space ID' }, + name: { type: 'string', description: 'Space name', nullable: true }, + }, + }, +} + +export const CLICKUP_TAG_OUTPUT_PROPERTIES: Record = { + name: { type: 'string', description: 'Tag name', nullable: true }, + tagFg: { type: 'string', description: 'Tag foreground color', nullable: true }, + tagBg: { type: 'string', description: 'Tag background color', nullable: true }, +} + +export function mapClickUpUser(value: unknown): ClickUpUser | null { + if (!isRecordLike(value)) { + return null + } + + return { + id: getOptionalNumber(value.id), + username: getOptionalString(value.username), + email: getOptionalString(value.email), + profilePicture: getOptionalString(value.profilePicture), + } +} + +function mapClickUpStatus(value: unknown): ClickUpStatus | null { + if (!isRecordLike(value)) { + return null + } + + return { + status: getOptionalString(value.status), + color: getOptionalString(value.color), + type: getOptionalString(value.type), + } +} + +function mapClickUpPriority(value: unknown): ClickUpPriority | null { + if (!isRecordLike(value)) { + return null + } + + return { + id: getOptionalString(value.id), + priority: getOptionalString(value.priority) ?? getOptionalString(value.name), + color: getOptionalString(value.color), + } +} + +export function mapClickUpTag(value: unknown): ClickUpTag | null { + if (!isRecordLike(value)) { + return null + } + + return { + name: getOptionalString(value.name), + tagFg: getOptionalString(value.tag_fg), + tagBg: getOptionalString(value.tag_bg), + } +} + +function mapIdName(value: unknown): { id: string; name: string | null } | null { + if (!isRecordLike(value)) { + return null + } + + const id = getOptionalString(value.id) + if (!id) { + return null + } + + return { id, name: getOptionalString(value.name) } +} + +export function mapClickUpTask(value: unknown): ClickUpTask { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid task object') + } + + const rawAssignees = Array.isArray(value.assignees) ? value.assignees : [] + const rawTags = Array.isArray(value.tags) ? value.tags : [] + + return { + id: getRequiredString(value.id, 'id'), + customId: getOptionalString(value.custom_id), + name: getRequiredString(value.name, 'name'), + textContent: getOptionalString(value.text_content), + description: getOptionalString(value.description), + status: mapClickUpStatus(value.status), + archived: typeof value.archived === 'boolean' ? value.archived : false, + creator: mapClickUpUser(value.creator), + assignees: rawAssignees + .map((assignee) => mapClickUpUser(assignee)) + .filter((assignee): assignee is ClickUpUser => assignee !== null), + tags: rawTags + .map((tag) => mapClickUpTag(tag)) + .filter((tag): tag is ClickUpTag => tag !== null), + parent: getOptionalString(value.parent), + priority: mapClickUpPriority(value.priority), + dueDate: getOptionalString(value.due_date), + startDate: getOptionalString(value.start_date), + points: getOptionalNumber(value.points), + timeEstimate: getOptionalNumber(value.time_estimate), + dateCreated: getOptionalString(value.date_created), + dateUpdated: getOptionalString(value.date_updated), + dateClosed: getOptionalString(value.date_closed), + dateDone: getOptionalString(value.date_done), + list: mapIdName(value.list), + url: getOptionalString(value.url), + } +} + +export function mapClickUpComment(value: unknown): ClickUpComment { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid comment object') + } + + return { + id: getRequiredString(value.id, 'id'), + commentText: getOptionalString(value.comment_text), + resolved: getOptionalBoolean(value.resolved), + user: mapClickUpUser(value.user), + assignee: mapClickUpUser(value.assignee), + date: getOptionalString(value.date), + replyCount: getOptionalString(value.reply_count), + } +} + +export function mapClickUpWorkspace(value: unknown): ClickUpWorkspace { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid workspace object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getOptionalString(value.name), + color: getOptionalString(value.color), + avatar: getOptionalString(value.avatar), + } +} + +export function mapClickUpSpace(value: unknown): ClickUpSpace { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid space object') + } + + const rawStatuses = Array.isArray(value.statuses) ? value.statuses : [] + + return { + id: getRequiredString(value.id, 'id'), + name: getOptionalString(value.name), + private: getOptionalBoolean(value.private), + archived: getOptionalBoolean(value.archived), + statuses: rawStatuses + .map((status) => mapClickUpStatus(status)) + .filter((status): status is ClickUpStatus => status !== null), + } +} + +export function mapClickUpFolder(value: unknown): ClickUpFolder { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid folder object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getOptionalString(value.name), + hidden: getOptionalBoolean(value.hidden), + taskCount: getOptionalString(value.task_count), + space: mapIdName(value.space), + } +} + +export function mapClickUpList(value: unknown): ClickUpList { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid list object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getOptionalString(value.name), + taskCount: getOptionalString(value.task_count), + archived: getOptionalBoolean(value.archived), + } +} + +export function mapClickUpMember(value: unknown): ClickUpMember { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid member object') + } + + return { + id: getOptionalNumber(value.id), + username: getOptionalString(value.username), + email: getOptionalString(value.email), + color: getOptionalString(value.color), + initials: getOptionalString(value.initials), + profilePicture: getOptionalString(value.profilePicture), + } +} + +export function mapClickUpCustomField(value: unknown): ClickUpCustomField { + if (!isRecordLike(value)) { + throw new Error('ClickUp returned an invalid custom field object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getOptionalString(value.name), + type: getOptionalString(value.type), + typeConfig: isRecordLike(value.type_config) ? value.type_config : null, + dateCreated: getOptionalString(value.date_created), + hideFromGuests: getOptionalBoolean(value.hide_from_guests), + required: getOptionalBoolean(value.required), + } +} + +export function extractClickUpErrorMessage( + response: Response, + data: unknown, + fallback: string +): string { + if (isRecordLike(data)) { + const err = data.err + const ecode = data.ECODE + + if (typeof err === 'string' && err.trim().length > 0) { + return typeof ecode === 'string' && ecode.trim().length > 0 + ? `${fallback}: ${err} (${ecode})` + : `${fallback}: ${err}` + } + } + + if (response.statusText) { + return `${fallback}: ${response.status} ${response.statusText}` + } + + return `${fallback}: ${response.status}` +} diff --git a/apps/sim/tools/clickup/types.ts b/apps/sim/tools/clickup/types.ts new file mode 100644 index 00000000000..bf5cd80338a --- /dev/null +++ b/apps/sim/tools/clickup/types.ts @@ -0,0 +1,430 @@ +import type { UserFile } from '@/executor/types' +import type { ToolResponse } from '@/tools/types' + +export interface ClickUpUser { + id: number | null + username: string | null + email: string | null + profilePicture: string | null +} + +export interface ClickUpMember { + id: number | null + username: string | null + email: string | null + color: string | null + initials: string | null + profilePicture: string | null +} + +export interface ClickUpStatus { + status: string | null + color: string | null + type: string | null +} + +export interface ClickUpPriority { + id: string | null + priority: string | null + color: string | null +} + +export interface ClickUpTag { + name: string | null + tagFg: string | null + tagBg: string | null +} + +export interface ClickUpTask { + id: string + customId: string | null + name: string + textContent: string | null + description: string | null + status: ClickUpStatus | null + archived: boolean + creator: ClickUpUser | null + assignees: ClickUpUser[] + tags: ClickUpTag[] + parent: string | null + priority: ClickUpPriority | null + dueDate: string | null + startDate: string | null + points: number | null + timeEstimate: number | null + dateCreated: string | null + dateUpdated: string | null + dateClosed: string | null + dateDone: string | null + list: { id: string; name: string | null } | null + url: string | null +} + +export interface ClickUpComment { + id: string + commentText: string | null + resolved: boolean | null + user: ClickUpUser | null + assignee: ClickUpUser | null + date: string | null + replyCount: string | null +} + +export interface ClickUpWorkspace { + id: string + name: string | null + color: string | null + avatar: string | null +} + +export interface ClickUpSpace { + id: string + name: string | null + private: boolean | null + archived: boolean | null + statuses: ClickUpStatus[] +} + +export interface ClickUpFolder { + id: string + name: string | null + hidden: boolean | null + taskCount: string | null + space: { id: string; name: string | null } | null +} + +export interface ClickUpList { + id: string + name: string | null + taskCount: string | null + archived: boolean | null +} + +export interface ClickUpAttachment { + id: string + title: string | null + extension: string | null + url: string | null + date: number | null +} + +export interface ClickUpCustomField { + id: string + name: string | null + type: string | null + typeConfig: Record | null + dateCreated: string | null + hideFromGuests: boolean | null + required: boolean | null +} + +export interface ClickUpCreateTaskParams { + accessToken: string + listId: string + name: string + description?: string + markdownContent?: string + status?: string + priority?: number + dueDate?: number + startDate?: number + assignees?: number[] + tags?: string[] + timeEstimate?: number + parent?: string + notifyAll?: boolean +} + +export interface ClickUpTaskResponse extends ToolResponse { + output: { + task?: ClickUpTask + error?: string + } +} + +export interface ClickUpGetTaskParams { + accessToken: string + taskId: string + includeSubtasks?: boolean + includeMarkdownDescription?: boolean +} + +export interface ClickUpUpdateTaskParams { + accessToken: string + taskId: string + name?: string + description?: string + markdownContent?: string + status?: string + priority?: number + dueDate?: number + startDate?: number + timeEstimate?: number + points?: number + parent?: string + archived?: boolean +} + +export interface ClickUpDeleteTaskParams { + accessToken: string + taskId: string +} + +export interface ClickUpDeleteResponse extends ToolResponse { + output: { + id?: string + deleted?: boolean + error?: string + } +} + +export interface ClickUpGetTasksParams { + accessToken: string + listId: string + page?: number + orderBy?: string + reverse?: boolean + subtasks?: boolean + includeClosed?: boolean + archived?: boolean + statuses?: string[] +} + +export interface ClickUpTaskListResponse extends ToolResponse { + output: { + tasks?: ClickUpTask[] + error?: string + } +} + +export interface ClickUpSearchTasksParams { + accessToken: string + workspaceId: string + page?: number + orderBy?: string + reverse?: boolean + subtasks?: boolean + listIds?: string[] + spaceIds?: string[] + folderIds?: string[] +} + +export interface ClickUpCreateCommentParams { + accessToken: string + taskId: string + commentText: string + assignee?: number + notifyAll?: boolean +} + +export interface ClickUpCreateCommentResponse extends ToolResponse { + output: { + id?: string + histId?: string + date?: number + error?: string + } +} + +export interface ClickUpGetCommentsParams { + accessToken: string + taskId: string + start?: number + startId?: string +} + +export interface ClickUpCommentListResponse extends ToolResponse { + output: { + comments?: ClickUpComment[] + error?: string + } +} + +export interface ClickUpUpdateCommentParams { + accessToken: string + commentId: string + commentText?: string + assignee?: number + resolved?: boolean +} + +export interface ClickUpUpdateCommentResponse extends ToolResponse { + output: { + id?: string + updated?: boolean + error?: string + } +} + +export interface ClickUpDeleteCommentParams { + accessToken: string + commentId: string +} + +export interface ClickUpUploadAttachmentParams { + accessToken: string + taskId: string + file: UserFile | string +} + +export interface ClickUpUploadAttachmentResponse extends ToolResponse { + output: { + attachment?: ClickUpAttachment + files?: UserFile[] + error?: string + } +} + +export interface ClickUpGetWorkspacesParams { + accessToken: string +} + +export interface ClickUpWorkspaceListResponse extends ToolResponse { + output: { + workspaces?: ClickUpWorkspace[] + error?: string + } +} + +export interface ClickUpGetSpacesParams { + accessToken: string + workspaceId: string + archived?: boolean +} + +export interface ClickUpSpaceListResponse extends ToolResponse { + output: { + spaces?: ClickUpSpace[] + error?: string + } +} + +export interface ClickUpGetFoldersParams { + accessToken: string + spaceId: string + archived?: boolean +} + +export interface ClickUpFolderListResponse extends ToolResponse { + output: { + folders?: ClickUpFolder[] + error?: string + } +} + +export interface ClickUpCreateFolderParams { + accessToken: string + spaceId: string + name: string +} + +export interface ClickUpFolderResponse extends ToolResponse { + output: { + folder?: ClickUpFolder + error?: string + } +} + +export interface ClickUpGetListsParams { + accessToken: string + folderId?: string + spaceId?: string + archived?: boolean +} + +export interface ClickUpListListResponse extends ToolResponse { + output: { + lists?: ClickUpList[] + error?: string + } +} + +export interface ClickUpCreateListParams { + accessToken: string + folderId?: string + spaceId?: string + name: string + content?: string + markdownContent?: string +} + +export interface ClickUpListResponse extends ToolResponse { + output: { + list?: ClickUpList + error?: string + } +} + +export interface ClickUpGetSpaceTagsParams { + accessToken: string + spaceId: string +} + +export interface ClickUpTagListResponse extends ToolResponse { + output: { + tags?: ClickUpTag[] + error?: string + } +} + +export interface ClickUpTaskTagParams { + accessToken: string + taskId: string + tagName: string +} + +export interface ClickUpTaskTagResponse extends ToolResponse { + output: { + taskId?: string + tagName?: string + error?: string + } +} + +export interface ClickUpGetTaskMembersParams { + accessToken: string + taskId: string +} + +export interface ClickUpGetListMembersParams { + accessToken: string + listId: string +} + +export interface ClickUpMemberListResponse extends ToolResponse { + output: { + members?: ClickUpMember[] + error?: string + } +} + +export interface ClickUpGetCustomFieldsParams { + accessToken: string + listId: string +} + +export interface ClickUpCustomFieldListResponse extends ToolResponse { + output: { + fields?: ClickUpCustomField[] + error?: string + } +} + +export type ClickUpResponse = + | ClickUpTaskResponse + | ClickUpTaskListResponse + | ClickUpDeleteResponse + | ClickUpCreateCommentResponse + | ClickUpCommentListResponse + | ClickUpUpdateCommentResponse + | ClickUpUploadAttachmentResponse + | ClickUpWorkspaceListResponse + | ClickUpSpaceListResponse + | ClickUpFolderListResponse + | ClickUpFolderResponse + | ClickUpListListResponse + | ClickUpListResponse + | ClickUpTagListResponse + | ClickUpTaskTagResponse + | ClickUpMemberListResponse + | ClickUpCustomFieldListResponse diff --git a/apps/sim/tools/clickup/update_comment.ts b/apps/sim/tools/clickup/update_comment.ts new file mode 100644 index 00000000000..b05c9ddd197 --- /dev/null +++ b/apps/sim/tools/clickup/update_comment.ts @@ -0,0 +1,91 @@ +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, +} from '@/tools/clickup/shared' +import type { ClickUpUpdateCommentParams, ClickUpUpdateCommentResponse } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupUpdateCommentTool: ToolConfig< + ClickUpUpdateCommentParams, + ClickUpUpdateCommentResponse +> = { + id: 'clickup_update_comment', + name: 'ClickUp Update Comment', + description: 'Update the content, assignee, or resolved state of a ClickUp task comment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the comment to update', + }, + commentText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New content for the comment', + }, + assignee: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'User ID to assign the comment to', + }, + resolved: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the comment is resolved', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/comment/${encodeURIComponent(params.commentId)}`, + method: 'PUT', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + + if (params.commentText !== undefined) body.comment_text = params.commentText + if (params.assignee !== undefined) body.assignee = params.assignee + if (params.resolved !== undefined) body.resolved = params.resolved + + return body + }, + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const data = await response.json().catch(() => null) + const error = extractClickUpErrorMessage(response, data, 'Failed to update comment') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { id: params?.commentId, updated: true }, + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the updated comment', optional: true }, + updated: { type: 'boolean', description: 'Whether the comment was updated', optional: true }, + }, +} diff --git a/apps/sim/tools/clickup/update_task.ts b/apps/sim/tools/clickup/update_task.ts new file mode 100644 index 00000000000..90d0a5270d8 --- /dev/null +++ b/apps/sim/tools/clickup/update_task.ts @@ -0,0 +1,151 @@ +import { + CLICKUP_API_BASE_URL, + CLICKUP_TASK_OUTPUT_PROPERTIES, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpTask, +} from '@/tools/clickup/shared' +import type { ClickUpTaskResponse, ClickUpUpdateTaskParams } from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupUpdateTaskTool: ToolConfig = { + id: 'clickup_update_task', + name: 'ClickUp Update Task', + description: 'Update an existing task in ClickUp', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the task', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New plain text description (use a single space to clear)', + }, + markdownContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New Markdown description (takes precedence over description)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New status for the task (must exist in the list)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Priority: 1 (urgent), 2 (high), 3 (normal), 4 (low)', + }, + dueDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New due date as a Unix timestamp in milliseconds', + }, + startDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New start date as a Unix timestamp in milliseconds', + }, + timeEstimate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New time estimate in milliseconds', + }, + points: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New sprint points value', + }, + parent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Parent task ID to move this task under (cannot be cleared)', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Set to true to archive the task, false to unarchive', + }, + }, + + request: { + url: (params) => `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}`, + method: 'PUT', + headers: (params) => ({ + Authorization: clickupAuthorizationHeader(params.accessToken), + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + + if (params.name !== undefined) body.name = params.name + if (params.description !== undefined) body.description = params.description + if (params.markdownContent !== undefined) body.markdown_content = params.markdownContent + if (params.status !== undefined) body.status = params.status + if (params.priority !== undefined) body.priority = params.priority + if (params.dueDate !== undefined) body.due_date = params.dueDate + if (params.startDate !== undefined) body.start_date = params.startDate + if (params.timeEstimate !== undefined) body.time_estimate = params.timeEstimate + if (params.points !== undefined) body.points = params.points + if (params.parent !== undefined) body.parent = params.parent + if (params.archived !== undefined) body.archived = params.archived + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractClickUpErrorMessage(response, data, 'Failed to update task') + return { success: false, output: { error }, error } + } + + return { + success: true, + output: { task: mapClickUpTask(data) }, + } + }, + + outputs: { + task: { + type: 'json', + description: 'The updated task', + optional: true, + properties: CLICKUP_TASK_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/clickup/upload_attachment.ts b/apps/sim/tools/clickup/upload_attachment.ts new file mode 100644 index 00000000000..f620a53de20 --- /dev/null +++ b/apps/sim/tools/clickup/upload_attachment.ts @@ -0,0 +1,86 @@ +import type { + ClickUpUploadAttachmentParams, + ClickUpUploadAttachmentResponse, +} from '@/tools/clickup/types' +import type { ToolConfig } from '@/tools/types' + +export const clickupUploadAttachmentTool: ToolConfig< + ClickUpUploadAttachmentParams, + ClickUpUploadAttachmentResponse +> = { + id: 'clickup_upload_attachment', + name: 'ClickUp Upload Attachment', + description: 'Upload a file to a ClickUp task as an attachment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'clickup', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token or personal API token for ClickUp', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the task to attach the file to', + }, + file: { + type: 'file', + required: true, + visibility: 'user-only', + description: 'File to attach to the task', + }, + }, + + request: { + url: '/api/tools/clickup/upload-attachment', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + taskId: params.taskId, + file: params.file, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok || !data.success) { + throw new Error(data.error || 'Failed to upload ClickUp attachment') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + attachment: { + type: 'json', + description: 'The created attachment', + optional: true, + properties: { + id: { type: 'string', description: 'Attachment ID' }, + title: { type: 'string', description: 'Attachment title', nullable: true }, + extension: { type: 'string', description: 'File extension', nullable: true }, + url: { type: 'string', description: 'URL of the uploaded attachment', nullable: true }, + date: { + type: 'number', + description: 'Upload timestamp (Unix ms)', + nullable: true, + }, + }, + }, + files: { type: 'file[]', description: 'The uploaded attachment file' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 28318b68ef0..1e532525661 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -501,6 +501,31 @@ import { clickhouseTruncateTableTool, clickhouseUpdateTool, } from '@/tools/clickhouse' +import { + clickupAddTagToTaskTool, + clickupCreateCommentTool, + clickupCreateFolderTool, + clickupCreateListTool, + clickupCreateTaskTool, + clickupDeleteCommentTool, + clickupDeleteTaskTool, + clickupGetCommentsTool, + clickupGetCustomFieldsTool, + clickupGetFoldersTool, + clickupGetListMembersTool, + clickupGetListsTool, + clickupGetSpaceTagsTool, + clickupGetSpacesTool, + clickupGetTaskTool, + clickupGetTaskMembersTool, + clickupGetTasksTool, + clickupGetWorkspacesTool, + clickupRemoveTagFromTaskTool, + clickupSearchTasksTool, + clickupUpdateCommentTool, + clickupUpdateTaskTool, + clickupUploadAttachmentTool, +} from '@/tools/clickup' import { cloudflareCreateDnsRecordTool, cloudflareCreateZoneTool, @@ -7536,6 +7561,29 @@ export const tools: Record = { clerk_get_jwt_template: clerkGetJwtTemplateTool, clerk_create_actor_token: clerkCreateActorTokenTool, clerk_revoke_actor_token: clerkRevokeActorTokenTool, + clickup_create_task: clickupCreateTaskTool, + clickup_get_task: clickupGetTaskTool, + clickup_update_task: clickupUpdateTaskTool, + clickup_delete_task: clickupDeleteTaskTool, + clickup_get_tasks: clickupGetTasksTool, + clickup_search_tasks: clickupSearchTasksTool, + clickup_create_comment: clickupCreateCommentTool, + clickup_get_comments: clickupGetCommentsTool, + clickup_update_comment: clickupUpdateCommentTool, + clickup_delete_comment: clickupDeleteCommentTool, + clickup_upload_attachment: clickupUploadAttachmentTool, + clickup_add_tag_to_task: clickupAddTagToTaskTool, + clickup_remove_tag_from_task: clickupRemoveTagFromTaskTool, + clickup_get_space_tags: clickupGetSpaceTagsTool, + clickup_get_task_members: clickupGetTaskMembersTool, + clickup_get_list_members: clickupGetListMembersTool, + clickup_get_custom_fields: clickupGetCustomFieldsTool, + clickup_get_workspaces: clickupGetWorkspacesTool, + clickup_get_spaces: clickupGetSpacesTool, + clickup_get_folders: clickupGetFoldersTool, + clickup_get_lists: clickupGetListsTool, + clickup_create_folder: clickupCreateFolderTool, + clickup_create_list: clickupCreateListTool, cloudflare_list_zones: cloudflareListZonesTool, cloudflare_get_zone: cloudflareGetZoneTool, cloudflare_create_zone: cloudflareCreateZoneTool, From 99fb4f0f33e5315451b3de35e46dea7237858ef6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Jul 2026 20:03:36 -0700 Subject: [PATCH 02/13] fix(clickup): apply validation-audit fixes across tools, block, and upload route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../integrations/clickup-service-account.mdx | 81 ------------ .../content/docs/en/integrations/clickup.mdx | 32 +++-- .../content/docs/en/integrations/meta.json | 2 + .../tools/clickup/upload-attachment/route.ts | 37 ++++-- apps/sim/blocks/blocks/clickup.ts | 118 ++++++++++++++++-- apps/sim/lib/api/contracts/tools/clickup.ts | 3 + apps/sim/lib/integrations/integrations.json | 116 +++++++++++++++++ apps/sim/tools/clickup/create_comment.ts | 7 +- apps/sim/tools/clickup/create_folder.ts | 3 +- apps/sim/tools/clickup/create_list.ts | 10 +- apps/sim/tools/clickup/create_task.ts | 17 ++- apps/sim/tools/clickup/get_custom_fields.ts | 5 - apps/sim/tools/clickup/get_lists.ts | 3 +- apps/sim/tools/clickup/get_space_tags.ts | 114 ++++++++--------- apps/sim/tools/clickup/get_tasks.ts | 47 ++++++- apps/sim/tools/clickup/get_workspaces.ts | 5 +- .../sim/tools/clickup/remove_tag_from_task.ts | 108 ++++++++-------- apps/sim/tools/clickup/search_tasks.ts | 68 +++++++++- apps/sim/tools/clickup/shared.ts | 89 +++++++++++-- apps/sim/tools/clickup/types.ts | 27 +++- apps/sim/tools/clickup/update_comment.ts | 11 +- apps/sim/tools/clickup/update_task.ts | 40 ++++++ apps/sim/tools/clickup/upload_attachment.ts | 3 + apps/sim/tools/registry.ts | 4 +- 24 files changed, 705 insertions(+), 245 deletions(-) delete mode 100644 apps/docs/content/docs/en/integrations/clickup-service-account.mdx diff --git a/apps/docs/content/docs/en/integrations/clickup-service-account.mdx b/apps/docs/content/docs/en/integrations/clickup-service-account.mdx deleted file mode 100644 index 4d06900ecb4..00000000000 --- a/apps/docs/content/docs/en/integrations/clickup-service-account.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: ClickUp API Tokens -description: Connect ClickUp to Sim with a personal API token — ideally created from a dedicated service user for production workflows ---- - -import { Callout } from 'fumadocs-ui/components/callout' -import { Step, Steps } from 'fumadocs-ui/components/steps' -import { Image } from '@/components/ui/image' -import { FAQ } from '@/components/ui/faq' - -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. - -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. - -## Prerequisites - -A ClickUp account with access to the workspaces your workflows need. Any user can generate a personal API token from their settings. - -## Creating the API Token - - - - Log in as the service user, click your avatar in ClickUp, and open **Settings** - - {/* TODO(screenshot): ClickUp avatar menu with Settings highlighted */} - - - In the sidebar, go to **Apps** (labeled **API Token** in some plans) - - - Click **Generate** to create your personal token - - {/* TODO(screenshot): ClickUp Apps page with the Generate API token button visible */} - - - Copy the token — it starts with `pk_` — and store it somewhere safe. - - - - -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. - - -## Adding the API Token to Sim - - - - Open your workspace **Settings** and go to the **Integrations** tab - - - Search for "ClickUp Service Account" and click it, then click **Add to Sim** and choose **Add API token** - - {/* TODO(screenshot): Integrations page with "ClickUp Service Account" in the service list */} - - - Paste the API token (`pk_...`) and optionally set a display name and description - - {/* TODO(screenshot): Add ClickUp API token dialog with the token filled in */} - - - 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. - - - -The token is encrypted before being stored. - -## Using the Service Account in Workflows - -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. - -{/* TODO(screenshot): ClickUp block in a workflow with the service account selected as the credential */} - -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. - - diff --git a/apps/docs/content/docs/en/integrations/clickup.mdx b/apps/docs/content/docs/en/integrations/clickup.mdx index 5b75cbc33c2..610f3ec9683 100644 --- a/apps/docs/content/docs/en/integrations/clickup.mdx +++ b/apps/docs/content/docs/en/integrations/clickup.mdx @@ -33,12 +33,14 @@ Create a new task in a ClickUp list | `status` | string | No | Status to create the task with \(must exist in the list\) | | `priority` | number | No | Priority: 1 \(urgent\), 2 \(high\), 3 \(normal\), 4 \(low\) | | `dueDate` | number | No | Due date as a Unix timestamp in milliseconds | +| `dueDateTime` | boolean | No | Whether the due date includes a time of day | | `startDate` | number | No | Start date as a Unix timestamp in milliseconds | +| `startDateTime` | boolean | No | Whether the start date includes a time of day | | `assignees` | array | No | User IDs to assign to the task | | `tags` | array | No | Tag names to apply to the task | | `timeEstimate` | number | No | Time estimate in milliseconds | | `parent` | string | No | Parent task ID to create this task as a subtask | -| `notifyAll` | boolean | No | Whether to notify the task creator on creation | +| `notifyAll` | boolean | No | When true, creation notifications are sent to everyone, including the task creator | #### Output @@ -79,11 +81,15 @@ Update an existing task in ClickUp | `status` | string | No | New status for the task \(must exist in the list\) | | `priority` | number | No | Priority: 1 \(urgent\), 2 \(high\), 3 \(normal\), 4 \(low\) | | `dueDate` | number | No | New due date as a Unix timestamp in milliseconds | +| `dueDateTime` | boolean | No | Whether the due date includes a time of day | | `startDate` | number | No | New start date as a Unix timestamp in milliseconds | +| `startDateTime` | boolean | No | Whether the start date includes a time of day | | `timeEstimate` | number | No | New time estimate in milliseconds | | `points` | number | No | New sprint points value | | `parent` | string | No | Parent task ID to move this task under \(cannot be cleared\) | | `archived` | boolean | No | Set to true to archive the task, false to unarchive | +| `assigneesToAdd` | array | No | User IDs to add as assignees | +| `assigneesToRemove` | array | No | User IDs to remove from assignees | #### Output @@ -110,7 +116,7 @@ Delete a task from ClickUp ### `clickup_get_tasks` -List the tasks in a ClickUp list (100 tasks per page) +List the tasks in a ClickUp list (100 tasks per page; increment page until an empty result to paginate) #### Input @@ -124,6 +130,10 @@ List the tasks in a ClickUp list (100 tasks per page) | `includeClosed` | boolean | No | Include closed tasks \(excluded by default\) | | `archived` | boolean | No | Return archived tasks | | `statuses` | array | No | Filter tasks by status names | +| `assignees` | array | No | Filter tasks by assignee user IDs | +| `tags` | array | No | Filter tasks by tag names | +| `dueDateGt` | number | No | Only tasks due after this Unix timestamp in milliseconds | +| `dueDateLt` | number | No | Only tasks due before this Unix timestamp in milliseconds | #### Output @@ -133,7 +143,7 @@ List the tasks in a ClickUp list (100 tasks per page) ### `clickup_search_tasks` -Search tasks across a ClickUp workspace, filtered by lists, folders, or spaces (100 tasks per page) +Search tasks across a ClickUp workspace with filters for lists, folders, spaces, statuses, assignees, tags, and due dates (100 tasks per page; increment page until an empty result to paginate) #### Input @@ -144,9 +154,15 @@ Search tasks across a ClickUp workspace, filtered by lists, folders, or spaces ( | `orderBy` | string | No | Order by field: id, created, updated, or due_date | | `reverse` | boolean | No | Return tasks in reverse order | | `subtasks` | boolean | No | Include subtasks \(excluded by default\) | +| `includeClosed` | boolean | No | Include closed tasks \(excluded by default\) | | `listIds` | array | No | Filter by list IDs | | `spaceIds` | array | No | Filter by space IDs | | `folderIds` | array | No | Filter by folder IDs | +| `statuses` | array | No | Filter tasks by status names | +| `assignees` | array | No | Filter tasks by assignee user IDs | +| `tags` | array | No | Filter tasks by tag names | +| `dueDateGt` | number | No | Only tasks due after this Unix timestamp in milliseconds | +| `dueDateLt` | number | No | Only tasks due before this Unix timestamp in milliseconds | #### Output @@ -165,7 +181,7 @@ Add a comment to a ClickUp task | `taskId` | string | Yes | ID of the task to comment on | | `commentText` | string | Yes | Content of the comment | | `assignee` | number | No | User ID to assign the comment to | -| `notifyAll` | boolean | No | Whether to also notify the comment creator \(assignees and watchers are always notified\) | +| `notifyAll` | boolean | No | When true, comment notifications are sent to everyone, including the comment creator | #### Output @@ -247,10 +263,13 @@ Upload a file to a ClickUp task as an attachment | --------- | ---- | ----------- | | `attachment` | json | The created attachment | | ↳ `id` | string | Attachment ID | +| ↳ `version` | string | Attachment version | | ↳ `title` | string | Attachment title | | ↳ `extension` | string | File extension | | ↳ `url` | string | URL of the uploaded attachment | | ↳ `date` | number | Upload timestamp \(Unix ms\) | +| ↳ `thumbnailSmall` | string | Small thumbnail URL | +| ↳ `thumbnailLarge` | string | Large thumbnail URL | | `files` | file[] | The uploaded attachment file | ### `clickup_add_tag_to_task` @@ -358,7 +377,6 @@ List the custom fields accessible in a ClickUp list | ↳ `typeConfig` | json | Type-specific configuration \(e.g. dropdown options\) | | ↳ `dateCreated` | string | Creation timestamp \(Unix ms\) | | ↳ `hideFromGuests` | boolean | Whether the field is hidden from guests | -| ↳ `required` | boolean | Whether the field is required | ### `clickup_get_workspaces` @@ -429,7 +447,7 @@ List the lists in a ClickUp folder, or the folderless lists in a space when a sp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `folderId` | string | No | ID of the folder to list lists from \(provide this or spaceId\) | +| `folderId` | string | No | ID of the folder to list lists from \(provide this or spaceId; folderId takes precedence when both are set\) | | `spaceId` | string | No | ID of the space to list folderless lists from \(provide this or folderId\) | | `archived` | boolean | No | Return archived lists | @@ -464,7 +482,7 @@ Create a new list in a ClickUp folder, or a folderless list in a space when a sp | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `folderId` | string | No | ID of the folder to create the list in \(provide this or spaceId\) | +| `folderId` | string | No | ID of the folder to create the list in \(provide this or spaceId; folderId takes precedence when both are set\) | | `spaceId` | string | No | ID of the space to create a folderless list in \(provide this or folderId\) | | `name` | string | Yes | Name of the list | | `content` | string | No | Plain text description of the list | diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 28522766552..b1e93bab1b5 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -38,6 +38,8 @@ "clickhouse", "clickup", "clickup-service-account", + "clickup", + "clickup-service-account", "cloudflare", "cloudformation", "cloudwatch", diff --git a/apps/sim/app/api/tools/clickup/upload-attachment/route.ts b/apps/sim/app/api/tools/clickup/upload-attachment/route.ts index 5e599857d07..b5a1d0ee207 100644 --- a/apps/sim/app/api/tools/clickup/upload-attachment/route.ts +++ b/apps/sim/app/api/tools/clickup/upload-attachment/route.ts @@ -10,12 +10,27 @@ import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/ import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' -import { clickupAuthorizationHeader, extractClickUpErrorMessage } from '@/tools/clickup/shared' +import { + CLICKUP_API_BASE_URL, + clickupAuthorizationHeader, + extractClickUpErrorMessage, + mapClickUpAttachment, +} from '@/tools/clickup/shared' export const dynamic = 'force-dynamic' const logger = createLogger('ClickUpUploadAttachmentAPI') +const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024 + +function uploadSizeError(bytes: number): NextResponse { + const sizeMB = (bytes / (1024 * 1024)).toFixed(2) + return NextResponse.json( + { success: false, error: `File size (${sizeMB}MB) exceeds upload limit of 100MB` }, + { status: 400 } + ) +} + export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -44,6 +59,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) if (denied) return denied + if (userFile.size > MAX_UPLOAD_SIZE_BYTES) { + return uploadSizeError(userFile.size) + } + let buffer: Buffer let downloadedContentType = '' try { @@ -56,13 +75,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => { throw error } + if (buffer.length > MAX_UPLOAD_SIZE_BYTES) { + return uploadSizeError(buffer.length) + } + const formData = new FormData() const blob = new Blob([new Uint8Array(buffer)], { type: downloadedContentType || userFile.type || 'application/octet-stream', }) formData.append('attachment', blob, userFile.name) - const url = `https://api.clickup.com/api/v2/task/${encodeURIComponent(params.taskId)}/attachment` + const url = `${CLICKUP_API_BASE_URL}/task/${encodeURIComponent(params.taskId)}/attachment` const response = await fetch(url, { method: 'POST', headers: { @@ -85,18 +108,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: message }, { status: response.status }) } - const record = (data ?? {}) as Record - return NextResponse.json({ success: true, output: { - attachment: { - id: typeof record.id === 'string' ? record.id : '', - title: typeof record.title === 'string' ? record.title : null, - extension: typeof record.extension === 'string' ? record.extension : null, - url: typeof record.url === 'string' ? record.url : null, - date: typeof record.date === 'number' ? record.date : null, - }, + attachment: mapClickUpAttachment(data), files: userFiles, }, }) diff --git a/apps/sim/blocks/blocks/clickup.ts b/apps/sim/blocks/blocks/clickup.ts index b88877988d9..6eb3591816d 100644 --- a/apps/sim/blocks/blocks/clickup.ts +++ b/apps/sim/blocks/blocks/clickup.ts @@ -203,7 +203,7 @@ export const ClickUpBlock: BlockConfig = { placeholder: 'Markdown description (overrides Description)', condition: { field: 'operation', - value: ['create_task', 'update_task'], + value: ['create_task', 'update_task', 'create_list'], }, }, { @@ -273,7 +273,29 @@ export const ClickUpBlock: BlockConfig = { placeholder: 'Comma-separated user IDs (e.g. 183, 184)', condition: { field: 'operation', - value: ['create_task'], + value: ['create_task', 'get_tasks', 'search_tasks'], + }, + }, + { + id: 'assigneesToAdd', + title: 'Add Assignees', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated user IDs to add', + condition: { + field: 'operation', + value: ['update_task'], + }, + }, + { + id: 'assigneesToRemove', + title: 'Remove Assignees', + type: 'short-input', + mode: 'advanced', + placeholder: 'Comma-separated user IDs to remove', + condition: { + field: 'operation', + value: ['update_task'], }, }, { @@ -283,7 +305,7 @@ export const ClickUpBlock: BlockConfig = { placeholder: 'Comma-separated tag names', condition: { field: 'operation', - value: ['create_task'], + value: ['create_task', 'get_tasks', 'search_tasks'], }, }, { @@ -319,9 +341,29 @@ export const ClickUpBlock: BlockConfig = { value: ['create_task', 'update_task'], }, }, + { + id: 'dueDateTime', + title: 'Due Date Has Time', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, + { + id: 'startDateTime', + title: 'Start Date Has Time', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_task', 'update_task'], + }, + }, { id: 'notifyAll', - title: 'Notify Creator', + title: 'Notify All', type: 'switch', mode: 'advanced', condition: { @@ -420,7 +462,7 @@ export const ClickUpBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['get_tasks'], + value: ['get_tasks', 'search_tasks'], }, }, { @@ -441,7 +483,41 @@ export const ClickUpBlock: BlockConfig = { placeholder: 'Comma-separated status names to filter by', condition: { field: 'operation', - value: ['get_tasks'], + value: ['get_tasks', 'search_tasks'], + }, + }, + { + id: 'dueDateGt', + title: 'Due After', + type: 'short-input', + mode: 'advanced', + placeholder: 'Unix timestamp in milliseconds', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + wandConfig: { + enabled: true, + prompt: TIMESTAMP_WAND_PROMPT, + placeholder: 'Describe the earliest due date (e.g., "start of this week")...', + generationType: 'timestamp', + }, + }, + { + id: 'dueDateLt', + title: 'Due Before', + type: 'short-input', + mode: 'advanced', + placeholder: 'Unix timestamp in milliseconds', + condition: { + field: 'operation', + value: ['get_tasks', 'search_tasks'], + }, + wandConfig: { + enabled: true, + prompt: TIMESTAMP_WAND_PROMPT, + placeholder: 'Describe the latest due date (e.g., "end of next week")...', + generationType: 'timestamp', }, }, { @@ -481,7 +557,7 @@ export const ClickUpBlock: BlockConfig = { id: 'commentText', title: 'Comment Text', type: 'long-input', - required: { field: 'operation', value: ['create_comment'] }, + required: { field: 'operation', value: ['create_comment', 'update_comment'] }, placeholder: 'Enter comment text', condition: { field: 'operation', @@ -635,7 +711,9 @@ export const ClickUpBlock: BlockConfig = { status: params.status || undefined, priority: priorityValue, dueDate: optionalNumber(params.dueDate), + dueDateTime: params.dueDateTime ? true : undefined, startDate: optionalNumber(params.startDate), + startDateTime: params.startDateTime ? true : undefined, assignees: splitCommaSeparatedNumbers(params.assignees), tags: splitCommaSeparated(params.tags), timeEstimate: optionalNumber(params.timeEstimate), @@ -659,10 +737,14 @@ export const ClickUpBlock: BlockConfig = { status: params.status || undefined, priority: priorityValue, dueDate: optionalNumber(params.dueDate), + dueDateTime: params.dueDateTime ? true : undefined, startDate: optionalNumber(params.startDate), + startDateTime: params.startDateTime ? true : undefined, timeEstimate: optionalNumber(params.timeEstimate), points: optionalNumber(params.points), parent: params.parent || undefined, + assigneesToAdd: splitCommaSeparatedNumbers(params.assigneesToAdd), + assigneesToRemove: splitCommaSeparatedNumbers(params.assigneesToRemove), archived: params.archiveAction === 'archive' ? true @@ -686,6 +768,10 @@ export const ClickUpBlock: BlockConfig = { includeClosed: params.includeClosed ? true : undefined, archived: params.archived ? true : undefined, statuses: splitCommaSeparated(params.statuses), + assignees: splitCommaSeparated(params.assignees), + tags: splitCommaSeparated(params.tags), + dueDateGt: optionalNumber(params.dueDateGt), + dueDateLt: optionalNumber(params.dueDateLt), } case 'search_tasks': return { @@ -695,9 +781,15 @@ export const ClickUpBlock: BlockConfig = { orderBy: params.orderBy || undefined, reverse: params.reverse ? true : undefined, subtasks: params.subtasks ? true : undefined, + includeClosed: params.includeClosed ? true : undefined, listIds: splitCommaSeparated(params.listIds), spaceIds: splitCommaSeparated(params.spaceIds), folderIds: splitCommaSeparated(params.folderIds), + statuses: splitCommaSeparated(params.statuses), + assignees: splitCommaSeparated(params.assignees), + tags: splitCommaSeparated(params.tags), + dueDateGt: optionalNumber(params.dueDateGt), + dueDateLt: optionalNumber(params.dueDateLt), } case 'create_comment': return { @@ -829,8 +921,18 @@ export const ClickUpBlock: BlockConfig = { status: { type: 'string', description: 'Task status' }, priority: { type: 'string', description: 'Task priority (1-4)' }, dueDate: { type: 'string', description: 'Due date (Unix ms)' }, + dueDateTime: { type: 'boolean', description: 'Whether the due date includes a time of day' }, startDate: { type: 'string', description: 'Start date (Unix ms)' }, + startDateTime: { + type: 'boolean', + description: 'Whether the start date includes a time of day', + }, assignees: { type: 'string', description: 'Comma-separated assignee user IDs' }, + assigneesToAdd: { type: 'string', description: 'Comma-separated user IDs to add as assignees' }, + assigneesToRemove: { + type: 'string', + description: 'Comma-separated user IDs to remove from assignees', + }, tags: { type: 'string', description: 'Comma-separated tag names' }, timeEstimate: { type: 'string', description: 'Time estimate in milliseconds' }, points: { type: 'string', description: 'Sprint points' }, @@ -849,6 +951,8 @@ export const ClickUpBlock: BlockConfig = { includeClosed: { type: 'boolean', description: 'Include closed tasks' }, archived: { type: 'boolean', description: 'Return archived items' }, statuses: { type: 'string', description: 'Comma-separated status filters' }, + dueDateGt: { type: 'string', description: 'Only tasks due after this Unix ms timestamp' }, + dueDateLt: { type: 'string', description: 'Only tasks due before this Unix ms timestamp' }, listIds: { type: 'string', description: 'Comma-separated list ID filters' }, spaceIds: { type: 'string', description: 'Comma-separated space ID filters' }, folderIds: { type: 'string', description: 'Comma-separated folder ID filters' }, diff --git a/apps/sim/lib/api/contracts/tools/clickup.ts b/apps/sim/lib/api/contracts/tools/clickup.ts index 0f8d29342c8..cebc498265f 100644 --- a/apps/sim/lib/api/contracts/tools/clickup.ts +++ b/apps/sim/lib/api/contracts/tools/clickup.ts @@ -11,10 +11,13 @@ export const clickupUploadAttachmentBodySchema = z.object({ const clickupAttachmentSchema = z.object({ id: z.string(), + version: z.string().nullable(), title: z.string().nullable(), extension: z.string().nullable(), url: z.string().nullable(), date: z.number().nullable(), + thumbnailSmall: z.string().nullable(), + thumbnailLarge: z.string().nullable(), }) const clickupUserFileSchema = z diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 5f0a54db88e..62a03fd0a50 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -3355,6 +3355,122 @@ "automation" ] }, + { + "type": "clickup", + "slug": "clickup", + "name": "ClickUp", + "description": "Interact with ClickUp", + "longDescription": "Integrate ClickUp into the workflow. Create, read, update, and delete tasks, manage comments, tags, folders, and lists, upload attachments, and look up workspaces, members, and custom fields.", + "bgColor": "#FFFFFF", + "iconName": "ClickUpIcon", + "docsUrl": "https://docs.sim.ai/integrations/clickup", + "operations": [ + { + "name": "Create Task", + "description": "Create a new task in a ClickUp list" + }, + { + "name": "Get Task", + "description": "Retrieve a task from ClickUp by ID" + }, + { + "name": "Update Task", + "description": "Update an existing task in ClickUp" + }, + { + "name": "Delete Task", + "description": "Delete a task from ClickUp" + }, + { + "name": "Get Tasks", + "description": "List the tasks in a ClickUp list (100 tasks per page; increment page until an empty result to paginate)" + }, + { + "name": "Search Tasks", + "description": "Search tasks across a ClickUp workspace with filters for lists, folders, spaces, statuses, assignees, tags, and due dates (100 tasks per page; increment page until an empty result to paginate)" + }, + { + "name": "Create Comment", + "description": "Add a comment to a ClickUp task" + }, + { + "name": "Get Comments", + "description": "Retrieve comments on a ClickUp task, newest first (25 per page; paginate with start and startId)" + }, + { + "name": "Update Comment", + "description": "Update the content, assignee, or resolved state of a ClickUp task comment" + }, + { + "name": "Delete Comment", + "description": "Delete a comment from a ClickUp task" + }, + { + "name": "Upload Attachment", + "description": "Upload a file to a ClickUp task as an attachment" + }, + { + "name": "Add Tag to Task", + "description": "Add an existing space tag to a ClickUp task" + }, + { + "name": "Remove Tag from Task", + "description": "Remove a tag from a ClickUp task" + }, + { + "name": "Get Space Tags", + "description": "List the task tags available in a ClickUp space" + }, + { + "name": "Get Task Members", + "description": "List the workspace members who have explicit access to a ClickUp task" + }, + { + "name": "Get List Members", + "description": "List the workspace members who have explicit access to a ClickUp list" + }, + { + "name": "Get Custom Fields", + "description": "List the custom fields accessible in a ClickUp list" + }, + { + "name": "Get Workspaces", + "description": "List the ClickUp workspaces (teams) available to the connected account" + }, + { + "name": "Get Spaces", + "description": "List the spaces in a ClickUp workspace" + }, + { + "name": "Get Folders", + "description": "List the folders in a ClickUp space" + }, + { + "name": "Get Lists", + "description": "List the lists in a ClickUp folder, or the folderless lists in a space when a space ID is provided instead" + }, + { + "name": "Create Folder", + "description": "Create a new folder in a ClickUp space" + }, + { + "name": "Create List", + "description": "Create a new list in a ClickUp folder, or a folderless list in a space when a space ID is provided instead" + } + ], + "operationCount": 23, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "oauthServiceId": "clickup", + "category": "tools", + "integrationType": "productivity", + "tags": [ + "project-management", + "ticketing", + "automation" + ] + }, { "type": "cloudflare", "slug": "cloudflare", diff --git a/apps/sim/tools/clickup/create_comment.ts b/apps/sim/tools/clickup/create_comment.ts index 0f54beb37bd..8855583b586 100644 --- a/apps/sim/tools/clickup/create_comment.ts +++ b/apps/sim/tools/clickup/create_comment.ts @@ -4,7 +4,10 @@ import { clickupAuthorizationHeader, extractClickUpErrorMessage, } from '@/tools/clickup/shared' -import type { ClickUpCreateCommentParams, ClickUpCreateCommentResponse } from '@/tools/clickup/types' +import type { + ClickUpCreateCommentParams, + ClickUpCreateCommentResponse, +} from '@/tools/clickup/types' import type { ToolConfig } from '@/tools/types' export const clickupCreateCommentTool: ToolConfig< @@ -51,7 +54,7 @@ export const clickupCreateCommentTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Whether to also notify the comment creator (assignees and watchers are always notified)', + 'When true, comment notifications are sent to everyone, including the comment creator', }, }, diff --git a/apps/sim/tools/clickup/create_folder.ts b/apps/sim/tools/clickup/create_folder.ts index fa67c709610..e8d6c53b474 100644 --- a/apps/sim/tools/clickup/create_folder.ts +++ b/apps/sim/tools/clickup/create_folder.ts @@ -42,8 +42,7 @@ export const clickupCreateFolderTool: ToolConfig - `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/folder`, + url: (params) => `${CLICKUP_API_BASE_URL}/space/${encodeURIComponent(params.spaceId)}/folder`, method: 'POST', headers: (params) => ({ Authorization: clickupAuthorizationHeader(params.accessToken), diff --git a/apps/sim/tools/clickup/create_list.ts b/apps/sim/tools/clickup/create_list.ts index b7f462c3239..5dd011348a6 100644 --- a/apps/sim/tools/clickup/create_list.ts +++ b/apps/sim/tools/clickup/create_list.ts @@ -31,7 +31,8 @@ export const clickupCreateListTool: ToolConfig