Skip to contents

clickrup status badge

Interacting with the ClickUp v2 API from R

ClickUp is a cloud-based collaboration and project management tool. Features include tasks, docs, chat, goals, and more.

The {clickrup} R package wraps the ClickUp API v2. See the roadmap for what is currently included in the package.

Setup

Install the {clickrup} package:

## using remotes
remotes::install_github("psolymos/clickrup")

## with r-universe
options(repos = c(
    psolymos = "https://psolymos.r-universe.dev",
    CRAN = "https://cloud.r-project.org"))
install.packages('clickrup')

Follow this tutorial:

  • sign up for ClickUp (you can use this referral link to do so, it’s free),
  • navigate to your personal Settings,
  • click Apps in the left sidebar,
  • click Generate to create your API token,
  • click Copy to copy the token to your clipboard.

Now add your ClickUp token as an environment variable:

  • open the file .Renviron: file.edit("~/.Renviron"),
  • add a line with CU_PAT="your_token" to the .Renviron file and save it,
  • check with Sys.getenv("CU_PAT"), it should return the token.

The ClickUp token will look something like pk_4753994_EXP7MPOJ7XQM5UJDV2M45MPF0YHH5YHO.

The cu_set_pat function uses Sys.setenv to set the CU_PAT environment variable for other processes called from within R or future calls to Sys.getenv from the same R process.

API endpoints

The first step you want to do is to get the IDs for your workspaces (teams is the legacy term for this in the API).

If your setup was successful, cu_get_teams() should return a list with the workspace IDs and names.

The easiest way to get going with the various endpoints is to browse the API v2 documentation. Once you found what you are after, just look at the API heading. For example, for Get Teams, the R function is prepended with cu_ (to facilitate RStudio code completion), then comes the heading in lower case and spaces replaced by underscores. Similarly, Get Filtered Team Tasks will be cu_get_filtered_team_tasks(). Articles (a, an, the) are dropped (e.g. Create a Time Entry) will be cu_create_time_entry().

Function arguments are the Parameters listed on the API page, ... passes optional parameters, query parameters, or elements for the body.

Examples

Load the package and set the access token if it is not already set. cu_get_pat() returns the token invisibly, so it doesn’t end up in logs.

library(clickrup)
#> clickrup 0.0.5    2021-12-12
cu_get_pat() # returns PAT invisibly

ClickUp hierarchy

The ClickUp hierarchy includes the following levels:

  • Workspaces (teams is the legacy term for this in the API)
  • Spaces
  • Folders
  • Lists
  • Tasks
  • Subtasks
  • Checklists

We can list Workspaces that we have access to using the cu_get_teams() function. It takes no arguments, it only passes the access token behind the scenes, so it is a good way to test if things are working as expected

Teams <- cu_get_teams()
str(Teams, 3)
#> List of 1
#>  $ teams:List of 2
#>   ..$ :List of 5
#>   .. ..$ id     : chr "2253762"
#>   .. ..$ name   : chr "Peter's Workspace"
#>   .. ..$ color  : chr "#40BC86"
#>   .. ..$ avatar : NULL
#>   .. ..$ members:List of 1
#>   ..$ :List of 5
#>   .. ..$ id     : chr "2399763"
#>   .. ..$ name   : chr "My Workspace"
#>   .. ..$ color  : chr "#ea80fc"
#>   .. ..$ avatar : NULL
#>   .. ..$ members:List of 1

Spaces within a specific Workspace defined by its team_id

team_id <- Teams$teams[[2]]$id
Spaces <- cu_get_spaces(team_id)
str(Spaces, 3)
#> List of 1
#>  $ spaces:List of 2
#>   ..$ :List of 7
#>   .. ..$ id                : chr "6331651"
#>   .. ..$ name              : chr "Extras"
#>   .. ..$ private           : logi FALSE
#>   .. ..$ statuses          :List of 4
#>   .. ..$ multiple_assignees: logi TRUE
#>   .. ..$ features          :List of 14
#>   .. ..$ archived          : logi FALSE
#>   ..$ :List of 7
#>   .. ..$ id                : chr "6331576"
#>   .. ..$ name              : chr "Bugs"
#>   .. ..$ private           : logi FALSE
#>   .. ..$ statuses          :List of 2
#>   .. ..$ multiple_assignees: logi FALSE
#>   .. ..$ features          :List of 11
#>   .. ..$ archived          : logi FALSE

