← All Modules

assay.clickup

ClickUp client covering sprint execution (tasks, lists, statuses), quarterly targets (Goals), KPI carriers (custom fields), time tracking, comments, and Docs. Everything is API v2 except Docs, which ClickUp only ships on v3.

Client

local clickup = require("assay.clickup")
local c = clickup.client({ token = env.get("CLICKUP_TOKEN") })

clickup.client(opts) accepts:

The token travels in Authorization without a Bearer prefix; ClickUp rejects the prefix on personal tokens.

Workspaces, spaces, folders, lists

API v2 calls a workspace a team, and the module keeps the API's vocabulary.

Tasks

Task pagination is zero-based page, and the envelope ends the walk with last_page = true. List-valued filters (statuses, assignees, tags) are passed as Lua arrays and encode as repeated key[]= parameters.

local open = c.tasks:list(list_id, { statuses = { "to do", "in progress" }, page = 0 })

Comments

ClickUp renders comments as Quill rich text. A string goes out on comment_text and is displayed verbatim, so markdown arrives with its asterisks and pipes intact and a plain @Name tags nobody. Use clickup.rich() for anything beyond a one-line note.

local bharat = clickup.resolve_member(c, team.id, "nsmtech.development@gmail.com")

local body = clickup.rich()
  :bold("Docs cutover: done and live."):br()
  :mention(bharat):text(" — the revision is yours."):br()
  :text("Live at "):link("docs.agentkit.sbs", "https://docs.agentkit.sbs/"):bullet()
  :text("Source: "):code("docs/hextra/content/**"):bullet()
  :text("Review it against the acceptance criteria"):number()

c.comments:create(task_id, body, { notify_all = true })

Builder methods, each returning the builder so calls chain:

The terminator formats the line it closes, because a Quill delta carries line-level attributes on the newline op rather than on the text before it. There is no table type — flatten tabular content into labelled bullets.

clickup.resolve_member(c, team_id, needle) -> user matches a username or email exactly, then falls back to a username substring, and raises rather than guess when several members match. A mention notifies on the numeric id; the @Name text is only a label, so a name that was never resolved against the roster silently reaches no one.

clickup.comment_payload(body) -> table exposes the same normalisation for callers assembling a request by hand.

Goals

Goals carry quarterly targets. Listing is scoped to a workspace; every other operation addresses the goal directly.

Custom fields

Custom fields are the queryable home for KPIs.

Time tracking

Docs (API v3)

Docs are the only resource on v3, which addresses the workspace explicitly rather than calling it a team.

Helpers

local team = clickup.resolve_team(c)
local task = clickup.ensure_task(c, list_id, { name = "Fix token refresh race" })
c.tasks:update(task.id, { status = "in progress" })
c.comments:create(task.id, "Root cause: … Fix: …")

Rate limits

The REST API allows 100 requests per minute per token on the Free, Unlimited, and Business plans. ClickUp's hosted MCP server is a separate, far tighter budget — 50 calls per 24 hours on Free and 300 on paid tiers — so automation belongs on the REST API this module speaks.