Folders within a specific Space defined by its space_id

space_id <- Spaces$spaces[[2]]$id
Folders <- cu_get_folders(space_id)
str(Folders, 3)
#> List of 1
#>  $ folders:List of 1
#>   ..$ :List of 11
#>   .. ..$ id               : chr "12783555"
#>   .. ..$ name             : chr "Bug queues"
#>   .. ..$ orderindex       : int 1
#>   .. ..$ override_statuses: logi TRUE
#>   .. ..$ hidden           : logi FALSE
#>   .. ..$ space            :List of 2
#>   .. ..$ task_count       : chr "10"
#>   .. ..$ archived         : logi FALSE
#>   .. ..$ statuses         :List of 13
#>   .. ..$ lists            :List of 2
#>   .. ..$ permission_level : chr "create"

Lists within a specific Folder defined by its folder_id (or Folderless Lists based on space_id)

folder_id <- Folders$folders[[1]]$id
Lists <- cu_get_lists(folder_id)
str(Lists, 3)
#> List of 1
#>  $ lists:List of 2
#>   ..$ :List of 14
#>   .. ..$ id               : chr "31538033"
#>   .. ..$ name             : chr "Hotfix"
#>   .. ..$ orderindex       : int 2
#>   .. ..$ status           : NULL
#>   .. ..$ priority         : NULL
#>   .. ..$ assignee         : NULL
#>   .. ..$ task_count       : int 5
#>   .. ..$ due_date         : NULL
#>   .. ..$ start_date       : NULL
#>   .. ..$ folder           :List of 4
#>   .. ..$ space            :List of 3
#>   .. ..$ archived         : logi FALSE
#>   .. ..$ override_statuses: logi FALSE
#>   .. ..$ permission_level : chr "create"
#>   ..$ :List of 14
#>   .. ..$ id               : chr "31538032"
#>   .. ..$ name             : chr "Internal Bugs"
#>   .. ..$ orderindex       : int 3
#>   .. ..$ status           : NULL
#>   .. ..$ priority         : NULL
#>   .. ..$ assignee         : NULL
#>   .. ..$ task_count       : int 5
#>   .. ..$ due_date         : NULL
#>   .. ..$ start_date       : NULL
#>   .. ..$ folder           :List of 4
#>   .. ..$ space            :List of 3
#>   .. ..$ archived         : logi FALSE
#>   .. ..$ override_statuses: logi FALSE
#>   .. ..$ permission_level : chr "create"

cu_get_lists_folderless(space_id)
#> $lists
#> list()

The functions cu_get_spaces, cu_get_folders, and cu_get_lists accept the argument archived to return archived spaces/folders/lists

str(cu_get_lists(folder_id, archived=TRUE), 3)
#> List of 1
#>  $ lists:List of 1
#>   ..$ :List of 14
#>   .. ..$ id               : chr "31538034"
#>   .. ..$ name             : chr "Backlog"
#>   .. ..$ orderindex       : int 4
#>   .. ..$ status           : NULL
#>   .. ..$ priority         : NULL
#>   .. ..$ assignee         : NULL
#>   .. ..$ task_count       : int 5
#>   .. ..$ due_date         : NULL
#>   .. ..$ start_date       : NULL
#>   .. ..$ folder           :List of 4
#>   .. ..$ space            :List of 3
#>   .. ..$ archived         : logi TRUE
#>   .. ..$ override_statuses: logi FALSE
#>   .. ..$ permission_level : chr "create"

Tasks

We can list Tasks within a specific List defined by its list_id

list_id <- Lists$lists[[1]]$id
Tasks <- cu_get_tasks(list_id)
str(Tasks, 2)
#> List of 1
#>  $ tasks:List of 5
#>   ..$ :List of 32
#>   ..$ :List of 32
#>   ..$ :List of 32
#>   ..$ :List of 32
#>   ..$ :List of 32

Notice that we have 5 tasks in the list. We can include Subtasks too

subTasks <- cu_get_tasks(list_id, subtasks=TRUE)
length(subTasks$tasks)
#> [1] 7

Getting all the tasks (possibly filtered) in a Workspace is done via the cu_get_filtered_team_tasks function. This function returns tasks in batches of 100. If you don’t want to deal with paging, use the wrapper function cu_get_all_team_tasks The list of tasks returned does not include closed tasks, to get those as well we need to pass the include_closed query parameter

## without closed tasks
length(cu_get_all_team_tasks(team_id, subtasks=TRUE)$tasks)
#> [1] 17

## with closed tasks
allSubTasks <- cu_get_all_team_tasks(team_id, subtasks=TRUE,
                                include_closed=TRUE)
length(allSubTasks$tasks)
#> [1] 18

Status <- sapply(allSubTasks$tasks, function(z) z$status$status)
data.frame(table(Status))
#>          Status Freq
#> 1        Closed    1
#> 2   in progress    3
#> 3 investigating    2
#> 4          Open    5
#> 5    production    1
#> 6        review    4
#> 7        staged    2

Let’s inspect the first few elements of a Task object

str(Tasks$tasks[[1]][1:10])
#> List of 10
#>  $ id          : chr "8ckc8h"
#>  $ custom_id   : NULL
#>  $ name        : chr "Slow speed report"
#>  $ text_content: NULL
#>  $ description : NULL
#>  $ status      :List of 4
#>   ..$ status    : chr "investigating"
#>   ..$ color     : chr "#ff1010"
#>   ..$ type      : chr "custom"
#>   ..$ orderindex: int 2
#>  $ orderindex  : chr "6.00000000000000000000000000000000"
#>  $ date_created: chr "1592452913384"
#>  $ date_updated: chr "1592452913384"
#>  $ date_closed : NULL

Dates are given as Unix time (in milliseconds), cu_date_from and cu_date_to is there to convert back and forth

Tasks$tasks[[1]]$date_created
#> [1] "1592452913384"
cu_date_from(Tasks$tasks[[1]]$date_created)
#> [1] "2020-06-17 22:01:53 MDT"
cu_date_to(cu_date_from(Tasks$tasks[[1]]$date_created))
#> [1] "1592452913384"

A single task can be accessed through the task ID (note: copying the task ID from the ClickUp GUI will prepend the task ID by a hash, "#8ckjp5", but the API expects ID without the hash "8ckjp5"). Use the cu_task_id function to get rid of the leading hash (all API call use this function to normalize task IDs).

The $parent property is NULL for Tasks, and it contains the parent Task ID for Subtasks

x1 <- cu_get_task(cu_task_id("#8ckjp5")) # task
x1$parent
#> NULL

x2 <- cu_get_task("8ckjru") # subtask
x2$parent
#> [1] "8ckjp5"

Dependencies are stored in the $dependencies property

str(x1$dependencies)
#> List of 1
#>  $ :List of 5
#>   ..$ task_id     : chr "8ckjp5"
#>   ..$ depends_on  : chr "8ckjpg"
#>   ..$ type        : int 1
#>   ..$ date_created: chr "1592458378628"
#>   ..$ userid      : chr "4300475"

Helper functions

cu_options stores the API settings

str(cu_options())
#> List of 4
#>  $ baseurl  : chr "https://api.clickup.com"
#>  $ version  : chr "v2"
#>  $ tz       : chr ""
#>  $ useragent: chr "http://github.com/psolymos/clickrup"

cu_response allows inspecting the response object

cu_response(Teams)
#> Response [https://api.clickup.com/api/v2/team]
#>   Date: 2021-12-13 05:23
#>   Status: 200
#>   Content-Type: application/json; charset=utf-8
#>   Size: 814 B

Check rate limits and remaining requests (rate is limited by the minute)

cu_ratelimit(subTasks)
#> $limit
#> [1] 100
#> 
#> $remaining
#> [1] 92

Formatting the request body

PUSH and PUT requests often require to send data as part of the request body. Check the API examples and use I() when the API expects an array as part of the body. For example when passing data to cu_create_task, assignees is an array of the assignees’ userids to be added. Adding a single userid without I() will drop the brackets, but list(name = "New Task Name", assignees = I(183)) will result in the expected JSON object: { "name": "New Task Name", "assignees": [183] }.

Attachments

We can upload files as attachments to tasks:

f <- file.path(tempdir(), "example.png")

png(f)
hist(rnorm(10^6), col=2, main="Example")
dev.off()

cu_post_task_attachment("8ach57", f)

unlink(f) # delete file

License

MIT © 2020 Peter Solymos